Add support for OAuth 2.0 Dynamic Client Registration Protocol
CodeQL Advanced / codeql-analysis-call (push) Waiting to run Details
CI / Build (17, ubuntu-latest) (push) Waiting to run Details
CI / Build (17, windows-latest) (push) Waiting to run Details
CI / Deploy Artifacts (push) Blocked by required conditions Details
CI / Deploy Docs (push) Blocked by required conditions Details
CI / Deploy Schema (push) Blocked by required conditions Details
CI / Perform Release (push) Blocked by required conditions Details
CI / Send Notification (push) Blocked by required conditions Details
Deploy Docs / build (push) Waiting to run Details

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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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")

View File

@ -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
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,367 @@
/*
* 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.oauth2.server.authorization;
import java.io.Serial;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.util.Assert;
/**
* A base representation of an OAuth 2.0 Client Registration Request and Response, which
* is sent to and returned from the Client Registration Endpoint, and contains a set of
* claims about the Client's Registration information. The claims are defined by the OAuth
* 2.0 Dynamic Client Registration Protocol specification.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2ClientMetadataClaimAccessor
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-3.1">3.1. Client Registration
* Request</a>
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1">3.2.1. Client
* Registration Response</a>
*/
public abstract class AbstractOAuth2ClientRegistration implements OAuth2ClientMetadataClaimAccessor, Serializable {
@Serial
private static final long serialVersionUID = 8042785346181558593L;
private final Map<String, Object> claims;
protected AbstractOAuth2ClientRegistration(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
/**
* Returns the metadata as claims.
* @return a {@code Map} of the metadata as claims
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
/**
* A builder for subclasses of {@link AbstractOAuth2ClientRegistration}.
*
* @param <T> the type of object
* @param <B> the type of the builder
*/
protected abstract static class AbstractBuilder<T extends AbstractOAuth2ClientRegistration, B extends AbstractBuilder<T, B>> {
private final Map<String, Object> claims = new LinkedHashMap<>();
protected AbstractBuilder() {
}
protected Map<String, Object> getClaims() {
return this.claims;
}
@SuppressWarnings("unchecked")
protected final B getThis() {
// avoid unchecked casts in subclasses by using "getThis()" instead of "(B)
// this"
return (B) this;
}
/**
* Sets the Client Identifier, REQUIRED.
* @param clientId the Client Identifier
* @return the {@link AbstractBuilder} for further configuration
*/
public B clientId(String clientId) {
return claim(OAuth2ClientMetadataClaimNames.CLIENT_ID, clientId);
}
/**
* Sets the time at which the Client Identifier was issued, OPTIONAL.
* @param clientIdIssuedAt the time at which the Client Identifier was issued
* @return the {@link AbstractBuilder} for further configuration
*/
public B clientIdIssuedAt(Instant clientIdIssuedAt) {
return claim(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, clientIdIssuedAt);
}
/**
* Sets the Client Secret, OPTIONAL.
* @param clientSecret the Client Secret
* @return the {@link AbstractBuilder} for further configuration
*/
public B clientSecret(String clientSecret) {
return claim(OAuth2ClientMetadataClaimNames.CLIENT_SECRET, clientSecret);
}
/**
* Sets the time at which the {@code client_secret} will expire or {@code null} if
* it will not expire, REQUIRED if {@code client_secret} was issued.
* @param clientSecretExpiresAt the time at which the {@code client_secret} will
* expire or {@code null} if it will not expire
* @return the {@link AbstractBuilder} for further configuration
*/
public B clientSecretExpiresAt(Instant clientSecretExpiresAt) {
return claim(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
}
/**
* Sets the name of the Client to be presented to the End-User, OPTIONAL.
* @param clientName the name of the Client to be presented to the End-User
* @return the {@link AbstractBuilder} for further configuration
*/
public B clientName(String clientName) {
return claim(OAuth2ClientMetadataClaimNames.CLIENT_NAME, clientName);
}
/**
* Add the redirection {@code URI} used by the Client, REQUIRED for redirect-based
* flows.
* @param redirectUri the redirection {@code URI} used by the Client
* @return the {@link AbstractBuilder} for further configuration
*/
public B redirectUri(String redirectUri) {
addClaimToClaimList(OAuth2ClientMetadataClaimNames.REDIRECT_URIS, redirectUri);
return getThis();
}
/**
* A {@code Consumer} of the redirection {@code URI} values used by the Client,
* allowing the ability to add, replace, or remove, REQUIRED for redirect-based
* flows.
* @param redirectUrisConsumer a {@code Consumer} of the redirection {@code URI}
* values used by the Client
* @return the {@link AbstractBuilder} for further configuration
*/
public B redirectUris(Consumer<List<String>> redirectUrisConsumer) {
acceptClaimValues(OAuth2ClientMetadataClaimNames.REDIRECT_URIS, redirectUrisConsumer);
return getThis();
}
/**
* Sets the authentication method used by the Client for the Token Endpoint,
* OPTIONAL.
* @param tokenEndpointAuthenticationMethod the authentication method used by the
* Client for the Token Endpoint
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenEndpointAuthenticationMethod(String tokenEndpointAuthenticationMethod) {
return claim(OAuth2ClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
}
/**
* Add the OAuth 2.0 {@code grant_type} that the Client will restrict itself to
* using, OPTIONAL.
* @param grantType the OAuth 2.0 {@code grant_type} that the Client will restrict
* itself to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B grantType(String grantType) {
addClaimToClaimList(OAuth2ClientMetadataClaimNames.GRANT_TYPES, grantType);
return getThis();
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code grant_type} values that the Client
* will restrict itself to using, allowing the ability to add, replace, or remove,
* OPTIONAL.
* @param grantTypesConsumer a {@code Consumer} of the OAuth 2.0
* {@code grant_type} values that the Client will restrict itself to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
acceptClaimValues(OAuth2ClientMetadataClaimNames.GRANT_TYPES, grantTypesConsumer);
return getThis();
}
/**
* Add the OAuth 2.0 {@code response_type} that the Client will restrict itself to
* using, OPTIONAL.
* @param responseType the OAuth 2.0 {@code response_type} that the Client will
* restrict itself to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B responseType(String responseType) {
addClaimToClaimList(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, responseType);
return getThis();
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code response_type} values that the
* Client will restrict itself to using, allowing the ability to add, replace, or
* remove, OPTIONAL.
* @param responseTypesConsumer a {@code Consumer} of the OAuth 2.0
* {@code response_type} values that the Client will restrict itself to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
acceptClaimValues(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, responseTypesConsumer);
return getThis();
}
/**
* Add the OAuth 2.0 {@code scope} that the Client will restrict itself to using,
* OPTIONAL.
* @param scope the OAuth 2.0 {@code scope} that the Client will restrict itself
* to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B scope(String scope) {
addClaimToClaimList(OAuth2ClientMetadataClaimNames.SCOPE, scope);
return getThis();
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code scope} values that the Client will
* restrict itself to using, allowing the ability to add, replace, or remove,
* OPTIONAL.
* @param scopesConsumer a {@code Consumer} of the OAuth 2.0 {@code scope} values
* that the Client will restrict itself to using
* @return the {@link AbstractBuilder} for further configuration
*/
public B scopes(Consumer<List<String>> scopesConsumer) {
acceptClaimValues(OAuth2ClientMetadataClaimNames.SCOPE, scopesConsumer);
return getThis();
}
/**
* Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL.
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
* @return the {@link AbstractBuilder} for further configuration
*/
public B jwkSetUrl(String jwkSetUrl) {
return claim(OAuth2ClientMetadataClaimNames.JWKS_URI, jwkSetUrl);
}
/**
* Sets the claim.
* @param name the claim name
* @param value the claim value
* @return the {@link AbstractBuilder} for further configuration
*/
public B claim(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.put(name, value);
return getThis();
}
/**
* Provides access to every {@link #claim(String, Object)} declared so far
* allowing the ability to add, replace, or remove.
* @param claimsConsumer a {@code Consumer} of the claims
* @return the {@link AbstractBuilder} for further configurations
*/
public B claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return getThis();
}
/**
* Validate the claims and build the {@link AbstractOAuth2ClientRegistration}.
* @return the {@link AbstractOAuth2ClientRegistration}
*/
public abstract T build();
protected void validate() {
if (this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT) != null
|| this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_SECRET) != null) {
Assert.notNull(this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_ID), "client_id cannot be null");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT) != null) {
Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT),
"client_id_issued_at must be of type Instant");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT) != null) {
Assert.notNull(this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_SECRET),
"client_secret cannot be null");
Assert.isInstanceOf(Instant.class,
this.claims.get(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT),
"client_secret_expires_at must be of type Instant");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.REDIRECT_URIS) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OAuth2ClientMetadataClaimNames.REDIRECT_URIS),
"redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OAuth2ClientMetadataClaimNames.REDIRECT_URIS),
"redirect_uris cannot be empty");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.GRANT_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OAuth2ClientMetadataClaimNames.GRANT_TYPES),
"grant_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OAuth2ClientMetadataClaimNames.GRANT_TYPES),
"grant_types cannot be empty");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES),
"response_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES),
"response_types cannot be empty");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.SCOPE) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OAuth2ClientMetadataClaimNames.SCOPE),
"scope must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OAuth2ClientMetadataClaimNames.SCOPE),
"scope cannot be empty");
}
if (this.claims.get(OAuth2ClientMetadataClaimNames.JWKS_URI) != null) {
validateURL(this.claims.get(OAuth2ClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
}
}
@SuppressWarnings("unchecked")
private void addClaimToClaimList(String name, String value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
((List<String>) this.claims.get(name)).add(value);
}
@SuppressWarnings("unchecked")
private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
List<String> values = (List<String>) this.claims.get(name);
valuesConsumer.accept(values);
}
private static void validateURL(Object url, String errorMessage) {
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}
try {
new URI(url.toString()).toURL();
}
catch (Exception ex) {
throw new IllegalArgumentException(errorMessage, ex);
}
}
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.oauth2.server.authorization;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.springframework.security.oauth2.core.ClaimAccessor;
/**
* A {@link ClaimAccessor} for the claims that are contained in the OAuth 2.0 Client
* Registration Request and Response.
*
* @author Joe Grandja
* @since 7.0
* @see ClaimAccessor
* @see OAuth2ClientMetadataClaimNames
* @see OAuth2ClientRegistration
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-2">2. Client Metadata</a>
*/
public interface OAuth2ClientMetadataClaimAccessor extends ClaimAccessor {
/**
* Returns the Client Identifier {@code (client_id)}.
* @return the Client Identifier
*/
default String getClientId() {
return getClaimAsString(OAuth2ClientMetadataClaimNames.CLIENT_ID);
}
/**
* Returns the time at which the Client Identifier was issued
* {@code (client_id_issued_at)}.
* @return the time at which the Client Identifier was issued
*/
default Instant getClientIdIssuedAt() {
return getClaimAsInstant(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT);
}
/**
* Returns the Client Secret {@code (client_secret)}.
* @return the Client Secret
*/
default String getClientSecret() {
return getClaimAsString(OAuth2ClientMetadataClaimNames.CLIENT_SECRET);
}
/**
* Returns the time at which the {@code client_secret} will expire
* {@code (client_secret_expires_at)}.
* @return the time at which the {@code client_secret} will expire
*/
default Instant getClientSecretExpiresAt() {
return getClaimAsInstant(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT);
}
/**
* Returns the name of the Client to be presented to the End-User
* {@code (client_name)}.
* @return the name of the Client to be presented to the End-User
*/
default String getClientName() {
return getClaimAsString(OAuth2ClientMetadataClaimNames.CLIENT_NAME);
}
/**
* Returns the redirection {@code URI} values used by the Client
* {@code (redirect_uris)}.
* @return the redirection {@code URI} values used by the Client
*/
default List<String> getRedirectUris() {
return getClaimAsStringList(OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
/**
* Returns the authentication method used by the Client for the Token Endpoint
* {@code (token_endpoint_auth_method)}.
* @return the authentication method used by the Client for the Token Endpoint
*/
default String getTokenEndpointAuthenticationMethod() {
return getClaimAsString(OAuth2ClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}
/**
* Returns the OAuth 2.0 {@code grant_type} values that the Client will restrict
* itself to using {@code (grant_types)}.
* @return the OAuth 2.0 {@code grant_type} values that the Client will restrict
* itself to using
*/
default List<String> getGrantTypes() {
return getClaimAsStringList(OAuth2ClientMetadataClaimNames.GRANT_TYPES);
}
/**
* Returns the OAuth 2.0 {@code response_type} values that the Client will restrict
* itself to using {@code (response_types)}.
* @return the OAuth 2.0 {@code response_type} values that the Client will restrict
* itself to using
*/
default List<String> getResponseTypes() {
return getClaimAsStringList(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES);
}
/**
* Returns the OAuth 2.0 {@code scope} values that the Client will restrict itself to
* using {@code (scope)}.
* @return the OAuth 2.0 {@code scope} values that the Client will restrict itself to
* using
*/
default List<String> getScopes() {
return getClaimAsStringList(OAuth2ClientMetadataClaimNames.SCOPE);
}
/**
* Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}.
* @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
*/
default URL getJwkSetUrl() {
return getClaimAsURL(OAuth2ClientMetadataClaimNames.JWKS_URI);
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.oauth2.server.authorization;
/**
* The names of the claims defined by OAuth 2.0 Dynamic Client Registration Protocol that
* are contained in the OAuth 2.0 Client Registration Request and Response.
*
* @author Joe Grandja
* @since 7.0
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-2">2. Client Metadata</a>
*/
public class OAuth2ClientMetadataClaimNames {
/**
* {@code client_id} - the Client Identifier
*/
public static final String CLIENT_ID = "client_id";
/**
* {@code client_id_issued_at} - the time at which the Client Identifier was issued
*/
public static final String CLIENT_ID_ISSUED_AT = "client_id_issued_at";
/**
* {@code client_secret} - the Client Secret
*/
public static final String CLIENT_SECRET = "client_secret";
/**
* {@code client_secret_expires_at} - the time at which the {@code client_secret} will
* expire or 0 if it will not expire
*/
public static final String CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at";
/**
* {@code client_name} - the name of the Client to be presented to the End-User
*/
public static final String CLIENT_NAME = "client_name";
/**
* {@code redirect_uris} - the redirection {@code URI} values used by the Client
*/
public static final String REDIRECT_URIS = "redirect_uris";
/**
* {@code token_endpoint_auth_method} - the authentication method used by the Client
* for the Token Endpoint
*/
public static final String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
/**
* {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will
* restrict itself to using
*/
public static final String GRANT_TYPES = "grant_types";
/**
* {@code response_types} - the OAuth 2.0 {@code response_type} values that the Client
* will restrict itself to using
*/
public static final String RESPONSE_TYPES = "response_types";
/**
* {@code scope} - a space-separated list of OAuth 2.0 {@code scope} values that the
* Client will restrict itself to using
*/
public static final String SCOPE = "scope";
/**
* {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set
*/
public static final String JWKS_URI = "jwks_uri";
protected OAuth2ClientMetadataClaimNames() {
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.oauth2.server.authorization;
import java.io.Serial;
import java.util.Map;
import org.springframework.util.Assert;
/**
* A representation of an OAuth 2.0 Client Registration Request and Response, which is
* sent to and returned from the Client Registration Endpoint, and contains a set of
* claims about the Client's Registration information. The claims are defined by the OAuth
* 2.0 Dynamic Client Registration Protocol specification.
*
* @author Joe Grandja
* @since 7.0
* @see AbstractOAuth2ClientRegistration
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-3.1">3.1. Client Registration
* Request</a>
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1">3.2.1. Client
* Registration Response</a>
*/
public final class OAuth2ClientRegistration extends AbstractOAuth2ClientRegistration {
@Serial
private static final long serialVersionUID = 283805553286847831L;
private OAuth2ClientRegistration(Map<String, Object> claims) {
super(claims);
}
/**
* Constructs a new {@link Builder} with empty claims.
* @return the {@link Builder}
*/
public static Builder builder() {
return new Builder();
}
/**
* Constructs a new {@link Builder} with the provided claims.
* @param claims the claims to initialize the builder
* @return the {@link Builder}
*/
public static Builder withClaims(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
return new Builder().claims((c) -> c.putAll(claims));
}
/**
* Helps configure an {@link OAuth2ClientRegistration}.
*/
public static final class Builder extends AbstractBuilder<OAuth2ClientRegistration, Builder> {
private Builder() {
}
/**
* Validate the claims and build the {@link OAuth2ClientRegistration}.
* @return the {@link OAuth2ClientRegistration}
*/
@Override
public OAuth2ClientRegistration build() {
validate();
return new OAuth2ClientRegistration(getClaims());
}
}
}

View File

@ -0,0 +1,305 @@
/*
* 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.oauth2.server.authorization.authentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
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.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.converter.OAuth2ClientRegistrationRegisteredClientConverter;
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Dynamic Client
* Registration Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see OAuth2ClientRegistrationAuthenticationToken
* @see PasswordEncoder
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7591#section-3">3. Client
* Registration Endpoint</a>
*/
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
private final Log logger = LogFactory.getLog(getClass());
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private Converter<RegisteredClient, OAuth2ClientRegistration> clientRegistrationConverter;
private Converter<OAuth2ClientRegistration, RegisteredClient> registeredClientConverter;
private PasswordEncoder passwordEncoder;
private boolean openRegistrationAllowed;
/**
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
* provided parameters.
* @param registeredClientRepository the repository of registered clients
*/
public OAuth2ClientRegistrationAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = (OAuth2ClientRegistrationAuthenticationToken) authentication;
// Check if "initial" access token is not provided
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
if (clientRegistrationAuthentication.getPrincipal() != null && AbstractOAuth2TokenAuthenticationToken.class
.isAssignableFrom(clientRegistrationAuthentication.getPrincipal().getClass())) {
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) clientRegistrationAuthentication
.getPrincipal();
}
if (accessTokenAuthentication == null) {
if (this.openRegistrationAllowed) {
return registerClient(clientRegistrationAuthentication, null);
}
else {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
}
// Validate the "initial" access token
if (!accessTokenAuthentication.isAuthenticated()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(accessTokenValue,
OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with initial access token");
}
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
if (!authorizedAccessToken.isActive()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
checkScope(authorizedAccessToken, Collections.singleton(DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE));
return registerClient(clientRegistrationAuthentication, authorization);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientRegistrationAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@link Converter} used for converting an {@link OAuth2ClientRegistration}
* to a {@link RegisteredClient}.
* @param registeredClientConverter the {@link Converter} used for converting an
* {@link OAuth2ClientRegistration} to a {@link RegisteredClient}
*/
public void setRegisteredClientConverter(
Converter<OAuth2ClientRegistration, RegisteredClient> registeredClientConverter) {
Assert.notNull(registeredClientConverter, "registeredClientConverter cannot be null");
this.registeredClientConverter = registeredClientConverter;
}
/**
* Sets the {@link Converter} used for converting a {@link RegisteredClient} to an
* {@link OAuth2ClientRegistration}.
* @param clientRegistrationConverter the {@link Converter} used for converting a
* {@link RegisteredClient} to an {@link OAuth2ClientRegistration}
*/
public void setClientRegistrationConverter(
Converter<RegisteredClient, OAuth2ClientRegistration> clientRegistrationConverter) {
Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null");
this.clientRegistrationConverter = clientRegistrationConverter;
}
/**
* Sets the {@link PasswordEncoder} used to encode the
* {@link RegisteredClient#getClientSecret() client secret}. If not set, the client
* secret will be encoded using
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}.
* @param passwordEncoder the {@link PasswordEncoder} used to encode the client secret
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
}
/**
* 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
*/
public void setOpenRegistrationAllowed(boolean openRegistrationAllowed) {
this.openRegistrationAllowed = openRegistrationAllowed;
}
private OAuth2ClientRegistrationAuthenticationToken registerClient(
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client registration request parameters");
}
RegisteredClient registeredClient = this.registeredClientConverter
.convert(clientRegistrationAuthentication.getClientRegistration());
if (StringUtils.hasText(registeredClient.getClientSecret())) {
// Encode the client secret
RegisteredClient updatedRegisteredClient = RegisteredClient.from(registeredClient)
.clientSecret(this.passwordEncoder.encode(registeredClient.getClientSecret()))
.build();
this.registeredClientRepository.save(updatedRegisteredClient);
if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()
.equals(clientRegistrationAuthentication.getClientRegistration()
.getTokenEndpointAuthenticationMethod())) {
// Return the hashed client_secret
registeredClient = updatedRegisteredClient;
}
}
else {
this.registeredClientRepository.save(registeredClient);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved registered client");
}
if (authorization != null) {
// Invalidate the "initial" access token as it can only be used once
OAuth2Authorization.Builder builder = OAuth2Authorization.from(authorization)
.invalidate(authorization.getAccessToken().getToken());
if (authorization.getRefreshToken() != null) {
builder.invalidate(authorization.getRefreshToken().getToken());
}
authorization = builder.build();
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization with invalidated initial access token");
}
}
OAuth2ClientRegistration clientRegistration = this.clientRegistrationConverter.convert(registeredClient);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated client registration request");
}
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult = new OAuth2ClientRegistrationAuthenticationToken(
(Authentication) clientRegistrationAuthentication.getPrincipal(), clientRegistration);
clientRegistrationAuthenticationResult.setDetails(clientRegistrationAuthentication.getDetails());
return clientRegistrationAuthenticationResult;
}
@SuppressWarnings("unchecked")
private static void checkScope(OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken,
Set<String> requiredScope) {
Collection<String> authorizedScope = Collections.emptySet();
if (authorizedAccessToken.getClaims().containsKey(OAuth2ParameterNames.SCOPE)) {
authorizedScope = (Collection<String>) authorizedAccessToken.getClaims().get(OAuth2ParameterNames.SCOPE);
}
if (!authorizedScope.containsAll(requiredScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
}
else if (authorizedScope.size() != requiredScope.size()) {
// Restrict the access token to only contain the required scope
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
}
}
private static boolean isValidRedirectUris(List<String> redirectUris) {
if (CollectionUtils.isEmpty(redirectUris)) {
return true;
}
for (String redirectUri : redirectUris) {
try {
URI validRedirectUri = new URI(redirectUri);
if (validRedirectUri.getFragment() != null) {
return false;
}
}
catch (URISyntaxException ex) {
return false;
}
}
return true;
}
private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.oauth2.server.authorization.authentication;
import java.io.Serial;
import java.util.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation used for the OAuth 2.0 Dynamic Client
* Registration Endpoint.
*
* @author Joe Grandja
* @since 7.0
* @see AbstractAuthenticationToken
* @see OAuth2ClientRegistration
* @see OAuth2ClientRegistrationAuthenticationProvider
*/
public class OAuth2ClientRegistrationAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = 7135429161909989115L;
@Nullable
private final Authentication principal;
private final OAuth2ClientRegistration clientRegistration;
/**
* Constructs an {@code OAuth2ClientRegistrationAuthenticationToken} using the
* provided parameters.
* @param principal the authenticated principal
* @param clientRegistration the client registration
*/
public OAuth2ClientRegistrationAuthenticationToken(@Nullable Authentication principal,
OAuth2ClientRegistration clientRegistration) {
super(Collections.emptyList());
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
this.principal = principal;
this.clientRegistration = clientRegistration;
if (principal != null) {
setAuthenticated(principal.isAuthenticated());
}
}
@Nullable
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the client registration.
* @return the client registration
*/
public OAuth2ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.oauth2.server.authorization.converter;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.util.CollectionUtils;
/**
* A {@link Converter} that converts the provided {@link OAuth2ClientRegistration} to a
* {@link RegisteredClient}.
*
* @author Joe Grandja
* @since 7.0
*/
public final class OAuth2ClientRegistrationRegisteredClientConverter
implements Converter<OAuth2ClientRegistration, RegisteredClient> {
private static final StringKeyGenerator CLIENT_ID_GENERATOR = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 32);
private static final StringKeyGenerator CLIENT_SECRET_GENERATOR = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 48);
@Override
public RegisteredClient convert(OAuth2ClientRegistration clientRegistration) {
// @formatter:off
RegisteredClient.Builder builder = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(CLIENT_ID_GENERATOR.generateKey())
.clientIdIssuedAt(Instant.now())
.clientName(clientRegistration.getClientName());
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
builder
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
}
else if (ClientAuthenticationMethod.NONE.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
builder.clientAuthenticationMethod(ClientAuthenticationMethod.NONE);
}
else {
builder
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientSecret(CLIENT_SECRET_GENERATOR.generateKey());
}
if (!CollectionUtils.isEmpty(clientRegistration.getRedirectUris())) {
builder.redirectUris((redirectUris) ->
redirectUris.addAll(clientRegistration.getRedirectUris()));
}
if (!CollectionUtils.isEmpty(clientRegistration.getGrantTypes())) {
builder.authorizationGrantTypes((authorizationGrantTypes) ->
clientRegistration.getGrantTypes().forEach((grantType) ->
authorizationGrantTypes.add(new AuthorizationGrantType(grantType))));
}
else {
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
}
if (!CollectionUtils.isEmpty(clientRegistration.getResponseTypes()) &&
clientRegistration.getResponseTypes().contains(OAuth2AuthorizationResponseType.CODE.getValue())) {
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
}
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
builder.scopes((scopes) ->
scopes.addAll(clientRegistration.getScopes()));
}
ClientSettings.Builder clientSettingsBuilder = ClientSettings.builder()
.requireProofKey(true)
.requireAuthorizationConsent(true);
if (clientRegistration.getJwkSetUrl() != null) {
clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString());
}
builder
.clientSettings(clientSettingsBuilder.build());
return builder.build();
// @formatter:on
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.oauth2.server.authorization.converter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.util.CollectionUtils;
/**
* A {@link Converter} that converts the provided {@link RegisteredClient} to an
* {@link OAuth2ClientRegistration}.
*
* @author Joe Grandja
* @since 7.0
*/
public final class RegisteredClientOAuth2ClientRegistrationConverter
implements Converter<RegisteredClient, OAuth2ClientRegistration> {
@Override
public OAuth2ClientRegistration convert(RegisteredClient registeredClient) {
// @formatter:off
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.clientId(registeredClient.getClientId())
.clientIdIssuedAt(registeredClient.getClientIdIssuedAt())
.clientName(registeredClient.getClientName());
builder
.tokenEndpointAuthenticationMethod(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
if (registeredClient.getClientSecret() != null) {
builder.clientSecret(registeredClient.getClientSecret());
}
if (registeredClient.getClientSecretExpiresAt() != null) {
builder.clientSecretExpiresAt(registeredClient.getClientSecretExpiresAt());
}
if (!CollectionUtils.isEmpty(registeredClient.getRedirectUris())) {
builder.redirectUris((redirectUris) ->
redirectUris.addAll(registeredClient.getRedirectUris()));
}
builder.grantTypes((grantTypes) ->
registeredClient.getAuthorizationGrantTypes().forEach((authorizationGrantType) ->
grantTypes.add(authorizationGrantType.getValue())));
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
builder.responseType(OAuth2AuthorizationResponseType.CODE.getValue());
}
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
builder.scopes((scopes) ->
scopes.addAll(registeredClient.getScopes()));
}
ClientSettings clientSettings = registeredClient.getClientSettings();
if (clientSettings.getJwkSetUrl() != null) {
builder.jwkSetUrl(clientSettings.getJwkSetUrl());
}
return builder.build();
// @formatter:on
}
}

View File

@ -0,0 +1,233 @@
/*
* 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.oauth2.server.authorization.http.converter;
import java.net.URL;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A {@link HttpMessageConverter} for an {@link OAuth2ClientRegistration OAuth 2.0 Dynamic
* Client Registration Request and Response}.
*
* @author Joe Grandja
* @since 7.0
* @see AbstractHttpMessageConverter
* @see OAuth2ClientRegistration
*/
public class OAuth2ClientRegistrationHttpMessageConverter
extends AbstractHttpMessageConverter<OAuth2ClientRegistration> {
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
.getJsonMessageConverter();
private Converter<Map<String, Object>, OAuth2ClientRegistration> clientRegistrationConverter = new MapOAuth2ClientRegistrationConverter();
private Converter<OAuth2ClientRegistration, Map<String, Object>> clientRegistrationParametersConverter = new OAuth2ClientRegistrationMapConverter();
public OAuth2ClientRegistrationHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
@Override
protected boolean supports(Class<?> clazz) {
return OAuth2ClientRegistration.class.isAssignableFrom(clazz);
}
@Override
@SuppressWarnings("unchecked")
protected OAuth2ClientRegistration readInternal(Class<? extends OAuth2ClientRegistration> clazz,
HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
try {
Map<String, Object> clientRegistrationParameters = (Map<String, Object>) this.jsonMessageConverter
.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
return this.clientRegistrationConverter.convert(clientRegistrationParameters);
}
catch (Exception ex) {
throw new HttpMessageNotReadableException(
"An error occurred reading the OAuth 2.0 Client Registration: " + ex.getMessage(), ex,
inputMessage);
}
}
@Override
protected void writeInternal(OAuth2ClientRegistration clientRegistration, HttpOutputMessage outputMessage)
throws HttpMessageNotWritableException {
try {
Map<String, Object> clientRegistrationParameters = this.clientRegistrationParametersConverter
.convert(clientRegistration);
this.jsonMessageConverter.write(clientRegistrationParameters, STRING_OBJECT_MAP.getType(),
MediaType.APPLICATION_JSON, outputMessage);
}
catch (Exception ex) {
throw new HttpMessageNotWritableException(
"An error occurred writing the OAuth 2.0 Client Registration: " + ex.getMessage(), ex);
}
}
/**
* Sets the {@link Converter} used for converting the OAuth 2.0 Client Registration
* parameters to an {@link OAuth2ClientRegistration}.
* @param clientRegistrationConverter the {@link Converter} used for converting to an
* {@link OAuth2ClientRegistration}
*/
public final void setClientRegistrationConverter(
Converter<Map<String, Object>, OAuth2ClientRegistration> clientRegistrationConverter) {
Assert.notNull(clientRegistrationConverter, "clientRegistrationConverter cannot be null");
this.clientRegistrationConverter = clientRegistrationConverter;
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2ClientRegistration}
* to a {@code Map} representation of the OAuth 2.0 Client Registration parameters.
* @param clientRegistrationParametersConverter the {@link Converter} used for
* converting to a {@code Map} representation of the OAuth 2.0 Client Registration
* parameters
*/
public final void setClientRegistrationParametersConverter(
Converter<OAuth2ClientRegistration, Map<String, Object>> clientRegistrationParametersConverter) {
Assert.notNull(clientRegistrationParametersConverter, "clientRegistrationParametersConverter cannot be null");
this.clientRegistrationParametersConverter = clientRegistrationParametersConverter;
}
private static final class MapOAuth2ClientRegistrationConverter
implements Converter<Map<String, Object>, OAuth2ClientRegistration> {
private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService
.getSharedInstance();
private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
private static final Converter<Object, ?> INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR);
private final ClaimTypeConverter claimTypeConverter;
private MapOAuth2ClientRegistrationConverter() {
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
claimConverters.put(OAuth2ClientMetadataClaimNames.CLIENT_ID, stringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, INSTANT_CONVERTER);
claimConverters.put(OAuth2ClientMetadataClaimNames.CLIENT_SECRET, stringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT,
MapOAuth2ClientRegistrationConverter::convertClientSecretExpiresAt);
claimConverters.put(OAuth2ClientMetadataClaimNames.CLIENT_NAME, stringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter);
claimConverters.put(OAuth2ClientMetadataClaimNames.SCOPE,
MapOAuth2ClientRegistrationConverter::convertScope);
claimConverters.put(OAuth2ClientMetadataClaimNames.JWKS_URI, urlConverter);
this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
}
@Override
public OAuth2ClientRegistration convert(Map<String, Object> source) {
Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
Object clientSecretExpiresAt = parsedClaims.get(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT);
if (clientSecretExpiresAt instanceof Number && clientSecretExpiresAt.equals(0)) {
parsedClaims.remove(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT);
}
return OAuth2ClientRegistration.withClaims(parsedClaims).build();
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
private static Instant convertClientSecretExpiresAt(Object clientSecretExpiresAt) {
if (clientSecretExpiresAt != null && String.valueOf(clientSecretExpiresAt).equals("0")) {
// 0 indicates that client_secret_expires_at does not expire
return null;
}
return (Instant) INSTANT_CONVERTER.convert(clientSecretExpiresAt);
}
private static List<String> convertScope(Object scope) {
if (scope == null) {
return Collections.emptyList();
}
return Arrays.asList(StringUtils.delimitedListToStringArray(scope.toString(), " "));
}
}
private static final class OAuth2ClientRegistrationMapConverter
implements Converter<OAuth2ClientRegistration, Map<String, Object>> {
@Override
public Map<String, Object> convert(OAuth2ClientRegistration source) {
Map<String, Object> responseClaims = new LinkedHashMap<>(source.getClaims());
if (source.getClientIdIssuedAt() != null) {
responseClaims.put(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT,
source.getClientIdIssuedAt().getEpochSecond());
}
if (source.getClientSecret() != null) {
long clientSecretExpiresAt = 0;
if (source.getClientSecretExpiresAt() != null) {
clientSecretExpiresAt = source.getClientSecretExpiresAt().getEpochSecond();
}
responseClaims.put(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
}
if (!CollectionUtils.isEmpty(source.getScopes())) {
responseClaims.put(OAuth2ClientMetadataClaimNames.SCOPE,
StringUtils.collectionToDelimitedString(source.getScopes(), " "));
}
return responseClaims;
}
}
}

View File

@ -17,7 +17,6 @@
package org.springframework.security.oauth2.server.authorization.oidc;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.springframework.security.oauth2.core.ClaimAccessor;
@ -26,6 +25,7 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimAccessor;
/**
* A {@link ClaimAccessor} for the "claims" that are contained in the OpenID Client
@ -34,7 +34,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* @author Ovidiu Popa
* @author Joe Grandja
* @since 7.0
* @see ClaimAccessor
* @see OAuth2ClientMetadataClaimAccessor
* @see OidcClientMetadataClaimNames
* @see OidcClientRegistration
* @see <a target="_blank" href=
@ -44,59 +44,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* "https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1.
* Client Registration Metadata</a>
*/
public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
/**
* Returns the Client Identifier {@code (client_id)}.
* @return the Client Identifier
*/
default String getClientId() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_ID);
}
/**
* Returns the time at which the Client Identifier was issued
* {@code (client_id_issued_at)}.
* @return the time at which the Client Identifier was issued
*/
default Instant getClientIdIssuedAt() {
return getClaimAsInstant(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT);
}
/**
* Returns the Client Secret {@code (client_secret)}.
* @return the Client Secret
*/
default String getClientSecret() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_SECRET);
}
/**
* Returns the time at which the {@code client_secret} will expire
* {@code (client_secret_expires_at)}.
* @return the time at which the {@code client_secret} will expire
*/
default Instant getClientSecretExpiresAt() {
return getClaimAsInstant(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT);
}
/**
* Returns the name of the Client to be presented to the End-User
* {@code (client_name)}.
* @return the name of the Client to be presented to the End-User
*/
default String getClientName() {
return getClaimAsString(OidcClientMetadataClaimNames.CLIENT_NAME);
}
/**
* Returns the redirection {@code URI} values used by the Client
* {@code (redirect_uris)}.
* @return the redirection {@code URI} values used by the Client
*/
default List<String> getRedirectUris() {
return getClaimAsStringList(OidcClientMetadataClaimNames.REDIRECT_URIS);
}
public interface OidcClientMetadataClaimAccessor extends OAuth2ClientMetadataClaimAccessor {
/**
* Returns the post logout redirection {@code URI} values used by the Client
@ -109,15 +57,6 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsStringList(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
/**
* Returns the authentication method used by the Client for the Token Endpoint
* {@code (token_endpoint_auth_method)}.
* @return the authentication method used by the Client for the Token Endpoint
*/
default String getTokenEndpointAuthenticationMethod() {
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}
/**
* Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the
* {@link Jwt JWT} used to authenticate the Client at the Token Endpoint for the
@ -131,44 +70,6 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG);
}
/**
* Returns the OAuth 2.0 {@code grant_type} values that the Client will restrict
* itself to using {@code (grant_types)}.
* @return the OAuth 2.0 {@code grant_type} values that the Client will restrict
* itself to using
*/
default List<String> getGrantTypes() {
return getClaimAsStringList(OidcClientMetadataClaimNames.GRANT_TYPES);
}
/**
* Returns the OAuth 2.0 {@code response_type} values that the Client will restrict
* itself to using {@code (response_types)}.
* @return the OAuth 2.0 {@code response_type} values that the Client will restrict
* itself to using
*/
default List<String> getResponseTypes() {
return getClaimAsStringList(OidcClientMetadataClaimNames.RESPONSE_TYPES);
}
/**
* Returns the OAuth 2.0 {@code scope} values that the Client will restrict itself to
* using {@code (scope)}.
* @return the OAuth 2.0 {@code scope} values that the Client will restrict itself to
* using
*/
default List<String> getScopes() {
return getClaimAsStringList(OidcClientMetadataClaimNames.SCOPE);
}
/**
* Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}.
* @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
*/
default URL getJwkSetUrl() {
return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI);
}
/**
* Returns the {@link SignatureAlgorithm JWS} algorithm required for signing the
* {@link OidcIdToken ID Token} issued to the Client

View File

@ -20,6 +20,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
/**
* The names of the "claims" defined by OpenID Connect Dynamic Client Registration 1.0
@ -28,6 +29,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* @author Ovidiu Popa
* @author Joe Grandja
* @since 7.0
* @see OAuth2ClientMetadataClaimNames
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">2.
* Client Metadata</a>
@ -35,38 +37,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* "https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1.
* Client Registration Metadata</a>
*/
public final class OidcClientMetadataClaimNames {
/**
* {@code client_id} - the Client Identifier
*/
public static final String CLIENT_ID = "client_id";
/**
* {@code client_id_issued_at} - the time at which the Client Identifier was issued
*/
public static final String CLIENT_ID_ISSUED_AT = "client_id_issued_at";
/**
* {@code client_secret} - the Client Secret
*/
public static final String CLIENT_SECRET = "client_secret";
/**
* {@code client_secret_expires_at} - the time at which the {@code client_secret} will
* expire or 0 if it will not expire
*/
public static final String CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at";
/**
* {@code client_name} - the name of the Client to be presented to the End-User
*/
public static final String CLIENT_NAME = "client_name";
/**
* {@code redirect_uris} - the redirection {@code URI} values used by the Client
*/
public static final String REDIRECT_URIS = "redirect_uris";
public final class OidcClientMetadataClaimNames extends OAuth2ClientMetadataClaimNames {
/**
* {@code post_logout_redirect_uris} - the post logout redirection {@code URI} values
@ -76,12 +47,6 @@ public final class OidcClientMetadataClaimNames {
*/
public static final String POST_LOGOUT_REDIRECT_URIS = "post_logout_redirect_uris";
/**
* {@code token_endpoint_auth_method} - the authentication method used by the Client
* for the Token Endpoint
*/
public static final String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
/**
* {@code token_endpoint_auth_signing_alg} - the {@link JwsAlgorithm JWS} algorithm
* that must be used for signing the {@link Jwt JWT} used to authenticate the Client
@ -91,29 +56,6 @@ public final class OidcClientMetadataClaimNames {
*/
public static final String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg";
/**
* {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will
* restrict itself to using
*/
public static final String GRANT_TYPES = "grant_types";
/**
* {@code response_types} - the OAuth 2.0 {@code response_type} values that the Client
* will restrict itself to using
*/
public static final String RESPONSE_TYPES = "response_types";
/**
* {@code scope} - a space-separated list of OAuth 2.0 {@code scope} values that the
* Client will restrict itself to using
*/
public static final String SCOPE = "scope";
/**
* {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set
*/
public static final String JWKS_URI = "jwks_uri";
/**
* {@code id_token_signed_response_alg} - the {@link JwsAlgorithm JWS} algorithm
* required for signing the {@link OidcIdToken ID Token} issued to the Client

View File

@ -17,12 +17,6 @@
package org.springframework.security.oauth2.server.authorization.oidc;
import java.io.Serial;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -33,6 +27,7 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.AbstractOAuth2ClientRegistration;
import org.springframework.util.Assert;
/**
@ -44,6 +39,7 @@ import org.springframework.util.Assert;
* @author Ovidiu Popa
* @author Joe Grandja
* @since 7.0
* @see AbstractOAuth2ClientRegistration
* @see OidcClientMetadataClaimAccessor
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest">3.1.
@ -55,25 +51,14 @@ import org.springframework.util.Assert;
* "https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1.
* Client Registration Metadata</a>
*/
public final class OidcClientRegistration implements OidcClientMetadataClaimAccessor, Serializable {
public final class OidcClientRegistration extends AbstractOAuth2ClientRegistration
implements OidcClientMetadataClaimAccessor {
@Serial
private static final long serialVersionUID = 6518710174552040014L;
private final Map<String, Object> claims;
private static final long serialVersionUID = -8485448209864668396L;
private OidcClientRegistration(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
/**
* Returns the metadata as claims.
* @return a {@code Map} of the metadata as claims
*/
@Override
public Map<String, Object> getClaims() {
return this.claims;
super(claims);
}
/**
@ -97,82 +82,11 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
/**
* Helps configure an {@link OidcClientRegistration}.
*/
public static final class Builder {
private final Map<String, Object> claims = new LinkedHashMap<>();
public static final class Builder extends AbstractBuilder<OidcClientRegistration, Builder> {
private Builder() {
}
/**
* Sets the Client Identifier, REQUIRED.
* @param clientId the Client Identifier
* @return the {@link Builder} for further configuration
*/
public Builder clientId(String clientId) {
return claim(OidcClientMetadataClaimNames.CLIENT_ID, clientId);
}
/**
* Sets the time at which the Client Identifier was issued, OPTIONAL.
* @param clientIdIssuedAt the time at which the Client Identifier was issued
* @return the {@link Builder} for further configuration
*/
public Builder clientIdIssuedAt(Instant clientIdIssuedAt) {
return claim(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, clientIdIssuedAt);
}
/**
* Sets the Client Secret, OPTIONAL.
* @param clientSecret the Client Secret
* @return the {@link Builder} for further configuration
*/
public Builder clientSecret(String clientSecret) {
return claim(OidcClientMetadataClaimNames.CLIENT_SECRET, clientSecret);
}
/**
* Sets the time at which the {@code client_secret} will expire or {@code null} if
* it will not expire, REQUIRED if {@code client_secret} was issued.
* @param clientSecretExpiresAt the time at which the {@code client_secret} will
* expire or {@code null} if it will not expire
* @return the {@link Builder} for further configuration
*/
public Builder clientSecretExpiresAt(Instant clientSecretExpiresAt) {
return claim(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
}
/**
* Sets the name of the Client to be presented to the End-User, OPTIONAL.
* @param clientName the name of the Client to be presented to the End-User
* @return the {@link Builder} for further configuration
*/
public Builder clientName(String clientName) {
return claim(OidcClientMetadataClaimNames.CLIENT_NAME, clientName);
}
/**
* Add the redirection {@code URI} used by the Client, REQUIRED.
* @param redirectUri the redirection {@code URI} used by the Client
* @return the {@link Builder} for further configuration
*/
public Builder redirectUri(String redirectUri) {
addClaimToClaimList(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUri);
return this;
}
/**
* A {@code Consumer} of the redirection {@code URI} values used by the Client,
* allowing the ability to add, replace, or remove, REQUIRED.
* @param redirectUrisConsumer a {@code Consumer} of the redirection {@code URI}
* values used by the Client
* @return the {@link Builder} for further configuration
*/
public Builder redirectUris(Consumer<List<String>> redirectUrisConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.REDIRECT_URIS, redirectUrisConsumer);
return this;
}
/**
* Add the post logout redirection {@code URI} used by the Client, OPTIONAL. The
* {@code post_logout_redirect_uri} parameter is used by the client when
@ -199,17 +113,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return this;
}
/**
* Sets the authentication method used by the Client for the Token Endpoint,
* OPTIONAL.
* @param tokenEndpointAuthenticationMethod the authentication method used by the
* Client for the Token Endpoint
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpointAuthenticationMethod(String tokenEndpointAuthenticationMethod) {
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthenticationMethod);
}
/**
* Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the
* {@link Jwt JWT} used to authenticate the Client at the Token Endpoint for the
@ -225,90 +128,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, authenticationSigningAlgorithm);
}
/**
* Add the OAuth 2.0 {@code grant_type} that the Client will restrict itself to
* using, OPTIONAL.
* @param grantType the OAuth 2.0 {@code grant_type} that the Client will restrict
* itself to using
* @return the {@link Builder} for further configuration
*/
public Builder grantType(String grantType) {
addClaimToClaimList(OidcClientMetadataClaimNames.GRANT_TYPES, grantType);
return this;
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code grant_type} values that the Client
* will restrict itself to using, allowing the ability to add, replace, or remove,
* OPTIONAL.
* @param grantTypesConsumer a {@code Consumer} of the OAuth 2.0
* {@code grant_type} values that the Client will restrict itself to using
* @return the {@link Builder} for further configuration
*/
public Builder grantTypes(Consumer<List<String>> grantTypesConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.GRANT_TYPES, grantTypesConsumer);
return this;
}
/**
* Add the OAuth 2.0 {@code response_type} that the Client will restrict itself to
* using, OPTIONAL.
* @param responseType the OAuth 2.0 {@code response_type} that the Client will
* restrict itself to using
* @return the {@link Builder} for further configuration
*/
public Builder responseType(String responseType) {
addClaimToClaimList(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseType);
return this;
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code response_type} values that the
* Client will restrict itself to using, allowing the ability to add, replace, or
* remove, OPTIONAL.
* @param responseTypesConsumer a {@code Consumer} of the OAuth 2.0
* {@code response_type} values that the Client will restrict itself to using
* @return the {@link Builder} for further configuration
*/
public Builder responseTypes(Consumer<List<String>> responseTypesConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.RESPONSE_TYPES, responseTypesConsumer);
return this;
}
/**
* Add the OAuth 2.0 {@code scope} that the Client will restrict itself to using,
* OPTIONAL.
* @param scope the OAuth 2.0 {@code scope} that the Client will restrict itself
* to using
* @return the {@link Builder} for further configuration
*/
public Builder scope(String scope) {
addClaimToClaimList(OidcClientMetadataClaimNames.SCOPE, scope);
return this;
}
/**
* A {@code Consumer} of the OAuth 2.0 {@code scope} values that the Client will
* restrict itself to using, allowing the ability to add, replace, or remove,
* OPTIONAL.
* @param scopesConsumer a {@code Consumer} of the OAuth 2.0 {@code scope} values
* that the Client will restrict itself to using
* @return the {@link Builder} for further configuration
*/
public Builder scopes(Consumer<List<String>> scopesConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.SCOPE, scopesConsumer);
return this;
}
/**
* Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL.
* @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
* @return the {@link Builder} for further configuration
*/
public Builder jwkSetUrl(String jwkSetUrl) {
return claim(OidcClientMetadataClaimNames.JWKS_URI, jwkSetUrl);
}
/**
* Sets the {@link SignatureAlgorithm JWS} algorithm required for signing the
* {@link OidcIdToken ID Token} issued to the Client, OPTIONAL.
@ -343,120 +162,51 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUrl);
}
/**
* Sets the claim.
* @param name the claim name
* @param value the claim value
* @return the {@link Builder} for further configuration
*/
public Builder claim(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.put(name, value);
return this;
}
/**
* Provides access to every {@link #claim(String, Object)} declared so far
* allowing the ability to add, replace, or remove.
* @param claimsConsumer a {@code Consumer} of the claims
* @return the {@link Builder} for further configurations
*/
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return this;
}
/**
* Validate the claims and build the {@link OidcClientRegistration}.
* <p>
* The following claims are REQUIRED: {@code client_id}, {@code redirect_uris}.
* @return the {@link OidcClientRegistration}
*/
@Override
public OidcClientRegistration build() {
validate();
return new OidcClientRegistration(this.claims);
return new OidcClientRegistration(getClaims());
}
private void validate() {
if (this.claims.get(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT) != null
|| this.claims.get(OidcClientMetadataClaimNames.CLIENT_SECRET) != null) {
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.CLIENT_ID), "client_id cannot be null");
}
if (this.claims.get(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT) != null) {
Assert.isInstanceOf(Instant.class, this.claims.get(OidcClientMetadataClaimNames.CLIENT_ID_ISSUED_AT),
"client_id_issued_at must be of type Instant");
}
if (this.claims.get(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT) != null) {
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.CLIENT_SECRET),
"client_secret cannot be null");
Assert.isInstanceOf(Instant.class,
this.claims.get(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT),
"client_secret_expires_at must be of type Instant");
}
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS),
@Override
protected void validate() {
super.validate();
Assert.notNull(getClaims().get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
Assert.isInstanceOf(List.class, getClaims().get(OidcClientMetadataClaimNames.REDIRECT_URIS),
"redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS),
Assert.notEmpty((List<?>) getClaims().get(OidcClientMetadataClaimNames.REDIRECT_URIS),
"redirect_uris cannot be empty");
if (this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS),
if (getClaims().get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS) != null) {
Assert.isInstanceOf(List.class, getClaims().get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS),
"post_logout_redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS),
Assert.notEmpty((List<?>) getClaims().get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS),
"post_logout_redirect_uris cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES),
"grant_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES),
"grant_types cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES),
"response_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.RESPONSE_TYPES),
"response_types cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.SCOPE) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.SCOPE),
"scope must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.JWKS_URI) != null) {
validateURL(this.claims.get(OidcClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
}
}
@SuppressWarnings("unchecked")
private void addClaimToClaimList(String name, String value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
((List<String>) this.claims.get(name)).add(value);
getClaims().computeIfAbsent(name, (k) -> new LinkedList<String>());
((List<String>) getClaims().get(name)).add(value);
}
@SuppressWarnings("unchecked")
private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
this.claims.computeIfAbsent(name, (k) -> new LinkedList<String>());
List<String> values = (List<String>) this.claims.get(name);
getClaims().computeIfAbsent(name, (k) -> new LinkedList<String>());
List<String> values = (List<String>) getClaims().get(name);
valuesConsumer.accept(values);
}
private static void validateURL(Object url, String errorMessage) {
if (URL.class.isAssignableFrom(url.getClass())) {
return;
}
try {
new URI(url.toString()).toURL();
}
catch (Exception ex) {
throw new IllegalArgumentException(errorMessage, ex);
}
}
}
}

