Adds ability to inject custom metadata at client registration
Closes gh-1172
This commit is contained in:
committed by
Joe Grandja
parent
99cd1b8d1e
commit
3de6a7dfd1
@@ -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.OAuth2TokenType;
|
||||
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 @@ import org.springframework.util.StringUtils;
|
||||
* @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 <a href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint">4. Client Configuration Endpoint</a>
|
||||
*/
|
||||
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<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
private Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientConfigurationAuthenticationProvider} using the provided parameters.
|
||||
@@ -75,6 +78,17 @@ public final class OidcClientConfigurationAuthenticationProvider implements Auth
|
||||
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<RegisteredClient, OidcClientRegistration> clientRegistrationConverter) {
|
||||
Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null");
|
||||
this.clientRegistrationConverter = clientRegistrationConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication =
|
||||
|
||||
@@ -17,15 +17,12 @@ package org.springframework.security.oauth2.server.authorization.oidc.authentica
|
||||
|
||||
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.authentication.AuthenticationProvider;
|
||||
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.OAuth2AuthenticationException;
|
||||
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.client.Registere
|
||||
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 @@ import org.springframework.util.StringUtils;
|
||||
* @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<? extends OAuth2Token> tokenGenerator;
|
||||
private final Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
private Converter<RegisteredClient, OidcClientRegistration> clientRegistrationConverter;
|
||||
private Converter<OidcClientRegistration, RegisteredClient> registeredClientConverter;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@@ -172,6 +167,17 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.registeredClientConverter = registeredClientConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<RegisteredClient, OidcClientRegistration> 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 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
|
||||
private static final class OidcClientRegistrationRegisteredClientConverter implements Converter<OidcClientRegistration, RegisteredClient> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OidcClientRegistration, RegisteredClient> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.4.0
|
||||
* @since 1.2.0
|
||||
*/
|
||||
final class RegisteredClientOidcClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> {
|
||||
public final class RegisteredClientOidcClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> {
|
||||
|
||||
@Override
|
||||
public OidcClientRegistration convert(RegisteredClient registeredClient) {
|
||||
@@ -17,11 +17,13 @@ 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.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.Customizer;
|
||||
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.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,11 +91,12 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
|
||||
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 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @author Dmitriy Dubson
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class OidcClientRegistrationTests {
|
||||
@@ -420,6 +421,7 @@ public class OidcClientRegistrationTests {
|
||||
.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 class OidcClientRegistrationTests {
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
|
||||
clientRegistrationResponse.getClientId());
|
||||
|
||||
assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-1")).isEqualTo("value-1");
|
||||
assertThat(clientRegistrationResponse.<String>getClaim("custom-metadata-name-2")).isEqualTo("value-2");
|
||||
assertThat(clientRegistrationResponse.<String>getClaim("non-registered-custom-metadata")).isNull();
|
||||
|
||||
assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-1"))
|
||||
.isEqualTo("value-1");
|
||||
assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-2"))
|
||||
.isEqualTo("value-2");
|
||||
assertThat(registeredClient.getClientSettings().<String>getSetting("non-registered-custom-metadata")).isNull();
|
||||
}
|
||||
|
||||
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
||||
@@ -581,7 +588,7 @@ public class OidcClientRegistrationTests {
|
||||
oidc
|
||||
.clientRegistrationEndpoint(clientRegistration ->
|
||||
clientRegistration
|
||||
.authenticationProviders(configureRegisteredClientConverter())
|
||||
.authenticationProviders(configureRegisteredClientConverters())
|
||||
)
|
||||
);
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
@@ -600,111 +607,18 @@ public class OidcClientRegistrationTests {
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private Consumer<List<AuthenticationProvider>> 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<OidcClientRegistration, RegisteredClient> {
|
||||
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<List<AuthenticationProvider>> configureRegisteredClientConverters() {
|
||||
// @formatter:off
|
||||
return (authenticationProviders) ->
|
||||
authenticationProviders.forEach(authenticationProvider -> {
|
||||
List<String> 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 @@ public class OidcClientRegistrationTests {
|
||||
|
||||
}
|
||||
|
||||
static class CustomClientRegistrationConverter implements Converter<RegisteredClient, OidcClientRegistration> {
|
||||
private final List<String> customMetadata;
|
||||
|
||||
private final RegisteredClientOidcClientRegistrationConverter delegate;
|
||||
|
||||
CustomClientRegistrationConverter(List<String> customMetadata) {
|
||||
this.customMetadata = customMetadata;
|
||||
this.delegate = new RegisteredClientOidcClientRegistrationConverter();
|
||||
}
|
||||
|
||||
public OidcClientRegistration convert(RegisteredClient registeredClient) {
|
||||
var clientRegistration = delegate.convert(registeredClient);
|
||||
Map<String, Object> 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<OidcClientRegistration, RegisteredClient> {
|
||||
private final List<String> customMetadata;
|
||||
|
||||
private final OidcClientRegistrationRegisteredClientConverter delegate;
|
||||
|
||||
CustomRegisteredClientConverter(List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +378,13 @@ public class OidcClientConfigurationAuthenticationProviderTests {
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -158,6 +158,13 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
.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))
|
||||
|
||||
Reference in New Issue
Block a user