1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add support for OAuth 2.0 Dynamic Client Registration Protocol

Closes gh-17964
This commit is contained in:
Joe Grandja
2025-09-19 14:38:25 -04:00
parent 667cd4aa7c
commit f3761aff99
31 changed files with 4520 additions and 437 deletions
@@ -48,6 +48,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
@@ -58,6 +60,7 @@ import org.springframework.security.web.servlet.util.matcher.PathPatternRequestM
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.1 Authorization Server support.
@@ -78,6 +81,7 @@ import org.springframework.util.Assert;
* @see OAuth2TokenRevocationEndpointConfigurer
* @see OAuth2DeviceAuthorizationEndpointConfigurer
* @see OAuth2DeviceVerificationEndpointConfigurer
* @see OAuth2ClientRegistrationEndpointConfigurer
* @see OidcConfigurer
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
@@ -268,6 +272,25 @@ public final class OAuth2AuthorizationServerConfigurer
return this;
}
/**
* Configures the OAuth 2.0 Dynamic Client Registration Endpoint.
* @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access
* to the {@link OAuth2ClientRegistrationEndpointConfigurer}
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
*/
public OAuth2AuthorizationServerConfigurer clientRegistrationEndpoint(
Customizer<OAuth2ClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
OAuth2ClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer == null) {
addConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class,
new OAuth2ClientRegistrationEndpointConfigurer(this::postProcess));
clientRegistrationEndpointConfigurer = getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class);
}
clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
return this;
}
/**
* Configures OpenID Connect 1.0 support (disabled by default).
* @param oidcCustomizer the {@link Customizer} providing access to the
@@ -377,6 +400,12 @@ public final class OAuth2AuthorizationServerConfigurer
httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));
if (getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class) != null) {
httpSecurity
// Accept access tokens for Client Registration
.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
}
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
if (oidcConfigurer != null) {
if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
@@ -392,6 +421,27 @@ public final class OAuth2AuthorizationServerConfigurer
@Override
public void configure(HttpSecurity httpSecurity) {
OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
OAuth2ClientRegistrationEndpointConfigurer.class);
if (clientRegistrationEndpointConfigurer != null) {
OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
OAuth2AuthorizationServerMetadataEndpointConfigurer.class);
authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
String issuer = authorizationServerContext.getIssuer();
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
.getAuthorizationServerSettings();
String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
.path(authorizationServerSettings.getClientRegistrationEndpoint())
.build()
.toUriString();
builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
});
}
this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
@@ -0,0 +1,277 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientRegistrationEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientRegistrationAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OAuth 2.0 Dynamic Client Registration Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2AuthorizationServerConfigurer#clientRegistrationEndpoint
* @see OAuth2ClientRegistrationEndpointFilter
*/
public final class OAuth2ClientRegistrationEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> clientRegistrationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer = (
clientRegistrationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler clientRegistrationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private boolean openRegistrationAllowed;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2ClientRegistrationEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Client
* Registration Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2ClientRegistrationAuthenticationToken} used for authenticating the
* request.
* @param clientRegistrationRequestConverter an {@link AuthenticationConverter} used
* when attempting to extract a Client Registration Request from
* {@link HttpServletRequest}
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationRequestConverter(
AuthenticationConverter clientRegistrationRequestConverter) {
Assert.notNull(clientRegistrationRequestConverter, "clientRegistrationRequestConverter cannot be null");
this.clientRegistrationRequestConverters.add(clientRegistrationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #clientRegistrationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param clientRegistrationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationRequestConverters(
Consumer<List<AuthenticationConverter>> clientRegistrationRequestConvertersConsumer) {
Assert.notNull(clientRegistrationRequestConvertersConsumer,
"clientRegistrationRequestConvertersConsumer cannot be null");
this.clientRegistrationRequestConvertersConsumer = clientRegistrationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2ClientRegistrationAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2ClientRegistrationAuthenticationToken}
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2ClientRegistrationAuthenticationToken} and returning the
* {@link OAuth2ClientRegistration Client Registration Response}.
* @param clientRegistrationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OAuth2ClientRegistrationAuthenticationToken}
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer clientRegistrationResponseHandler(
AuthenticationSuccessHandler clientRegistrationResponseHandler) {
this.clientRegistrationResponseHandler = clientRegistrationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
/**
* Set to {@code true} if open client registration (with no initial access token) is
* allowed. The default is {@code false}.
* @param openRegistrationAllowed {@code true} if open client registration is allowed,
* {@code false} otherwise
* @return the {@link OAuth2ClientRegistrationEndpointConfigurer} for further
* configuration
*/
public OAuth2ClientRegistrationEndpointConfigurer openRegistrationAllowed(boolean openRegistrationAllowed) {
this.openRegistrationAllowed = openRegistrationAllowed;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getClientRegistrationEndpoint())
: authorizationServerSettings.getClientRegistrationEndpoint();
this.requestMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, clientRegistrationEndpointUri);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity,
this.openRegistrationAllowed);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String clientRegistrationEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getClientRegistrationEndpoint())
: authorizationServerSettings.getClientRegistrationEndpoint();
OAuth2ClientRegistrationEndpointFilter clientRegistrationEndpointFilter = new OAuth2ClientRegistrationEndpointFilter(
authenticationManager, clientRegistrationEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.clientRegistrationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
}
this.clientRegistrationRequestConvertersConsumer.accept(authenticationConverters);
clientRegistrationEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.clientRegistrationResponseHandler != null) {
clientRegistrationEndpointFilter.setAuthenticationSuccessHandler(this.clientRegistrationResponseHandler);
}
if (this.errorResponseHandler != null) {
clientRegistrationEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(clientRegistrationEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2ClientRegistrationAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity,
boolean openRegistrationAllowed) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2ClientRegistrationAuthenticationProvider clientRegistrationAuthenticationProvider = new OAuth2ClientRegistrationAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
PasswordEncoder passwordEncoder = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, PasswordEncoder.class);
if (passwordEncoder != null) {
clientRegistrationAuthenticationProvider.setPasswordEncoder(passwordEncoder);
}
clientRegistrationAuthenticationProvider.setOpenRegistrationAllowed(openRegistrationAllowed);
authenticationProviders.add(clientRegistrationAuthenticationProvider);
return authenticationProviders;
}
}
@@ -162,6 +162,7 @@ import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenIntrospection;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@@ -170,6 +171,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationConsentAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
@@ -478,6 +480,18 @@ final class SerializationSamples {
authenticationToken.setDetails(details);
return authenticationToken;
});
OAuth2ClientRegistration oauth2ClientRegistration = OAuth2ClientRegistration.builder()
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.scope("scope1")
.redirectUri("https://localhost/oauth2/callback")
.build();
generatorByClassName.put(OAuth2ClientRegistration.class, (r) -> oauth2ClientRegistration);
generatorByClassName.put(OAuth2ClientRegistrationAuthenticationToken.class, (r) -> {
OAuth2ClientRegistrationAuthenticationToken authenticationToken = new OAuth2ClientRegistrationAuthenticationToken(
principal, oauth2ClientRegistration);
authenticationToken.setDetails(details);
return authenticationToken;
});
OidcClientRegistration oidcClientRegistration = OidcClientRegistration.builder()
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.scope("scope1")
@@ -36,12 +36,14 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
@@ -75,6 +77,9 @@ public class OAuth2AuthorizationServerMetadataTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
@Autowired
private MockMvc mvc;
@@ -156,6 +161,17 @@ public class OAuth2AuthorizationServerMetadataTests {
hasItems("scope1", "scope2")));
}
@Test
public void requestWhenAuthorizationServerMetadataRequestAndClientRegistrationEnabledThenMetadataResponseIncludesRegistrationEndpoint()
throws Exception {
this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$.registration_endpoint")
.value(ISSUER.concat(this.authorizationServerSettings.getClientRegistrationEndpoint())));
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@@ -179,6 +195,11 @@ public class OAuth2AuthorizationServerMetadataTests {
return jwkSource;
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().issuer(ISSUER).build();
@@ -224,4 +245,26 @@ public class OAuth2AuthorizationServerMetadataTests {
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfigurationWithClientRegistrationEnabled
extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
}
@@ -0,0 +1,776 @@
/*
* Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.data.TemporalUnitWithinOffset;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationProvider;
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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
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.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
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.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.converter.OAuth2ClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2ClientRegistrationHttpMessageConverter;
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.web.authentication.OAuth2ClientRegistrationAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.CollectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for OAuth 2.0 Dynamic Client Registration.
*
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
public class OAuth2ClientRegistrationTests {
private static final String ISSUER = "https://example.com:8443/issuer1";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI = "/oauth2/register";
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private static final HttpMessageConverter<OAuth2ClientRegistration> clientRegistrationHttpMessageConverter = new OAuth2ClientRegistrationHttpMessageConverter();
private static EmbeddedDatabase db;
private static JWKSource<SecurityContext> jwkSource;
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
private MockMvc mvc;
@Autowired
private JdbcOperations jdbcOperations;
@Autowired
private RegisteredClientRepository registeredClientRepository;
private static AuthenticationConverter authenticationConverter;
private static Consumer<List<AuthenticationConverter>> authenticationConvertersConsumer;
private static AuthenticationProvider authenticationProvider;
private static Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer;
private static AuthenticationSuccessHandler authenticationSuccessHandler;
private static AuthenticationFailureHandler authenticationFailureHandler;
private MockWebServer server;
@BeforeAll
public static void init() {
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
db = new EmbeddedDatabaseBuilder().generateUniqueName(true)
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript(
"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
authenticationConverter = mock(AuthenticationConverter.class);
authenticationConvertersConsumer = mock(Consumer.class);
authenticationProvider = mock(AuthenticationProvider.class);
authenticationProvidersConsumer = mock(Consumer.class);
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
}
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
given(authenticationProvider.supports(OAuth2ClientRegistrationAuthenticationToken.class)).willReturn(true);
}
@AfterEach
public void tearDown() throws Exception {
this.server.shutdown();
this.jdbcOperations.update("truncate table oauth2_authorization");
this.jdbcOperations.update("truncate table oauth2_registered_client");
reset(authenticationConverter);
reset(authenticationConvertersConsumer);
reset(authenticationProvider);
reset(authenticationProvidersConsumer);
reset(authenticationSuccessHandler);
reset(authenticationFailureHandler);
}
@AfterAll
public static void destroy() {
db.shutdown();
}
@Test
public void requestWhenClientRegistrationRequestAuthorizedThenClientRegistrationResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
}
@Test
public void requestWhenOpenClientRegistrationRequestThenClientRegistrationResponse() throws Exception {
this.spring.register(OpenClientRegistrationConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
MvcResult mvcResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI))
.contentType(MediaType.APPLICATION_JSON)
.content(getClientRegistrationRequestContent(clientRegistration)))
.andExpect(status().isCreated())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andReturn();
OAuth2ClientRegistration clientRegistrationResponse = readClientRegistrationResponse(mvcResult.getResponse());
assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
}
@Test
public void requestWhenClientRegistrationEndpointCustomizedThenUsed() throws Exception {
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
willAnswer((invocation) -> {
HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
new OAuth2ClientRegistrationHttpMessageConverter().write(clientRegistration, null, httpResponse);
return null;
}).given(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
registerClient(clientRegistration);
verify(authenticationConverter).convert(any());
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
assertThat(authenticationConverters).hasSize(2)
.allMatch((converter) -> converter == authenticationConverter
|| converter instanceof OAuth2ClientRegistrationAuthenticationConverter);
verify(authenticationProvider).authenticate(any());
ArgumentCaptor<List<AuthenticationProvider>> authenticationProvidersCaptor = ArgumentCaptor
.forClass(List.class);
verify(authenticationProvidersConsumer).accept(authenticationProvidersCaptor.capture());
List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
assertThat(authenticationProviders).hasSize(2)
.allMatch((provider) -> provider == authenticationProvider
|| provider instanceof OAuth2ClientRegistrationAuthenticationProvider);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
verifyNoInteractions(authenticationFailureHandler);
}
@Test
public void requestWhenClientRegistrationEndpointCustomizedWithAuthenticationFailureHandlerThenUsed()
throws Exception {
this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
given(authenticationProvider.authenticate(any())).willThrow(new OAuth2AuthenticationException("error"));
this.mvc.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).with(jwt()));
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
verifyNoInteractions(authenticationSuccessHandler);
}
@Test
public void requestWhenClientRegistersWithSecretThenClientAuthenticationSuccess() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "scope1")
.with(httpBasic(clientRegistrationResponse.getClientId(),
clientRegistrationResponse.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value("scope1"))
.andReturn();
}
@Test
public void requestWhenClientRegistersWithCustomMetadataThenSavedToRegisteredClient() throws Exception {
this.spring.register(CustomClientMetadataConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.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")
.claim("non-registered-custom-metadata", "value-3")
.build();
// @formatter:on
OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
RegisteredClient registeredClient = this.registeredClientRepository
.findByClientId(clientRegistrationResponse.getClientId());
assertClientRegistrationResponse(clientRegistration, clientRegistrationResponse);
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();
}
@Test
public void requestWhenClientRegistersWithSecretExpirationThenClientRegistrationResponse() throws Exception {
this.spring.register(ClientSecretExpirationConfiguration.class).autowire();
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
OAuth2ClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
Instant expectedSecretExpiryDate = Instant.now().plus(Duration.ofHours(24));
TemporalUnitWithinOffset allowedDelta = new TemporalUnitWithinOffset(1, ChronoUnit.MINUTES);
// Returned response contains expiration date
assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNotNull()
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
RegisteredClient registeredClient = this.registeredClientRepository
.findByClientId(clientRegistrationResponse.getClientId());
// Persisted RegisteredClient contains expiration date
assertThat(registeredClient).isNotNull();
assertThat(registeredClient.getClientSecretExpiresAt()).isNotNull()
.isCloseTo(expectedSecretExpiryDate, allowedDelta);
}
private OAuth2ClientRegistration registerClient(OAuth2ClientRegistration clientRegistration) throws Exception {
// ***** (1) Obtain the "initial" access token used for registering the client
String clientRegistrationScope = "client.create";
// @formatter:off
RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-1")
.clientId("client-registrar-1")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(clientRegistrationScope)
.build();
// @formatter:on
this.registeredClientRepository.save(clientRegistrar);
MvcResult mvcResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
.with(httpBasic("client-registrar-1", "secret")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andExpect(jsonPath("$.scope").value(clientRegistrationScope))
.andReturn();
OAuth2AccessToken accessToken = readAccessTokenResponse(mvcResult.getResponse()).getAccessToken();
// ***** (2) Register the client
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(accessToken.getTokenValue());
// Register the client
mvcResult = this.mvc
.perform(post(ISSUER.concat(DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI)).headers(httpHeaders)
.contentType(MediaType.APPLICATION_JSON)
.content(getClientRegistrationRequestContent(clientRegistration)))
.andExpect(status().isCreated())
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
.andReturn();
return readClientRegistrationResponse(mvcResult.getResponse());
}
private static void assertClientRegistrationResponse(OAuth2ClientRegistration clientRegistrationRequest,
OAuth2ClientRegistration clientRegistrationResponse) {
assertThat(clientRegistrationResponse.getClientId()).isNotNull();
assertThat(clientRegistrationResponse.getClientIdIssuedAt()).isNotNull();
assertThat(clientRegistrationResponse.getClientSecret()).isNotNull();
assertThat(clientRegistrationResponse.getClientSecretExpiresAt()).isNull();
assertThat(clientRegistrationResponse.getClientName()).isEqualTo(clientRegistrationRequest.getClientName());
assertThat(clientRegistrationResponse.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getRedirectUris());
assertThat(clientRegistrationResponse.getGrantTypes())
.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getGrantTypes());
assertThat(clientRegistrationResponse.getResponseTypes())
.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistrationResponse.getScopes())
.containsExactlyInAnyOrderElementsOf(clientRegistrationRequest.getScopes());
assertThat(clientRegistrationResponse.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
}
private static OAuth2AccessTokenResponse readAccessTokenResponse(MockHttpServletResponse response)
throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
}
private static byte[] getClientRegistrationRequestContent(OAuth2ClientRegistration clientRegistration)
throws Exception {
MockHttpOutputMessage httpRequest = new MockHttpOutputMessage();
clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpRequest);
return httpRequest.getBodyAsBytes();
}
private static OAuth2ClientRegistration readClientRegistrationResponse(MockHttpServletResponse response)
throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.clientRegistrationRequestConverter(authenticationConverter)
.clientRegistrationRequestConverters(authenticationConvertersConsumer)
.authenticationProvider(authenticationProvider)
.authenticationProviders(authenticationProvidersConsumer)
.clientRegistrationResponseHandler(authenticationSuccessHandler)
.errorResponseHandler(authenticationFailureHandler)
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class CustomClientMetadataConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
// @formatter:off
return (authenticationProviders) ->
authenticationProviders.forEach((authenticationProvider) -> {
List<String> supportedCustomClientMetadata = List.of("custom-metadata-name-1", "custom-metadata-name-2");
if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
provider.setRegisteredClientConverter(new CustomRegisteredClientConverter(supportedCustomClientMetadata));
provider.setClientRegistrationConverter(new CustomClientRegistrationConverter(supportedCustomClientMetadata));
}
});
// @formatter:on
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class ClientSecretExpirationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.authenticationProviders(configureClientRegistrationConverters())
)
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
private Consumer<List<AuthenticationProvider>> configureClientRegistrationConverters() {
// @formatter:off
return (authenticationProviders) ->
authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OAuth2ClientRegistrationAuthenticationProvider provider) {
provider.setRegisteredClientConverter(new ClientSecretExpirationRegisteredClientConverter());
}
});
// @formatter:on
}
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class OpenClientRegistrationConfiguration extends AuthorizationServerConfiguration {
// @formatter:off
@Bean
@Override
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint((clientRegistration) ->
clientRegistration
.openRegistrationAllowed(true)
)
)
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers("/**/oauth2/register").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
}
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
static class AuthorizationServerConfiguration {
// @formatter:off
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer((authorizationServer) ->
authorizationServer
.clientRegistrationEndpoint(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
);
return http.build();
}
// @formatter:on
@Bean
RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
RegisteredClientParametersMapper registeredClientParametersMapper = new RegisteredClientParametersMapper();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(
jdbcOperations);
registeredClientRepository.setRegisteredClientParametersMapper(registeredClientParametersMapper);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
}
@Bean
JdbcOperations jdbcOperations() {
return new JdbcTemplate(db);
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().multipleIssuersAllowed(true).build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
private static final class CustomRegisteredClientConverter
implements Converter<OAuth2ClientRegistration, RegisteredClient> {
private final OAuth2ClientRegistrationRegisteredClientConverter delegate = new OAuth2ClientRegistrationRegisteredClientConverter();
private final List<String> supportedCustomClientMetadata;
private CustomRegisteredClientConverter(List<String> supportedCustomClientMetadata) {
this.supportedCustomClientMetadata = supportedCustomClientMetadata;
}
@Override
public RegisteredClient convert(OAuth2ClientRegistration clientRegistration) {
RegisteredClient registeredClient = this.delegate.convert(clientRegistration);
ClientSettings.Builder clientSettingsBuilder = ClientSettings
.withSettings(registeredClient.getClientSettings().getSettings());
if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
clientRegistration.getClaims().forEach((claim, value) -> {
if (this.supportedCustomClientMetadata.contains(claim)) {
clientSettingsBuilder.setting(claim, value);
}
});
}
return RegisteredClient.from(registeredClient).clientSettings(clientSettingsBuilder.build()).build();
}
}
private static final class CustomClientRegistrationConverter
implements Converter<RegisteredClient, OAuth2ClientRegistration> {
private final RegisteredClientOAuth2ClientRegistrationConverter delegate = new RegisteredClientOAuth2ClientRegistrationConverter();
private final List<String> supportedCustomClientMetadata;
private CustomClientRegistrationConverter(List<String> supportedCustomClientMetadata) {
this.supportedCustomClientMetadata = supportedCustomClientMetadata;
}
@Override
public OAuth2ClientRegistration convert(RegisteredClient registeredClient) {
OAuth2ClientRegistration clientRegistration = this.delegate.convert(registeredClient);
Map<String, Object> clientMetadata = new HashMap<>(clientRegistration.getClaims());
if (!CollectionUtils.isEmpty(this.supportedCustomClientMetadata)) {
Map<String, Object> clientSettings = registeredClient.getClientSettings().getSettings();
this.supportedCustomClientMetadata.forEach((customClaim) -> {
if (clientSettings.containsKey(customClaim)) {
clientMetadata.put(customClaim, clientSettings.get(customClaim));
}
});
}
return OAuth2ClientRegistration.withClaims(clientMetadata).build();
}
}
/**
* This customization adds client secret expiration time by setting
* {@code RegisteredClient.clientSecretExpiresAt} during
* {@code OAuth2ClientRegistration} -> {@code RegisteredClient} conversion
*/
private static final class ClientSecretExpirationRegisteredClientConverter
implements Converter<OAuth2ClientRegistration, RegisteredClient> {
private static final OAuth2ClientRegistrationRegisteredClientConverter delegate = new OAuth2ClientRegistrationRegisteredClientConverter();
@Override
public RegisteredClient convert(OAuth2ClientRegistration clientRegistration) {
RegisteredClient registeredClient = delegate.convert(clientRegistration);
RegisteredClient.Builder registeredClientBuilder = RegisteredClient.from(registeredClient);
Instant clientSecretExpiresAt = Instant.now().plus(Duration.ofHours(24));
registeredClientBuilder.clientSecretExpiresAt(clientSecretExpiresAt);
return registeredClientBuilder.build();
}
}
}