View File

@ -40,7 +40,7 @@ import org.springframework.util.Assert;
public class OidcClientRegistrationAuthenticationToken extends AbstractAuthenticationToken {
@Serial
private static final long serialVersionUID = -6198261907690781217L;
private static final long serialVersionUID = 5392324479052435784L;
private final Authentication principal;

View File

@ -137,6 +137,15 @@ public final class AuthorizationServerSettings extends AbstractSettings {
return getSetting(ConfigurationSettingNames.AuthorizationServer.TOKEN_INTROSPECTION_ENDPOINT);
}
/**
* Returns the OAuth 2.0 Dynamic Client Registration endpoint. The default is
* {@code /oauth2/register}.
* @return the OAuth 2.0 Dynamic Client Registration endpoint
*/
public String getClientRegistrationEndpoint() {
return getSetting(ConfigurationSettingNames.AuthorizationServer.CLIENT_REGISTRATION_ENDPOINT);
}
/**
* Returns the OpenID Connect 1.0 Client Registration endpoint. The default is
* {@code /connect/register}.
@ -177,6 +186,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
.jwkSetEndpoint("/oauth2/jwks")
.tokenRevocationEndpoint("/oauth2/revoke")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.clientRegistrationEndpoint("/oauth2/register")
.oidcClientRegistrationEndpoint("/connect/register")
.oidcUserInfoEndpoint("/userinfo")
.oidcLogoutEndpoint("/connect/logout");
@ -315,6 +325,17 @@ public final class AuthorizationServerSettings extends AbstractSettings {
tokenIntrospectionEndpoint);
}
/**
* Sets the OAuth 2.0 Dynamic Client Registration endpoint.
* @param clientRegistrationEndpoint the OAuth 2.0 Dynamic Client Registration
* endpoint
* @return the {@link Builder} for further configuration
*/
public Builder clientRegistrationEndpoint(String clientRegistrationEndpoint) {
return setting(ConfigurationSettingNames.AuthorizationServer.CLIENT_REGISTRATION_ENDPOINT,
clientRegistrationEndpoint);
}
/**
* Sets the OpenID Connect 1.0 Client Registration endpoint.
* @param oidcClientRegistrationEndpoint the OpenID Connect 1.0 Client

View File

@ -150,6 +150,12 @@ public final class ConfigurationSettingNames {
public static final String TOKEN_INTROSPECTION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
.concat("token-introspection-endpoint");
/**
* Set the OAuth 2.0 Dynamic Client Registration endpoint.
*/
public static final String CLIENT_REGISTRATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
.concat("client-registration-endpoint");
/**
* Set the OpenID Connect 1.0 Client Registration endpoint.
*/

View File

@ -0,0 +1,212 @@
/*
* 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.oauth2.server.authorization.web;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
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.http.converter.OAuth2ClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientRegistrationAuthenticationConverter;
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.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A {@code Filter} that processes OAuth 2.0 Dynamic Client Registration Requests.
*
* @author Joe Grandja
* @since 7.0
* @see OAuth2ClientRegistration
* @see OAuth2ClientRegistrationAuthenticationConverter
* @see OAuth2ClientRegistrationAuthenticationProvider
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7591#section-3">3. Client
* Registration Endpoint</a>
*/
public final class OAuth2ClientRegistrationEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OAuth 2.0 Client Registration requests.
*/
private static final String DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI = "/oauth2/register";
private final AuthenticationManager authenticationManager;
private final RequestMatcher clientRegistrationEndpointMatcher;
private final HttpMessageConverter<OAuth2ClientRegistration> clientRegistrationHttpMessageConverter = new OAuth2ClientRegistrationHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
private AuthenticationConverter authenticationConverter = new OAuth2ClientRegistrationAuthenticationConverter();
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendClientRegistrationResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
/**
* Constructs an {@code OAuth2ClientRegistrationEndpointFilter} using the provided
* parameters.
* @param authenticationManager the authentication manager
*/
public OAuth2ClientRegistrationEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI);
}
/**
* Constructs an {@code OAuth2ClientRegistrationEndpointFilter} using the provided
* parameters.
* @param authenticationManager the authentication manager
* @param clientRegistrationEndpointUri the endpoint {@code URI} for OAuth 2.0 Client
* Registration requests
*/
public OAuth2ClientRegistrationEndpointFilter(AuthenticationManager authenticationManager,
String clientRegistrationEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(clientRegistrationEndpointUri, "clientRegistrationEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.clientRegistrationEndpointMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.POST, clientRegistrationEndpointUri);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.clientRegistrationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication clientRegistrationAuthentication = this.authenticationConverter.convert(request);
Authentication clientRegistrationAuthenticationResult = this.authenticationManager
.authenticate(clientRegistrationAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
clientRegistrationAuthenticationResult);
}
catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Client registration request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
catch (Exception ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Client Registration Error: " + ex.getMessage(),
"https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2");
if (this.logger.isTraceEnabled()) {
this.logger.trace(error.getDescription(), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
new OAuth2AuthenticationException(error));
}
finally {
SecurityContextHolder.clearContext();
}
}
/**
* Sets the {@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 authenticationConverter an {@link AuthenticationConverter} used when
* attempting to extract a Client Registration Request from {@link HttpServletRequest}
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2ClientRegistrationAuthenticationToken} and returning the
* {@link OAuth2ClientRegistration Client Registration Response}.
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used
* for handling an {@link OAuth2ClientRegistrationAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used
* for handling an {@link OAuth2AuthenticationException}
*/
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
private void sendClientRegistrationResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2ClientRegistration clientRegistration = ((OAuth2ClientRegistrationAuthenticationToken) authentication)
.getClientRegistration();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpResponse);
}
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) authenticationException).getError();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
if (OAuth2ErrorCodes.INVALID_TOKEN.equals(error.getErrorCode())) {
httpStatus = HttpStatus.UNAUTHORIZED;
}
else if (OAuth2ErrorCodes.INSUFFICIENT_SCOPE.equals(error.getErrorCode())) {
httpStatus = HttpStatus.FORBIDDEN;
}
else if (OAuth2ErrorCodes.INVALID_CLIENT.equals(error.getErrorCode())) {
httpStatus = HttpStatus.UNAUTHORIZED;
}
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(httpStatus);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.oauth2.server.authorization.web.authentication;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2ClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientRegistrationEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
/**
* Attempts to extract an OAuth 2.0 Dynamic Client Registration Request from
* {@link HttpServletRequest} and then converts to an
* {@link OAuth2ClientRegistrationAuthenticationToken} used for authenticating the
* request.
*
* @author Joe Grandja
* @since 7.0
* @see AuthenticationConverter
* @see OAuth2ClientRegistrationAuthenticationToken
* @see OAuth2ClientRegistrationEndpointFilter
*/
public final class OAuth2ClientRegistrationAuthenticationConverter implements AuthenticationConverter {
private final HttpMessageConverter<OAuth2ClientRegistration> clientRegistrationHttpMessageConverter = new OAuth2ClientRegistrationHttpMessageConverter();
@Override
public Authentication convert(HttpServletRequest request) {
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
OAuth2ClientRegistration clientRegistration;
try {
clientRegistration = this.clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class,
new ServletServerHttpRequest(request));
}
catch (Exception ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Client Registration Error: " + ex.getMessage(),
"https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2");
throw new OAuth2AuthenticationException(error, ex);
}
return new OAuth2ClientRegistrationAuthenticationToken(principal, clientRegistration);
}
}

