diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProvider.java index c33c76fde..2df82b9b1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration; +import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,11 +47,13 @@ * @author Ovidiu Popa * @author Joe Grandja * @author Rafal Lewczuk + * @author Dmitriy Dubson * @since 0.4.0 * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see OidcClientRegistrationAuthenticationToken * @see OidcClientRegistrationAuthenticationProvider + * @see RegisteredClientOidcClientRegistrationConverter * @see 4. Client Configuration Endpoint */ public final class OidcClientConfigurationAuthenticationProvider implements AuthenticationProvider { @@ -58,7 +61,7 @@ public final class OidcClientConfigurationAuthenticationProvider implements Auth private final Log logger = LogFactory.getLog(getClass()); private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; - private final Converter clientRegistrationConverter; + private Converter clientRegistrationConverter; /** * Constructs an {@code OidcClientConfigurationAuthenticationProvider} using the provided parameters. @@ -75,6 +78,17 @@ public OidcClientConfigurationAuthenticationProvider(RegisteredClientRepository this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter(); } + /** + * Sets the {@link Converter} used for converting an {@link RegisteredClient} to a {@link OidcClientRegistration}. + * + * @param clientRegistrationConverter the {@link Converter} used for converting an {@link RegisteredClient} to a {@link OidcClientRegistration} + * @since 1.2.0 + */ + public void setClientRegistrationConverter(Converter clientRegistrationConverter) { + Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null"); + this.clientRegistrationConverter = clientRegistrationConverter; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = 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 fa7a9049e..9bb66511e 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 @@ -17,15 +17,12 @@ import java.net.URI; import java.net.URISyntaxException; -import java.time.Instant; -import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -35,8 +32,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; 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.ClaimAccessor; @@ -46,7 +41,6 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2Token; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -59,8 +53,8 @@ import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames; import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration; -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.oidc.converter.OidcClientRegistrationRegisteredClientConverter; +import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter; import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; @@ -75,6 +69,7 @@ * @author Ovidiu Popa * @author Joe Grandja * @author Rafal Lewczuk + * @author Dmitriy Dubson * @since 0.1.1 * @see RegisteredClientRepository * @see OAuth2AuthorizationService @@ -91,7 +86,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator tokenGenerator; - private final Converter clientRegistrationConverter; + private Converter clientRegistrationConverter; private Converter registeredClientConverter; private PasswordEncoder passwordEncoder; @@ -172,6 +167,17 @@ public void setRegisteredClientConverter(Converter clientRegistrationConverter) { + Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null"); + this.clientRegistrationConverter = clientRegistrationConverter; + } + /** * Sets the {@link PasswordEncoder} used to encode the {@link RegisteredClient#getClientSecret() client secret}. * If not set, the client secret will be encoded using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}. @@ -368,89 +374,4 @@ private static void throwInvalidClientRegistration(String errorCode, String fiel throw new OAuth2AuthenticationException(error); } - 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()); - } - - builder - .clientSettings(clientSettingsBuilder.build()) - .tokenSettings(TokenSettings.builder() - .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) - .build()); - - return builder.build(); - // @formatter:on - } - - } } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java new file mode 100644 index 000000000..e4ce51009 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.oidc.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.util.CollectionUtils; + +import java.time.Instant; +import java.util.Base64; +import java.util.UUID; + +/** + * @author Joe Grandja + * @author Dmitriy Dubson + * @since 1.2.0 + */ +public 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()); + } + + builder + .clientSettings(clientSettingsBuilder.build()) + .tokenSettings(TokenSettings.builder() + .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) + .build()); + + return builder.build(); + // @formatter:on + } + +} + diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/RegisteredClientOidcClientRegistrationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java similarity index 96% rename from oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/RegisteredClientOidcClientRegistrationConverter.java rename to oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java index 7cd62d223..76c433d46 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/RegisteredClientOidcClientRegistrationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.security.oauth2.server.authorization.oidc.authentication; +package org.springframework.security.oauth2.server.authorization.oidc.converter; import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -29,9 +29,9 @@ /** * @author Joe Grandja - * @since 0.4.0 + * @since 1.2.0 */ -final class RegisteredClientOidcClientRegistrationConverter implements Converter { +public final class RegisteredClientOidcClientRegistrationConverter implements Converter { @Override public OidcClientRegistration convert(RegisteredClient registeredClient) { 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 141bfb56e..aaf762bc2 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,11 +17,13 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.List; -import java.util.UUID; +import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; @@ -59,8 +61,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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; @@ -71,7 +71,6 @@ 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,11 +91,12 @@ import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.oidc.converter.OidcClientRegistrationRegisteredClientConverter; +import org.springframework.security.oauth2.server.authorization.oidc.converter.RegisteredClientOidcClientRegistrationConverter; import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter; 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; @@ -131,6 +131,7 @@ * * @author Ovidiu Popa * @author Joe Grandja + * @author Dmitriy Dubson */ @ExtendWith(SpringTestContextExtension.class) public class OidcClientRegistrationTests { @@ -420,6 +421,7 @@ public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredCli .scope("scope2") .claim("custom-metadata-name-1", "value-1") .claim("custom-metadata-name-2", "value-2") + .claim("non-registered-custom-metadata", "value-3") .build(); // @formatter:on @@ -428,10 +430,15 @@ public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredCli RegisteredClient registeredClient = this.registeredClientRepository.findByClientId( clientRegistrationResponse.getClientId()); + assertThat(clientRegistrationResponse.getClaim("custom-metadata-name-1")).isEqualTo("value-1"); + assertThat(clientRegistrationResponse.getClaim("custom-metadata-name-2")).isEqualTo("value-2"); + assertThat(clientRegistrationResponse.getClaim("non-registered-custom-metadata")).isNull(); + assertThat(registeredClient.getClientSettings().getSetting("custom-metadata-name-1")) .isEqualTo("value-1"); assertThat(registeredClient.getClientSettings().getSetting("custom-metadata-name-2")) .isEqualTo("value-2"); + assertThat(registeredClient.getClientSettings().getSetting("non-registered-custom-metadata")).isNull(); } private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception { @@ -581,7 +588,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h oidc .clientRegistrationEndpoint(clientRegistration -> clientRegistration - .authenticationProviders(configureRegisteredClientConverter()) + .authenticationProviders(configureRegisteredClientConverters()) ) ); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); @@ -600,111 +607,18 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h } // @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 - } - + private Consumer> configureRegisteredClientConverters() { + // @formatter:off + return (authenticationProviders) -> + authenticationProviders.forEach(authenticationProvider -> { + List customClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2"); + + if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider provider) { + provider.setRegisteredClientConverter(new CustomRegisteredClientConverter(customClientMetadata)); + provider.setClientRegistrationConverter(new CustomClientRegistrationConverter(customClientMetadata)); + } + }); + // @formatter:on } } @@ -781,4 +695,54 @@ PasswordEncoder passwordEncoder() { } + static class CustomClientRegistrationConverter implements Converter { + private final List customMetadata; + + private final RegisteredClientOidcClientRegistrationConverter delegate; + + CustomClientRegistrationConverter(List customMetadata) { + this.customMetadata = customMetadata; + this.delegate = new RegisteredClientOidcClientRegistrationConverter(); + } + + public OidcClientRegistration convert(RegisteredClient registeredClient) { + var clientRegistration = delegate.convert(registeredClient); + Map claims = new HashMap<>(clientRegistration.getClaims()); + if (!CollectionUtils.isEmpty(customMetadata)) { + ClientSettings clientSettings = registeredClient.getClientSettings(); + + claims.putAll(customMetadata.stream() + .filter(metadatum -> clientSettings.getSetting(metadatum) != null) + .collect(Collectors.toMap(Function.identity(), clientSettings::getSetting))); + } + return OidcClientRegistration.withClaims(claims).build(); + } + } + + static class CustomRegisteredClientConverter implements Converter { + private final List customMetadata; + + private final OidcClientRegistrationRegisteredClientConverter delegate; + + CustomRegisteredClientConverter(List customMetadata) { + this.customMetadata = customMetadata; + this.delegate = new OidcClientRegistrationRegisteredClientConverter(); + } + + public RegisteredClient convert(OidcClientRegistration clientRegistration) { + RegisteredClient convertedClient = delegate.convert(clientRegistration); + ClientSettings.Builder clientSettingsBuilder = ClientSettings + .withSettings(convertedClient.getClientSettings().getSettings()); + + if (!CollectionUtils.isEmpty(this.customMetadata)) { + clientRegistration.getClaims().forEach((claim, value) -> { + if (this.customMetadata.contains(claim)) { + clientSettingsBuilder.setting(claim, value); + } + }); + } + + return RegisteredClient.from(convertedClient).clientSettings(clientSettingsBuilder.build()).build(); + } + } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java index 36f86250f..02f08fc86 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java @@ -378,6 +378,13 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { assertThat(clientRegistrationResult.getRegistrationAccessToken()).isNull(); } + @Test + public void setClientRegistrationConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authenticationProvider.setClientRegistrationConverter(null)) + .withMessage("clientRegistrationConverter cannot be null"); + } + private static Jwt createJwtClientConfiguration() { return createJwt(Collections.singleton("client.read")); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java index c7b04d746..a7357585d 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java @@ -158,6 +158,13 @@ public void setRegisteredClientConverterWhenNullThenThrowIllegalArgumentExceptio .withMessage("registeredClientConverter cannot be null"); } + @Test + public void setClientRegistrationConverterWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authenticationProvider.setClientRegistrationConverter(null)) + .withMessage("clientRegistrationConverter cannot be null"); + } + @Test public void setPasswordEncoderWhenNullThenThrowIllegalArgumentException() { assertThatThrownBy(() -> this.authenticationProvider.setPasswordEncoder(null))