oauth2.0 如何正确测试JWT是否未经授权?

m4pnthwp  于 9个月前  发布在  其他
关注(0)|答案(2)|浏览(127)

我已经实现了一个控制器,它在服务器端(Sping Boot )使用JWT进行身份验证,该JWT是客户端通过Apple登录获得的。问题是我不知道如何测试接收到的JWT的算法,受众,issue-uri等是否正确。
我可以通过身份验证来存根JWT并检查状态200,但相反,我不能编写一个在错误的JWT情况下返回状态401的测试。(例如,一个确认收到JWT的测试,但由于issue-uri不正确而返回401)。
下面是环境和实现。

build.gradle.kts

plugins {
    id("org.springframework.boot") version "3.1.4"
    id("io.spring.dependency-management") version "1.1.3"
    id("org.jetbrains.kotlin.plugin.jpa") version "1.8.22"
    kotlin("jvm") version "1.8.22"
    kotlin("plugin.spring") version "1.8.22"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

    implementation("com.auth0:java-jwt:4.4.0")

    implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")

    testImplementation("junit:junit:4.13.2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test:6.1.5")
    testImplementation("com.ninja-squad:springmockk:4.0.2")
    testImplementation("org.wiremock:wiremock:3.3.1")
}

字符串

src/main/resources/application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://appleid.apple.com/auth/keys
          issuer-uri: https://appleid.apple.com
          audiences: ${APPLE_CLIENT_ID}
          jws-algorithms: ${JWS_ALGORITHMS}

src/test/resources/application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://expected.com/keys
          issuer-uri: https://expected.com
          audiences: expected audiences
          jws-algorithms: RS256

SecurityServerConfig.kt

@Configuration
class OAuth2ResourceServerSecurityConfig(
    private val tokenService: DefaultTokenService
) {
    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf {
                it.ignoringRequestMatchers("/api/**")
            }
            .authorizeHttpRequests {
                it.requestMatchers(
                    "/api/token/apple/code"
                ).authenticated()

                it.anyRequest().permitAll()
            }
            .oauth2ResourceServer {
                it.jwt {}
            }
            .authenticationManager {
                val bearerToken = it as BearerTokenAuthenticationToken
                val user = tokenService.parseToken(bearerToken.token)
                    ?: throw InvalidBearerTokenException("Invalid token")
                UsernamePasswordAuthenticationToken(
                    user,
                    "",
                    listOf(SimpleGrantedAuthority("USER"))
                )
            }
        return http.build()
    }
}

DefaultTokenService.kt