View File

@ -0,0 +1,360 @@
/*
* 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.oauth2.server.authorization;
import java.net.URL;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OAuth2ClientRegistration}.
*
* @author Joe Grandja
*/
public class OAuth2ClientRegistrationTests {
@Test
public void buildWhenAllClaimsProvidedThenCreated() throws Exception {
// @formatter:off
Instant clientIdIssuedAt = Instant.now();
Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS);
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientIdIssuedAt(clientIdIssuedAt)
.clientSecret("client-secret")
.clientSecretExpiresAt(clientSecretExpiresAt)
.clientName("client-name")
.redirectUri("https://client.example.com")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope("scope1")
.scope("scope2")
.jwkSetUrl("https://client.example.com/jwks")
.claim("a-claim", "a-value")
.build();
// @formatter:on
assertThat(clientRegistration.getClientId()).isEqualTo("client-id");
assertThat(clientRegistration.getClientIdIssuedAt()).isEqualTo(clientIdIssuedAt);
assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret");
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code",
"client_credentials");
assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value");
}
@Test
public void withClaimsWhenClaimsProvidedThenCreated() throws Exception {
Instant clientIdIssuedAt = Instant.now();
Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS);
HashMap<String, Object> claims = new HashMap<>();
claims.put(OAuth2ClientMetadataClaimNames.CLIENT_ID, "client-id");
claims.put(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, clientIdIssuedAt);
claims.put(OAuth2ClientMetadataClaimNames.CLIENT_SECRET, "client-secret");
claims.put(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
claims.put(OAuth2ClientMetadataClaimNames.CLIENT_NAME, "client-name");
claims.put(OAuth2ClientMetadataClaimNames.REDIRECT_URIS,
Collections.singletonList("https://client.example.com"));
claims.put(OAuth2ClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
claims.put(OAuth2ClientMetadataClaimNames.GRANT_TYPES,
Arrays.asList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue(),
AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()));
claims.put(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, Collections.singletonList("code"));
claims.put(OAuth2ClientMetadataClaimNames.SCOPE, Arrays.asList("scope1", "scope2"));
claims.put(OAuth2ClientMetadataClaimNames.JWKS_URI, "https://client.example.com/jwks");
claims.put("a-claim", "a-value");
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.withClaims(claims).build();
assertThat(clientRegistration.getClientId()).isEqualTo("client-id");
assertThat(clientRegistration.getClientIdIssuedAt()).isEqualTo(clientIdIssuedAt);
assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret");
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code",
"client_credentials");
assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value");
}
@Test
public void withClaimsWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> OAuth2ClientRegistration.withClaims(null))
.withMessage("claims cannot be empty");
}
@Test
public void withClaimsWhenEmptyThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2ClientRegistration.withClaims(Collections.emptyMap()))
.withMessage("claims cannot be empty");
}
@Test
public void buildWhenMissingClientIdThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder().clientIdIssuedAt(Instant.now());
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("client_id cannot be null");
}
@Test
public void buildWhenClientSecretAndMissingClientIdThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder().clientSecret("client-secret");
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("client_id cannot be null");
}
@Test
public void buildWhenClientIdIssuedAtNotInstantThenThrowIllegalArgumentException() {
// @formatter:off
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.clientId("client-id")
.claim(OAuth2ClientMetadataClaimNames.CLIENT_ID_ISSUED_AT, "clientIdIssuedAt");
// @formatter:on
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("client_id_issued_at must be of type Instant");
}
@Test
public void buildWhenMissingClientSecretThenThrowIllegalArgumentException() {
// @formatter:off
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientIdIssuedAt(Instant.now())
.clientSecretExpiresAt(Instant.now().plus(30, ChronoUnit.DAYS));
// @formatter:on
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("client_secret cannot be null");
}
@Test
public void buildWhenClientSecretExpiresAtNotInstantThenThrowIllegalArgumentException() {
// @formatter:off
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientIdIssuedAt(Instant.now())
.clientSecret("client-secret")
.claim(OAuth2ClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, "clientSecretExpiresAt");
// @formatter:on
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("client_secret_expires_at must be of type Instant");
}
@Test
public void buildWhenRedirectUrisNotListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.REDIRECT_URIS, "redirectUris");
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("redirect_uris must be of type List");
}
@Test
public void buildWhenRedirectUrisEmptyListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.REDIRECT_URIS, Collections.emptyList());
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("redirect_uris cannot be empty");
}
@Test
public void buildWhenRedirectUrisAddingOrRemovingThenCorrectValues() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client1.example.com")
.redirectUris((redirectUris) -> {
redirectUris.clear();
redirectUris.add("https://client2.example.com");
})
.build();
// @formatter:on
assertThat(clientRegistration.getRedirectUris()).containsExactly("https://client2.example.com");
}
@Test
public void buildWhenGrantTypesNotListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.GRANT_TYPES, "grantTypes");
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("grant_types must be of type List");
}
@Test
public void buildWhenGrantTypesEmptyListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.GRANT_TYPES, Collections.emptyList());
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("grant_types cannot be empty");
}
@Test
public void buildWhenGrantTypesAddingOrRemovingThenCorrectValues() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.grantType("authorization_code")
.grantTypes((grantTypes) -> {
grantTypes.clear();
grantTypes.add("client_credentials");
})
.build();
// @formatter:on
assertThat(clientRegistration.getGrantTypes()).containsExactly("client_credentials");
}
@Test
public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, "responseTypes");
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("response_types must be of type List");
}
@Test
public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.RESPONSE_TYPES, Collections.emptyList());
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("response_types cannot be empty");
}
@Test
public void buildWhenResponseTypesAddingOrRemovingThenCorrectValues() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.responseType("token")
.responseTypes((responseTypes) -> {
responseTypes.clear();
responseTypes.add("code");
})
.build();
// @formatter:on
assertThat(clientRegistration.getResponseTypes()).containsExactly("code");
}
@Test
public void buildWhenScopesNotListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.SCOPE, "scopes");
assertThatIllegalArgumentException().isThrownBy(builder::build)
.withMessageStartingWith("scope must be of type List");
}
@Test
public void buildWhenScopesEmptyListThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.SCOPE, Collections.emptyList());
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("scope cannot be empty");
}
@Test
public void buildWhenScopesAddingOrRemovingThenCorrectValues() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.scope("should-be-removed")
.scopes((scopes) -> {
scopes.clear();
scopes.add("scope1");
})
.build();
// @formatter:on
assertThat(clientRegistration.getScopes()).containsExactly("scope1");
}
@Test
public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() {
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder()
.claim(OAuth2ClientMetadataClaimNames.JWKS_URI, "not an url");
assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("jwksUri must be a valid URL");
}
@Test
public void claimWhenNameNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2ClientRegistration.builder().claim(null, "claim-value"))
.withMessage("name cannot be empty");
}
@Test
public void claimWhenValueNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2ClientRegistration.builder().claim("claim-name", null))
.withMessage("value cannot be null");
}
@Test
public void claimsWhenRemovingClaimThenNotPresent() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.claim("claim-name", "claim-value")
.claims((claims) -> claims.remove("claim-name"))
.build();
// @formatter:on
assertThat(clientRegistration.hasClaim("claim-name")).isFalse();
}
@Test
public void claimsWhenAddingClaimThenPresent() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.claim("claim-name", "claim-value")
.build();
// @formatter:on
assertThat(clientRegistration.hasClaim("claim-name")).isTrue();
}
}

