|
|
|
|
@ -17,8 +17,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
@@ -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;
@@ -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;
@@ -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
@@ -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
@@ -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
@@ -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 {
@@ -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().<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 { |
|
|
|
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
|
|
|
|
|
|
|
|
|
@ -530,6 +566,147 @@ public class OidcClientRegistrationTests {
@@ -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<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 |
|
|
|
|
@Configuration(proxyBeanMethods = false) |
|
|
|
|
static class AuthorizationServerConfiguration { |
|
|
|
|
|