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 ca178ed6..900b8584 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 @@ -17,8 +17,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.function.Consumer; import jakarta.servlet.http.HttpServletResponse; @@ -39,6 +41,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -58,6 +61,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -68,6 +73,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; @@ -92,6 +98,7 @@ import org.springframework.security.oauth2.server.authorization.oidc.http.conver import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.oauth2.server.authorization.test.SpringTestContext; import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension; import org.springframework.security.web.SecurityFilterChain; @@ -101,6 +108,7 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.CollectionUtils; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -400,6 +408,34 @@ public class OidcClientRegistrationTests { .andReturn(); } + @Test + public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredClient() throws Exception { + this.spring.register(CustomClientMetadataConfiguration.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()) + .scope("scope1") + .scope("scope2") + .claim("custom-metadata-name-1", "value-1") + .claim("custom-metadata-name-2", "value-2") + .build(); + // @formatter:on + + OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); + + RegisteredClient registeredClient = this.registeredClientRepository.findByClientId( + clientRegistrationResponse.getClientId()); + + assertThat(registeredClient.getClientSettings().getSetting("custom-metadata-name-1")) + .isEqualTo("value-1"); + assertThat(registeredClient.getClientSettings().getSetting("custom-metadata-name-2")) + .isEqualTo("value-2"); + } + private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception { // ***** (1) Obtain the "initial" access token used for registering the client @@ -530,6 +566,147 @@ public class OidcClientRegistrationTests { // @formatter:on } + @EnableWebSecurity + @Configuration(proxyBeanMethods = false) + static class CustomClientMetadataConfiguration extends AuthorizationServerConfiguration { + + // @formatter:off + @Bean + @Override + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + new OAuth2AuthorizationServerConfigurer(); + authorizationServerConfigurer + .oidc(oidc -> + oidc + .clientRegistrationEndpoint(clientRegistration -> + clientRegistration + .authenticationProviders(configureRegisteredClientConverter()) + ) + ); + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http + .securityMatcher(endpointsMatcher) + .authorizeHttpRequests(authorize -> + authorize.anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .apply(authorizationServerConfigurer); + return http.build(); + } + // @formatter:on + + private Consumer> configureRegisteredClientConverter() { + return (authenticationProviders) -> { + authenticationProviders.forEach((authenticationProvider) -> { + if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider) { + ((OidcClientRegistrationAuthenticationProvider) authenticationProvider) + .setRegisteredClientConverter(new OidcClientRegistrationRegisteredClientConverter()); + } + }); + }; + } + + // NOTE: + // This is a copy of OidcClientRegistrationAuthenticationProvider.OidcClientRegistrationRegisteredClientConverter + // with a minor enhancement supporting custom metadata claims. + private static final class OidcClientRegistrationRegisteredClientConverter implements Converter { + private static final StringKeyGenerator CLIENT_ID_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 32); + private static final StringKeyGenerator CLIENT_SECRET_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 48); + + @Override + public RegisteredClient convert(OidcClientRegistration clientRegistration) { + // @formatter:off + RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId(CLIENT_ID_GENERATOR.generateKey()) + .clientIdIssuedAt(Instant.now()) + .clientName(clientRegistration.getClientName()); + + if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + builder + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .clientSecret(CLIENT_SECRET_GENERATOR.generateKey()); + } else if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + builder + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) + .clientSecret(CLIENT_SECRET_GENERATOR.generateKey()); + } else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + builder.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT); + } else { + builder + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientSecret(CLIENT_SECRET_GENERATOR.generateKey()); + } + + builder.redirectUris(redirectUris -> + redirectUris.addAll(clientRegistration.getRedirectUris())); + + if (!CollectionUtils.isEmpty(clientRegistration.getPostLogoutRedirectUris())) { + builder.postLogoutRedirectUris(postLogoutRedirectUris -> + postLogoutRedirectUris.addAll(clientRegistration.getPostLogoutRedirectUris())); + } + + if (!CollectionUtils.isEmpty(clientRegistration.getGrantTypes())) { + builder.authorizationGrantTypes(authorizationGrantTypes -> + clientRegistration.getGrantTypes().forEach(grantType -> + authorizationGrantTypes.add(new AuthorizationGrantType(grantType)))); + } else { + builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE); + } + if (CollectionUtils.isEmpty(clientRegistration.getResponseTypes()) || + clientRegistration.getResponseTypes().contains(OAuth2AuthorizationResponseType.CODE.getValue())) { + builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE); + } + + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + builder.scopes(scopes -> + scopes.addAll(clientRegistration.getScopes())); + } + + ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder() + .requireProofKey(true) + .requireAuthorizationConsent(true); + + if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + MacAlgorithm macAlgorithm = MacAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()); + if (macAlgorithm == null) { + macAlgorithm = MacAlgorithm.HS256; + } + clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(macAlgorithm); + } else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()); + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.RS256; + } + clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(signatureAlgorithm); + clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString()); + } + + // Add custom metadata claims + clientRegistration.getClaims().forEach((claim, value) -> { + if (claim.startsWith("custom-metadata")) { + clientSettingsBuilder.setting(claim, value); + } + }); + + builder + .clientSettings(clientSettingsBuilder.build()) + .tokenSettings(TokenSettings.builder() + .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) + .build()); + + return builder.build(); + // @formatter:on + } + + } + + } + @EnableWebSecurity @Configuration(proxyBeanMethods = false) static class AuthorizationServerConfiguration {