View File

@ -0,0 +1,500 @@
/*
* 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.oauth2.server.authorization.authentication;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
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.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.TestJwsHeaders;
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
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.resource.authentication.JwtAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link OAuth2ClientRegistrationAuthenticationProvider}.
*
* @author Joe Grandja
*/
public class OAuth2ClientRegistrationAuthenticationProviderTests {
private RegisteredClientRepository registeredClientRepository;
private OAuth2AuthorizationService authorizationService;
private PasswordEncoder passwordEncoder;
private OAuth2ClientRegistrationAuthenticationProvider authenticationProvider;
@BeforeEach
public void setUp() {
this.registeredClientRepository = mock(RegisteredClientRepository.class);
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.passwordEncoder = spy(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return NoOpPasswordEncoder.getInstance().encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return NoOpPasswordEncoder.getInstance().matches(rawPassword, encodedPassword);
}
});
this.authenticationProvider = new OAuth2ClientRegistrationAuthenticationProvider(
this.registeredClientRepository, this.authorizationService);
this.authenticationProvider.setPasswordEncoder(this.passwordEncoder);
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OAuth2ClientRegistrationAuthenticationProvider(null, this.authorizationService))
.withMessage("registeredClientRepository cannot be null");
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OAuth2ClientRegistrationAuthenticationProvider(this.registeredClientRepository, null))
.withMessage("authorizationService cannot be null");
}
@Test
public void setRegisteredClientConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.authenticationProvider.setRegisteredClientConverter(null))
.withMessage("registeredClientConverter cannot be null");
}
@Test
public void setClientRegistrationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.authenticationProvider.setClientRegistrationConverter(null))
.withMessage("clientRegistrationConverter cannot be null");
}
@Test
public void setPasswordEncoderWhenNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.authenticationProvider.setPasswordEncoder(null))
.withMessage("passwordEncoder cannot be null");
}
@Test
public void supportsWhenTypeOAuth2ClientRegistrationAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OAuth2ClientRegistrationAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenPrincipalNotOAuth2TokenAuthenticationTokenThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
}
@Test
public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
JwtAuthenticationToken principal = new JwtAuthenticationToken(createJwtClientRegistration());
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
}
@Test
public void authenticateWhenAccessTokenNotFoundThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
verify(this.authorizationService).findByToken(eq(jwt.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenAccessTokenNotActiveThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.invalidate(jwtAccessToken)
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwt(Collections.singleton("unauthorized.scope"));
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_unauthorized.scope"));
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenAccessTokenContainsRequiredScopeAndAdditionalScopeThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwt(new HashSet<>(Arrays.asList("client.create", "scope1")));
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create", "SCOPE_scope1"));
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("invalid uri")
.build();
// @formatter:on
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REDIRECT_URI);
assertThat(error.getDescription()).contains(OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com#fragment")
.build();
// @formatter:on
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.satisfies((error) -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REDIRECT_URI);
assertThat(error.getDescription()).contains(OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations
.authorization(registeredClient, jwtAccessToken, jwt.getClaims())
.build();
given(this.authorizationService.findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN)))
.willReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @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
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
principal, clientRegistration);
OAuth2ClientRegistrationAuthenticationToken authenticationResult = (OAuth2ClientRegistrationAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).findByToken(eq(jwtAccessToken.getTokenValue()),
eq(OAuth2TokenType.ACCESS_TOKEN));
verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
verify(this.authorizationService).save(authorizationCaptor.capture());
verify(this.passwordEncoder).encode(any());
// assert "initial" access token is invalidated
OAuth2Authorization authorizationResult = authorizationCaptor.getValue();
assertThat(authorizationResult.getAccessToken().isInvalidated()).isTrue();
if (authorizationResult.getRefreshToken() != null) {
assertThat(authorizationResult.getRefreshToken().isInvalidated()).isTrue();
}
assertClientRegistration(clientRegistration, authenticationResult.getClientRegistration(),
registeredClientCaptor.getValue());
}
@Test
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
this.authenticationProvider.setOpenRegistrationAllowed(true);
// @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
OAuth2ClientRegistrationAuthenticationToken authentication = new OAuth2ClientRegistrationAuthenticationToken(
null, clientRegistration);
OAuth2ClientRegistrationAuthenticationToken authenticationResult = (OAuth2ClientRegistrationAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
verifyNoInteractions(this.authorizationService);
verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
verify(this.passwordEncoder).encode(any());
assertClientRegistration(clientRegistration, authenticationResult.getClientRegistration(),
registeredClientCaptor.getValue());
}
private static void assertClientRegistration(OAuth2ClientRegistration clientRegistrationRequest,
OAuth2ClientRegistration clientRegistrationResult, RegisteredClient registeredClient) {
assertThat(registeredClient.getId()).isNotNull();
assertThat(registeredClient.getClientId()).isNotNull();
assertThat(registeredClient.getClientIdIssuedAt()).isNotNull();
assertThat(registeredClient.getClientSecret()).isNotNull();
assertThat(registeredClient.getClientName()).isEqualTo(clientRegistrationRequest.getClientName());
assertThat(registeredClient.getClientAuthenticationMethods())
.containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(registeredClient.getRedirectUris()).containsExactly("https://client.example.com");
assertThat(registeredClient.getAuthorizationGrantTypes()).containsExactlyInAnyOrder(
AuthorizationGrantType.AUTHORIZATION_CODE, AuthorizationGrantType.CLIENT_CREDENTIALS);
assertThat(registeredClient.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
assertThat(registeredClient.getClientSettings().isRequireProofKey()).isTrue();
assertThat(registeredClient.getClientSettings().isRequireAuthorizationConsent()).isTrue();
assertThat(clientRegistrationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(clientRegistrationResult.getClientIdIssuedAt()).isEqualTo(registeredClient.getClientIdIssuedAt());
assertThat(clientRegistrationResult.getClientSecret()).isEqualTo(registeredClient.getClientSecret());
assertThat(clientRegistrationResult.getClientSecretExpiresAt())
.isEqualTo(registeredClient.getClientSecretExpiresAt());
assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClient.getClientName());
assertThat(clientRegistrationResult.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(registeredClient.getRedirectUris());
List<String> grantTypes = new ArrayList<>();
registeredClient.getAuthorizationGrantTypes()
.forEach((authorizationGrantType) -> grantTypes.add(authorizationGrantType.getValue()));
assertThat(clientRegistrationResult.getGrantTypes()).containsExactlyInAnyOrderElementsOf(grantTypes);
assertThat(clientRegistrationResult.getResponseTypes())
.containsExactly(OAuth2AuthorizationResponseType.CODE.getValue());
assertThat(clientRegistrationResult.getScopes())
.containsExactlyInAnyOrderElementsOf(registeredClient.getScopes());
assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod())
.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
}
private static Jwt createJwtClientRegistration() {
return createJwt(Collections.singleton("client.create"));
}
private static Jwt createJwt(Set<String> scopes) {
// @formatter:off
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader()
.build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet()
.claim(OAuth2ParameterNames.SCOPE, scopes)
.build();
Jwt jwt = Jwt.withTokenValue("jwt-access-token")
.headers((headers) -> headers.putAll(jwsHeader.getHeaders()))
.claims((claims) -> claims.putAll(jwtClaimsSet.getClaims()))
.build();
// @formatter:on
return jwt;
}
}

