如何正确实现 client_secret_jwt Token 请求自定义 DefaultClientCredentialsTokenResponseClient

问题描述 投票:0回答:0

我想实现 client_secret_jwt 客户端认证。在 Spring 安全文档中说

要仅自定义请求的参数,您可以提供带有自定义转换器的 OAuth2ClientCredentialsGrantRequestEntityConverter.setParametersConverter()> 以完全覆盖随请求发送的参数。这通常比直接构建 RequestEntity 更简单。

举个例子:https://docs.spring.io/spring-security/reference/servlet/oauth2/client/client-authentication.html#_authenticate_using_client_secret_jwt

但是在哪里做呢?

我发现的解决方案是复制类的代码:ClientCredentialsOAuth2AuthorizedClientProvider 在新类 JWKClientCredentialsOAuth2AuthorizedClientProvider 中并更改 accessTokenResponseClient 的初始化:

class JWKClientCredentialsOAuth2AuthorizedClientProvider : OAuth2AuthorizedClientProvider{
    private var accessTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> =
        DefaultClientCredentialsTokenResponseClient()
    private var clockSkew = Duration.ofSeconds(60)
    private var clock = Clock.systemUTC()
     constructor() {
         val jwkResolver = Function<ClientRegistration, JWK?> { clientRegistration: ClientRegistration ->
             if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) {
                 val secretKey = SecretKeySpec(
                     clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8),
                     "HmacSHA256"
                 )
                 OctetSequenceKey.Builder(secretKey)
                     .keyID(UUID.randomUUID().toString())
                     .build()
             }
             null
         }

         val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter()
         requestEntityConverter.addParametersConverter(
             NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
         )


         (accessTokenResponseClient as DefaultClientCredentialsTokenResponseClient).setRequestEntityConverter(requestEntityConverter)

     }


    /**
     * Attempt to authorize (or re-authorize) the
     * [client][OAuth2AuthorizationContext.getClientRegistration] in the provided
     * `context`. Returns `null` if authorization (or re-authorization) is not
     * supported, e.g. the client's [ authorization grant type][ClientRegistration.getAuthorizationGrantType] is not [ client_credentials][AuthorizationGrantType.CLIENT_CREDENTIALS] OR the [access][OAuth2AuthorizedClient.getAccessToken] is not expired.
     * @param context the context that holds authorization-specific state for the client
     * @return the [OAuth2AuthorizedClient] or `null` if authorization (or
     * re-authorization) is not supported
     */
    @Nullable
    override fun authorize(context: OAuth2AuthorizationContext): OAuth2AuthorizedClient? {
        Assert.notNull(context, "context cannot be null")
        val clientRegistration = context.clientRegistration
        if (AuthorizationGrantType.CLIENT_CREDENTIALS != clientRegistration.authorizationGrantType) {
            return null
        }
        val authorizedClient = context.authorizedClient
        if (authorizedClient != null && !hasTokenExpired(authorizedClient.accessToken)) {
            // If client is already authorized but access token is NOT expired than no
            // need for re-authorization
            return null
        }
        // As per spec, in section 4.4.3 Access Token Response
        // https://tools.ietf.org/html/rfc6749#section-4.4.3
        // A refresh token SHOULD NOT be included.
        //
        // Therefore, renewing an expired access token (re-authorization)
        // is the same as acquiring a new access token (authorization).
        val clientCredentialsGrantRequest = OAuth2ClientCredentialsGrantRequest(
            clientRegistration
        )
        val tokenResponse = getTokenResponse(clientRegistration, clientCredentialsGrantRequest)
        return OAuth2AuthorizedClient(
            clientRegistration, context.principal.name,
            tokenResponse.accessToken
        )
    }

    private fun getTokenResponse(
        clientRegistration: ClientRegistration,
        clientCredentialsGrantRequest: OAuth2ClientCredentialsGrantRequest
    ): OAuth2AccessTokenResponse {
        return try {
            accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest)
        } catch (ex: OAuth2AuthorizationException) {
            throw ClientAuthorizationException(ex.error, clientRegistration.registrationId, ex)
        }
    }

    private fun hasTokenExpired(token: OAuth2Token): Boolean {
        return clock.instant().isAfter(token.expiresAt!!.minus(clockSkew))
    }

    /**
     * Sets the client used when requesting an access token credential at the Token
     * Endpoint for the `client_credentials` grant.
     * @param accessTokenResponseClient the client used when requesting an access token
     * credential at the Token Endpoint for the `client_credentials` grant
     */
    fun setAccessTokenResponseClient(
        accessTokenResponseClient: OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest>
    ) {
        Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null")
        this.accessTokenResponseClient = accessTokenResponseClient
    }

    /**
     * Sets the maximum acceptable clock skew, which is used when checking the
     * [access token][OAuth2AuthorizedClient.getAccessToken] expiry. The default is
     * 60 seconds.
     *
     *
     *
     * An access token is considered expired if
     * `OAuth2AccessToken#getExpiresAt() - clockSkew` is before the current time
     * `clock#instant()`.
     * @param clockSkew the maximum acceptable clock skew
     */
    fun setClockSkew(clockSkew: Duration) {
        Assert.notNull(clockSkew, "clockSkew cannot be null")
        Assert.isTrue(clockSkew.seconds >= 0, "clockSkew must be >= 0")
        this.clockSkew = clockSkew
    }

    /**
     * Sets the [Clock] used in [Instant.now] when checking the access
     * token expiry.
     * @param clock the clock
     */
    fun setClock(clock: Clock) {
        Assert.notNull(clock, "clock cannot be null")
        this.clock = clock
    }
}

然后像这样更改 AuthorizedClientManager:

   @Bean
    fun authorizedClientManager(clientRegistrationRepository : ClientRegistrationRepository, oAuth2AuthorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager {
        val authorizedClientProvider = JWKClientCredentialsOAuth2AuthorizedClientProvider()
        val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager(
            clientRegistrationRepository,
            oAuth2AuthorizedClientService
        )
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
        return authorizedClientManager
    }

但我很确定有更好更聪明的方法来做到这一点

spring spring-security spring-security-oauth2 spring-boot-starter-oauth2-client
© www.soinside.com 2019 - 2024. All rights reserved.