@Service
class DefaultTokenService(
    private val jwtDecoder: JwtDecoder,
) : TokenService {
    override fun parseToken(bearerToken: String): AppleUser? {
        return try {
            val jwt = jwtDecoder.decode(bearerToken)
            val userId = jwt.claims["sub"] as String
            val email = jwt.claims["email"] as String
            DefaultAppleUser(userId, email)
        } catch (e: Exception) {
            null
        }
    }

    // more implements...

TokenController.kt

@RestController
@RequestMapping("/api/token")
class TokenController(
    private val tokenService: TokenService
) {
    @GetMapping("/apple/code")
    fun getRefreshTokenOfApple(
        authentication: Authentication,
        @RequestParam authorizationCode: String,
    ): AppleTokenResponse? {
        return tokenService.getRefreshToken(authorizationCode)
    }

    // more implements...
}


这是控制器测试
TokenControllerTests.kt

@SpringBootTest
@AutoConfigureMockMvc
class TokenControllerTest{
    @Autowired
    private lateinit var mockMvc: MockMvc

    @SpykBean
    private lateinit var spyStubTokenService: DefaultTokenService

    @Nested
    inner class GetRefreshTokenOfApple {
        @Test
        fun `when given jwt is invalid, status is unauthorized`() {
            val result = mockMvc.perform(
                MockMvcRequestBuilders
                    .get("/api/token/apple/code")
                    .param("authorizationCode", "authorization code 1")
                    .with(csrf())
                    .with(jwt().jwt {
                        it.issuer("https://invalid.com")
                        it.audience(listOf("com.invalid.app"))
                        it.header("alg", "RS255")
                    })
            )

            result.andExpect(MockMvcResultMatchers.status().isUnauthorized)
        }

        @Test
        fun `status is 200 and passes correct argument to token service`() {
            val result = mockMvc.perform(
                MockMvcRequestBuilders
                    .get("/api/token/apple/code")
                    .param("authorizationCode", "authorization code 1")
                    .with(jwt().jwt {
                        it.issuer("https://expected.com")
                        it.audience(listOf("com.expected.app"))
                        it.header("alg", "RS256")
                    })
            )

            result.andExpect(MockMvcResultMatchers.status().isOk)
            verify {
                spyStubTokenService.getRefreshToken(
                    "authorization code 1"
                )
            }
        }
    }


第一个预期未授权的测试将失败,因为它将返回状态200。我们希望确保正确检查JWT内容,并测试JWT身份验证是否正确完成。
我想最好能在src/test/resources/application.yml中测试spring.security.oauth2.resourceserver.jwt作为期望值。这是通常不需要首先测试的东西吗?
如果有人知道一个更好的方法来做到这一点,我会很感激。
(我已经试过了)

1. JwtDecoder,带@SpykBean注解。

@SpykBean
private late init var jwtDecoder: JwtDecoder


我发现这样可以在测试中始终通过认证,但不能擅自测试。

**2.我根据this article**编写了一个测试,但最终无法正确测试未经授权的内容。

jucafojl

jucafojl1#

我认为这实际上是一个很好的问题,因为开发人员应该能够经常测试他们的API的所有安全条件。这应该确保技术安全性,例如只允许有效的发行者,以及JWT验证后的授权条件。这些操作还确保API是以客户端为中心的,例如它返回有用的OAuth错误响应。

与模拟JWT访问令牌的集成测试

使用您提到的mock技术,必须使用与真实的令牌相同的access token contract。这应该使用相同的JWT标头和有效负载值。API中的安全代码不应更改。相反,API仅指向mock JSON Web Key Set(JWKS)URI。
然后,您可以编写这样的代码,以测试API身份验证和API授权条件,并Assert客户端的预期错误响应。每个API测试都可以快速获取任何用户的令牌,因此开发人员的设置是高效的。

@Test
public void CallApi_Returns401_ForInvalidIssuer() throws Throwable {

    var jwtOptions = new MockTokenOptions();
    jwtOptions.useStandardUser();
    jwtOptions.setIssuer("https://otherissuer.com");
    var accessToken = authorizationServer.issueAccessToken(jwtOptions);

    var apiOptions = new ApiRequestOptions(accessToken);
    var response = apiClient.getCompanies(apiOptions).join();
    Assertions.assertEquals(401, response.getStatusCode(), "Unexpected HTTP status");

    // TODO: read error and error_description from the www-authenticate response header
}

字符串

示例代码

要进行比较,可以运行我的these example tests。他们使用mock authorization server class来处理技术设置,JWT库做较低级别的工作。下面是我的示例API的测试结果:


的数据

其他测试技术

当然,测试API安全性还有很多其他方法,不同的开发人员有不同的偏好。通常使用单元测试进行大多数测试。但就我个人而言,我也喜欢进行一些能够发出真实的HTTP请求的测试,以证明端到端基础设施正在按预期工作。

ig9co6j1

ig9co6j12#

MockMvc适用于“单元”测试,但您尝试做的更多是“端到端”测试:您希望测试外部授权服务器交付的真实的JWT是否被您没有编写的JWT解码器接受或拒绝(您只是配置了它)。

使用MockMvc,访问令牌解析和解码(或内省)的整个过程都被跳过:MockMvc请求后处理器被设计为使用身份验证管理器的结果(Authentication示例,默认情况下,对于具有JWT解码器的资源服务器来说是JwtAuthenticationToken)填充测试安全上下文。

关于MockMvc的安全性,你应该“单元”测试的是访问控制规则

相关问题