View File

@ -0,0 +1,234 @@
/*
* 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.oauth2.server.authorization.http.converter;
import java.net.URL;
import java.time.Instant;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OAuth2ClientRegistrationHttpMessageConverter}
*
* @author Joe Grandja
* @since 7.0
*/
public class OAuth2ClientRegistrationHttpMessageConverterTests {
private final OAuth2ClientRegistrationHttpMessageConverter messageConverter = new OAuth2ClientRegistrationHttpMessageConverter();
@Test
public void supportsWhenOAuth2ClientRegistrationThenTrue() {
assertThat(this.messageConverter.supports(OAuth2ClientRegistration.class)).isTrue();
}
@Test
public void setClientRegistrationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setClientRegistrationConverter(null))
.withMessageContaining("clientRegistrationConverter cannot be null");
}
@Test
public void setClientRegistrationParametersConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setClientRegistrationParametersConverter(null))
.withMessageContaining("clientRegistrationParametersConverter cannot be null");
}
@Test
public void readInternalWhenValidParametersThenSuccess() throws Exception {
// @formatter:off
String clientRegistrationRequest = "{\n"
+ " \"client_id\": \"client-id\",\n"
+ " \"client_id_issued_at\": 1607633867,\n"
+ " \"client_secret\": \"client-secret\",\n"
+ " \"client_secret_expires_at\": 1607637467,\n"
+ " \"client_name\": \"client-name\",\n"
+ " \"redirect_uris\": [\n"
+ " \"https://client.example.com\"\n"
+ " ],\n"
+ " \"token_endpoint_auth_method\": \"client_secret_basic\",\n"
+ " \"grant_types\": [\n"
+ " \"authorization_code\",\n"
+ " \"client_credentials\"\n"
+ " ],\n"
+ " \"response_types\":[\n"
+ " \"code\"\n"
+ " ],\n"
+ " \"scope\": \"scope1 scope2\",\n"
+ " \"jwks_uri\": \"https://client.example.com/jwks\",\n"
+ " \"a-claim\": \"a-value\"\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationRequest.getBytes(),
HttpStatus.OK);
OAuth2ClientRegistration clientRegistration = this.messageConverter.readInternal(OAuth2ClientRegistration.class,
response);
assertThat(clientRegistration.getClientId()).isEqualTo("client-id");
assertThat(clientRegistration.getClientIdIssuedAt()).isEqualTo(Instant.ofEpochSecond(1607633867L));
assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret");
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L));
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod())
.isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code",
"client_credentials");
assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value");
}
@Test
public void readInternalWhenClientSecretNoExpiryThenSuccess() {
// @formatter:off
String clientRegistrationRequest = "{\n"
+ " \"client_id\": \"client-id\",\n"
+ " \"client_secret\": \"client-secret\",\n"
+ " \"client_secret_expires_at\": 0,\n"
+ " \"redirect_uris\": [\n"
+ " \"https://client.example.com\"\n"
+ " ]\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(clientRegistrationRequest.getBytes(),
HttpStatus.OK);
OAuth2ClientRegistration clientRegistration = this.messageConverter.readInternal(OAuth2ClientRegistration.class,
response);
assertThat(clientRegistration.getClaims()).hasSize(3);
assertThat(clientRegistration.getClientId()).isEqualTo("client-id");
assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret");
assertThat(clientRegistration.getClientSecretExpiresAt()).isNull();
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
}
@Test
public void readInternalWhenFailingConverterThenThrowException() {
String errorMessage = "this is not a valid converter";
this.messageConverter.setClientRegistrationConverter((source) -> {
throw new RuntimeException(errorMessage);
});
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OAuth2ClientRegistration.class, response))
.withMessageContaining("An error occurred reading the OAuth 2.0 Client Registration")
.withMessageContaining(errorMessage);
}
@Test
public void writeInternalWhenClientRegistrationThenSuccess() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientIdIssuedAt(Instant.ofEpochSecond(1607633867))
.clientSecret("client-secret")
.clientSecretExpiresAt(Instant.ofEpochSecond(1607637467))
.clientName("client-name")
.redirectUri("https://client.example.com")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope("scope1")
.scope("scope2")
.jwkSetUrl("https://client.example.com/jwks")
.claim("a-claim", "a-value")
.build();
// @formatter:on
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.messageConverter.writeInternal(clientRegistration, outputMessage);
String clientRegistrationResponse = outputMessage.getBodyAsString();
assertThat(clientRegistrationResponse).contains("\"client_id\":\"client-id\"");
assertThat(clientRegistrationResponse).contains("\"client_id_issued_at\":1607633867");
assertThat(clientRegistrationResponse).contains("\"client_secret\":\"client-secret\"");
assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":1607637467");
assertThat(clientRegistrationResponse).contains("\"client_name\":\"client-name\"");
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_basic\"");
assertThat(clientRegistrationResponse)
.contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");
assertThat(clientRegistrationResponse).contains("\"response_types\":[\"code\"]");
assertThat(clientRegistrationResponse).contains("\"scope\":\"scope1 scope2\"");
assertThat(clientRegistrationResponse).contains("\"jwks_uri\":\"https://client.example.com/jwks\"");
assertThat(clientRegistrationResponse).contains("\"a-claim\":\"a-value\"");
}
@Test
public void writeInternalWhenClientSecretNoExpiryThenSuccess() {
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientSecret("client-secret")
.redirectUri("https://client.example.com")
.build();
// @formatter:on
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.messageConverter.writeInternal(clientRegistration, outputMessage);
String clientRegistrationResponse = outputMessage.getBodyAsString();
assertThat(clientRegistrationResponse).contains("\"client_id\":\"client-id\"");
assertThat(clientRegistrationResponse).contains("\"client_secret\":\"client-secret\"");
assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":0");
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]");
}
@Test
public void writeInternalWhenWriteFailsThenThrowException() {
String errorMessage = "this is not a valid converter";
Converter<OAuth2ClientRegistration, Map<String, Object>> failingConverter = (source) -> {
throw new RuntimeException(errorMessage);
};
this.messageConverter.setClientRegistrationParametersConverter(failingConverter);
// @formatter:off
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
.redirectUri("https://client.example.com")
.build();
// @formatter:off
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThatExceptionOfType(HttpMessageNotWritableException.class).isThrownBy(() -> this.messageConverter.writeInternal(clientRegistration, outputMessage))
.withMessageContaining("An error occurred writing the OAuth 2.0 Client Registration")
.withMessageContaining(errorMessage);
}
}

