Add support for OAuth 2.0 Dynamic Client Registration Protocol
Closes gh-17964
This commit is contained in:
+50
@@ -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
|
||||
|
||||
+277
@@ -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")
|
||||
|
||||
+43
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+776
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user