Add test for dynamic client registration with custom metadata
Issue gh-1172
This commit is contained in:
@@ -17,8 +17,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -39,6 +41,7 @@ import org.mockito.ArgumentCaptor;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
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.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
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.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
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.endpoint.OAuth2ParameterNames;
|
||||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||||
import org.springframework.security.oauth2.jose.TestJwks;
|
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.jose.jws.SignatureAlgorithm;
|
||||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
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.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
||||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
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.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.SpringTestContext;
|
||||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension;
|
import org.springframework.security.oauth2.server.authorization.test.SpringTestContextExtension;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
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.security.web.util.matcher.RequestMatcher;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.MvcResult;
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -400,6 +408,34 @@ public class OidcClientRegistrationTests {
|
|||||||
.andReturn();
|
.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().<String>getSetting("custom-metadata-name-1"))
|
||||||
|
.isEqualTo("value-1");
|
||||||
|
assertThat(registeredClient.getClientSettings().<String>getSetting("custom-metadata-name-2"))
|
||||||
|
.isEqualTo("value-2");
|
||||||
|
}
|
||||||
|
|
||||||
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
||||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||||
|
|
||||||
@@ -530,6 +566,147 @@ public class OidcClientRegistrationTests {
|
|||||||
// @formatter:on
|
// @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<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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class AuthorizationServerConfiguration {
|
static class AuthorizationServerConfiguration {
|
||||||
|
|||||||
Reference in New Issue
Block a user