View File

@ -41,6 +41,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(authorizationServerSettings.getClientRegistrationEndpoint()).isEqualTo("/oauth2/register");
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo("/connect/logout");
@ -54,6 +55,7 @@ public class AuthorizationServerSettingsTests {
String jwkSetEndpoint = "/oauth2/v1/jwks";
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
String clientRegistrationEndpoint = "/oauth2/v1/register";
String oidcClientRegistrationEndpoint = "/connect/v1/register";
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
String oidcLogoutEndpoint = "/connect/v1/logout";
@ -68,6 +70,7 @@ public class AuthorizationServerSettingsTests {
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.clientRegistrationEndpoint(clientRegistrationEndpoint)
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
.oidcLogoutEndpoint(oidcLogoutEndpoint)
@ -82,6 +85,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
assertThat(authorizationServerSettings.getClientRegistrationEndpoint()).isEqualTo(clientRegistrationEndpoint);
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint())
.isEqualTo(oidcClientRegistrationEndpoint);
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
@ -111,6 +115,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(authorizationServerSettings.getClientRegistrationEndpoint()).isEqualTo("/oauth2/register");
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo("/connect/logout");
@ -123,7 +128,7 @@ public class AuthorizationServerSettingsTests {
.settings((settings) -> settings.put("name2", "value2"))
.build();
assertThat(authorizationServerSettings.getSettings()).hasSize(14);
assertThat(authorizationServerSettings.getSettings()).hasSize(15);
assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
}
@ -168,6 +173,13 @@ public class AuthorizationServerSettingsTests {
.withMessage("value cannot be null");
}
@Test
public void clientRegistrationEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorizationServerSettings.builder().clientRegistrationEndpoint(null))
.withMessage("value cannot be null");
}
@Test
public void oidcClientRegistrationEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()

