From 9109d3714b0bea33f9e91d570aaf4683d7c600a2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 2 Oct 2023 06:46:50 +0900 Subject: [PATCH] Fix to return hashed client_secret when registering with client_secret_jwt Closes gh-1344 --- ...entRegistrationAuthenticationProvider.java | 22 +++++--- .../OidcClientRegistrationTests.java | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java index a2ddbe4a..ded933fe 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java @@ -196,22 +196,28 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe } RegisteredClient registeredClient = this.registeredClientConverter.convert(clientRegistrationAuthentication.getClientRegistration()); + RegisteredClient.Builder registeredClientForDBBuilder = RegisteredClient.from(registeredClient); + RegisteredClient.Builder registeredClientForResponseBuilder = RegisteredClient.from(registeredClient); if (StringUtils.hasText(registeredClient.getClientSecret())) { // Encode the client secret - RegisteredClient updatedRegisteredClient = RegisteredClient.from(registeredClient) - .clientSecret(this.passwordEncoder.encode(registeredClient.getClientSecret())) - .build(); - this.registeredClientRepository.save(updatedRegisteredClient); - } else { - this.registeredClientRepository.save(registeredClient); + String encodedClientSecret = this.passwordEncoder.encode(registeredClient.getClientSecret()); + registeredClientForDBBuilder = registeredClientForDBBuilder + .clientSecret(encodedClientSecret); + if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistrationAuthentication.getClientRegistration().getTokenEndpointAuthenticationMethod())) { + registeredClientForResponseBuilder.clientSecret(encodedClientSecret); + } } + this.registeredClientRepository.save(registeredClientForDBBuilder.build()); + + RegisteredClient registeredClientForResponse = registeredClientForResponseBuilder.build(); + if (this.logger.isTraceEnabled()) { this.logger.trace("Saved registered client"); } - OAuth2Authorization registeredClientAuthorization = registerAccessToken(registeredClient); + OAuth2Authorization registeredClientAuthorization = registerAccessToken(registeredClientForResponse); // Invalidate the "initial" access token as it can only be used once authorization = OidcAuthenticationProviderUtils.invalidate(authorization, authorization.getAccessToken().getToken()); @@ -224,7 +230,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe this.logger.trace("Saved authorization with invalidated initial access token"); } - Map clientRegistrationClaims = this.clientRegistrationConverter.convert(registeredClient).getClaims(); + Map clientRegistrationClaims = this.clientRegistrationConverter.convert(registeredClientForResponse).getClaims(); OidcClientRegistration clientRegistration = OidcClientRegistration.withClaims(clientRegistrationClaims) .registrationAccessToken(registeredClientAuthorization.getAccessToken().getToken().getTokenValue()) .build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java index d6122ebb..f19861cf 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java @@ -67,6 +67,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.security.oauth2.jose.TestJwks; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwsHeader; import org.springframework.security.oauth2.jwt.Jwt; @@ -102,6 +103,8 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.util.UriComponentsBuilder; +import javax.crypto.spec.SecretKeySpec; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.ArgumentMatchers.any; @@ -399,6 +402,54 @@ public class OidcClientRegistrationTests { .andReturn(); } + @Test + public void requestWhenClientRegistersWithClientSecretJwtThenClientAuthenticationSuccess() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()) + .scope("scope1") + .scope("scope2") + .build(); + // @formatter:on + + OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); + + JwsHeader jwsHeader = JwsHeader.with(MacAlgorithm.HS256) + .build(); + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS); + JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder() + .issuer(clientRegistrationResponse.getClientId()) + .subject(clientRegistrationResponse.getClientId()) + .audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint()))) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .build(); + + JWKSet jwkSet = new JWKSet(TestJwks.jwk( + new SecretKeySpec(clientRegistrationResponse.getClientSecret().getBytes(), "HS256")).build()); + JwtEncoder jwtClientAssertionEncoder = new NimbusJwtEncoder((jwkSelector, securityContext) -> jwkSelector.select(jwkSet)); + + Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet)); + + this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .param(OAuth2ParameterNames.SCOPE, "scope1") + .param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + .param(OAuth2ParameterNames.CLIENT_ASSERTION, jwtAssertion.getTokenValue()) + .param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").isNotEmpty()) + .andExpect(jsonPath("$.scope").value("scope1")); + } + private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception { // ***** (1) Obtain the "initial" access token used for registering the client