View File

@ -0,0 +1,415 @@
/*
* 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.oauth2.server.authorization.web;
import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.TestJwsHeaders;
import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientRegistrationAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.http.converter.OAuth2ClientRegistrationHttpMessageConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link OAuth2ClientRegistrationEndpointFilter}.
*
* @author Joe Grandja
*/
public class OAuth2ClientRegistrationEndpointFilterTests {
private static final String DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI = "/oauth2/register";
private AuthenticationManager authenticationManager;
private OAuth2ClientRegistrationEndpointFilter filter;
private final HttpMessageConverter<OAuth2ClientRegistration> clientRegistrationHttpMessageConverter = new OAuth2ClientRegistrationHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
@BeforeEach
public void setup() {
this.authenticationManager = mock(AuthenticationManager.class);
this.filter = new OAuth2ClientRegistrationEndpointFilter(this.authenticationManager);
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new OAuth2ClientRegistrationEndpointFilter(null))
.withMessage("authenticationManager cannot be null");
}
@Test
public void constructorWhenClientRegistrationEndpointUriNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OAuth2ClientRegistrationEndpointFilter(this.authenticationManager, null))
.withMessage("clientRegistrationEndpointUri cannot be empty");
}
@Test
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationConverter(null))
.withMessage("authenticationConverter cannot be null");
}
@Test
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
.withMessage("authenticationSuccessHandler cannot be null");
}
@Test
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
.withMessage("authenticationFailureHandler cannot be null");
}
@Test
public void doFilterWhenNotClientRegistrationRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenClientRegistrationRequestGetThenNotProcessed() throws Exception {
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenClientRegistrationRequestInvalidThenInvalidRequestError() throws Exception {
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setContent("invalid content".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
OAuth2Error error = readError(response);
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).startsWith("OAuth 2.0 Client Registration Error: ");
}
@Test
public void doFilterWhenClientRegistrationRequestInvalidTokenThenUnauthorizedError() throws Exception {
doFilterWhenClientRegistrationRequestInvalidThenError(OAuth2ErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED);
}
@Test
public void doFilterWhenClientRegistrationRequestInsufficientTokenScopeThenForbiddenError() throws Exception {
doFilterWhenClientRegistrationRequestInvalidThenError(OAuth2ErrorCodes.INSUFFICIENT_SCOPE,
HttpStatus.FORBIDDEN);
}
private void doFilterWhenClientRegistrationRequestInvalidThenError(String errorCode, HttpStatus status)
throws Exception {
Jwt jwt = createJwt("client.create");
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(principal);
SecurityContextHolder.setContext(securityContext);
given(this.authenticationManager.authenticate(any())).willThrow(new OAuth2AuthenticationException(errorCode));
// @formatter:off
OAuth2ClientRegistration clientRegistrationRequest = 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
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
writeClientRegistrationRequest(request, clientRegistrationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(status.value());
OAuth2Error error = readError(response);
assertThat(error.getErrorCode()).isEqualTo(errorCode);
}
@Test
public void doFilterWhenClientRegistrationRequestValidThenSuccessResponse() throws Exception {
// @formatter:off
OAuth2ClientRegistration expectedClientRegistrationResponse = createClientRegistration();
OAuth2ClientRegistration clientRegistrationRequest = OAuth2ClientRegistration.builder()
.clientName(expectedClientRegistrationResponse.getClientName())
.redirectUris((redirectUris) -> redirectUris.addAll(expectedClientRegistrationResponse.getRedirectUris()))
.grantTypes((grantTypes) -> grantTypes.addAll(expectedClientRegistrationResponse.getGrantTypes()))
.scopes((scopes) -> scopes.addAll(expectedClientRegistrationResponse.getScopes()))
.build();
// @formatter:on
Jwt jwt = createJwt("client.create");
JwtAuthenticationToken principal = new JwtAuthenticationToken(jwt,
AuthorityUtils.createAuthorityList("SCOPE_client.create"));
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult = new OAuth2ClientRegistrationAuthenticationToken(
principal, expectedClientRegistrationResponse);
given(this.authenticationManager.authenticate(any())).willReturn(clientRegistrationAuthenticationResult);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(principal);
SecurityContextHolder.setContext(securityContext);
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
writeClientRegistrationRequest(request, clientRegistrationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
OAuth2ClientRegistration clientRegistrationResponse = readClientRegistrationResponse(response);
assertThat(clientRegistrationResponse.getClientId())
.isEqualTo(expectedClientRegistrationResponse.getClientId());
assertThat(clientRegistrationResponse.getClientIdIssuedAt()).isBetween(
expectedClientRegistrationResponse.getClientIdIssuedAt().minusSeconds(1),
expectedClientRegistrationResponse.getClientIdIssuedAt().plusSeconds(1));
assertThat(clientRegistrationResponse.getClientSecret())
.isEqualTo(expectedClientRegistrationResponse.getClientSecret());
assertThat(clientRegistrationResponse.getClientSecretExpiresAt())
.isEqualTo(expectedClientRegistrationResponse.getClientSecretExpiresAt());
assertThat(clientRegistrationResponse.getClientName())
.isEqualTo(expectedClientRegistrationResponse.getClientName());
assertThat(clientRegistrationResponse.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getRedirectUris());
assertThat(clientRegistrationResponse.getGrantTypes())
.containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getGrantTypes());
assertThat(clientRegistrationResponse.getResponseTypes())
.containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getResponseTypes());
assertThat(clientRegistrationResponse.getScopes())
.containsExactlyInAnyOrderElementsOf(expectedClientRegistrationResponse.getScopes());
assertThat(clientRegistrationResponse.getTokenEndpointAuthenticationMethod())
.isEqualTo(expectedClientRegistrationResponse.getTokenEndpointAuthenticationMethod());
}
@Test
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws ServletException, IOException {
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
this.filter.setAuthenticationConverter(authenticationConverter);
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationConverter).convert(request);
}
@Test
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
OAuth2ClientRegistration expectedClientRegistrationResponse = createClientRegistration();
Authentication principal = new TestingAuthenticationToken("principal", "Credentials");
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult = new OAuth2ClientRegistrationAuthenticationToken(
principal, expectedClientRegistrationResponse);
given(this.authenticationManager.authenticate(any())).willReturn(clientRegistrationAuthenticationResult);
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
this.filter.setAuthenticationSuccessHandler(successHandler);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(principal);
SecurityContextHolder.setContext(securityContext);
// @formatter:off
OAuth2ClientRegistration clientRegistrationRequest = 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
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
writeClientRegistrationRequest(request, clientRegistrationRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(successHandler).onAuthenticationSuccess(request, response, clientRegistrationAuthenticationResult);
}
@Test
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
String requestUri = DEFAULT_OAUTH2_CLIENT_REGISTRATION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setContent("invalid content".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationFailureHandler).onAuthenticationFailure(eq(request), eq(response),
any(OAuth2AuthenticationException.class));
}
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
}
private void writeClientRegistrationRequest(MockHttpServletRequest request,
OAuth2ClientRegistration clientRegistration) throws Exception {
MockClientHttpRequest httpRequest = new MockClientHttpRequest();
this.clientRegistrationHttpMessageConverter.write(clientRegistration, null, httpRequest);
request.setContent(httpRequest.getBodyAsBytes());
}
private OAuth2ClientRegistration readClientRegistrationResponse(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return this.clientRegistrationHttpMessageConverter.read(OAuth2ClientRegistration.class, httpResponse);
}
private static OAuth2ClientRegistration createClientRegistration() {
// @formatter:off
return OAuth2ClientRegistration.builder()
.clientId("client-id")
.clientIdIssuedAt(Instant.now())
.clientSecret("client-secret")
.clientName("client-name")
.redirectUri("https://client.example.com")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.scope("scope1")
.scope("scope2")
.build();
// @formatter:on
}
private static Jwt createJwt(String scope) {
// @formatter:off
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader()
.build();
JwtClaimsSet jwtClaimsSet = TestJwtClaimsSets.jwtClaimsSet()
.claim(OAuth2ParameterNames.SCOPE, Collections.singleton(scope))
.build();
Jwt jwt = Jwt.withTokenValue("jwt-access-token")
.headers((headers) -> headers.putAll(jwsHeader.getHeaders()))
.claims((claims) -> claims.putAll(jwtClaimsSet.getClaims()))
.build();
// @formatter:on
return jwt;
}
}