|
@@ -18,6 +18,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
|
import java.time.Instant;
|
|
import java.time.Instant;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
|
+import java.util.List;
|
|
|
|
+import java.util.function.Consumer;
|
|
|
|
+
|
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
|
import com.nimbusds.jose.jwk.JWKSet;
|
|
import com.nimbusds.jose.jwk.JWKSet;
|
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
|
@@ -30,6 +34,7 @@ import org.junit.Before;
|
|
import org.junit.BeforeClass;
|
|
import org.junit.BeforeClass;
|
|
import org.junit.Rule;
|
|
import org.junit.Rule;
|
|
import org.junit.Test;
|
|
import org.junit.Test;
|
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Bean;
|
|
@@ -38,6 +43,7 @@ import org.springframework.http.HttpHeaders;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.converter.HttpMessageConverter;
|
|
import org.springframework.http.converter.HttpMessageConverter;
|
|
|
|
+import org.springframework.http.server.ServletServerHttpResponse;
|
|
import org.springframework.jdbc.core.JdbcOperations;
|
|
import org.springframework.jdbc.core.JdbcOperations;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
|
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
|
@@ -46,6 +52,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
|
import org.springframework.mock.http.MockHttpOutputMessage;
|
|
import org.springframework.mock.http.MockHttpOutputMessage;
|
|
import org.springframework.mock.http.client.MockClientHttpResponse;
|
|
import org.springframework.mock.http.client.MockClientHttpResponse;
|
|
import org.springframework.mock.web.MockHttpServletResponse;
|
|
import org.springframework.mock.web.MockHttpServletResponse;
|
|
|
|
+import org.springframework.security.authentication.AuthenticationProvider;
|
|
import org.springframework.security.config.Customizer;
|
|
import org.springframework.security.config.Customizer;
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
@@ -55,6 +62,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
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.OAuth2AccessTokenResponse;
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
@@ -77,11 +85,18 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
|
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
|
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
|
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
|
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientConfigurationAuthenticationProvider;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationProvider;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcClientRegistrationAuthenticationToken;
|
|
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
|
import org.springframework.security.oauth2.server.authorization.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
|
|
|
|
+import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
|
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
|
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
|
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
|
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
|
import org.springframework.security.web.SecurityFilterChain;
|
|
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.security.web.util.matcher.RequestMatcher;
|
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
import org.springframework.test.web.servlet.MvcResult;
|
|
import org.springframework.test.web.servlet.MvcResult;
|
|
@@ -89,6 +104,14 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.hamcrest.CoreMatchers.containsString;
|
|
import static org.hamcrest.CoreMatchers.containsString;
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
+import static org.mockito.Mockito.doAnswer;
|
|
|
|
+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.mockito.Mockito.when;
|
|
|
|
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
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.header;
|
|
@@ -128,6 +151,18 @@ public class OidcClientRegistrationTests {
|
|
@Autowired
|
|
@Autowired
|
|
private AuthorizationServerSettings authorizationServerSettings;
|
|
private AuthorizationServerSettings authorizationServerSettings;
|
|
|
|
|
|
|
|
+ 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;
|
|
private MockWebServer server;
|
|
private String clientJwkSetUrl;
|
|
private String clientJwkSetUrl;
|
|
|
|
|
|
@@ -145,6 +180,12 @@ public class OidcClientRegistrationTests {
|
|
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
|
|
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
|
|
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
|
|
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
|
|
.build();
|
|
.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);
|
|
}
|
|
}
|
|
|
|
|
|
@Before
|
|
@Before
|
|
@@ -158,6 +199,7 @@ public class OidcClientRegistrationTests {
|
|
.setBody(clientJwkSet.toString());
|
|
.setBody(clientJwkSet.toString());
|
|
// @formatter:on
|
|
// @formatter:on
|
|
this.server.enqueue(response);
|
|
this.server.enqueue(response);
|
|
|
|
+ when(authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).thenReturn(true);
|
|
}
|
|
}
|
|
|
|
|
|
@After
|
|
@After
|
|
@@ -165,6 +207,12 @@ public class OidcClientRegistrationTests {
|
|
this.server.shutdown();
|
|
this.server.shutdown();
|
|
jdbcOperations.update("truncate table oauth2_authorization");
|
|
jdbcOperations.update("truncate table oauth2_authorization");
|
|
jdbcOperations.update("truncate table oauth2_registered_client");
|
|
jdbcOperations.update("truncate table oauth2_registered_client");
|
|
|
|
+ reset(authenticationConverter);
|
|
|
|
+ reset(authenticationConvertersConsumer);
|
|
|
|
+ reset(authenticationProvider);
|
|
|
|
+ reset(authenticationProvidersConsumer);
|
|
|
|
+ reset(authenticationSuccessHandler);
|
|
|
|
+ reset(authenticationFailureHandler);
|
|
}
|
|
}
|
|
|
|
|
|
@AfterClass
|
|
@AfterClass
|
|
@@ -261,6 +309,67 @@ public class OidcClientRegistrationTests {
|
|
assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull();
|
|
assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenClientRegistrationEndpointCustomizedThenUsed() throws Exception {
|
|
|
|
+ this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
|
|
|
|
+ .clientName("client-name")
|
|
|
|
+ .redirectUri("https://client.example.com")
|
|
|
|
+ .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
|
|
|
+ .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
|
|
|
+ .scope("scope1")
|
|
|
|
+ .scope("scope2")
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+
|
|
|
|
+ doAnswer(invocation -> {
|
|
|
|
+ HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
|
|
|
|
+ ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
|
|
|
+ httpResponse.setStatusCode(HttpStatus.CREATED);
|
|
|
|
+ new OidcClientRegistrationHttpMessageConverter().write(clientRegistration, null, httpResponse);
|
|
|
|
+ return null;
|
|
|
|
+ }).when(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 OidcClientRegistrationAuthenticationConverter);
|
|
|
|
+
|
|
|
|
+ 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(3)
|
|
|
|
+ .allMatch(provider -> provider == authenticationProvider
|
|
|
|
+ || provider instanceof OidcClientRegistrationAuthenticationProvider
|
|
|
|
+ || provider instanceof OidcClientConfigurationAuthenticationProvider);
|
|
|
|
+
|
|
|
|
+ verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
|
|
|
+ verifyNoInteractions(authenticationFailureHandler);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Test
|
|
|
|
+ public void requestWhenClientRegistrationEndpointCustomizedWithAuthenticationFailureHandlerThenUsed() throws Exception {
|
|
|
|
+ this.spring.register(CustomClientRegistrationConfiguration.class).autowire();
|
|
|
|
+
|
|
|
|
+ when(authenticationProvider.authenticate(any())).thenThrow(new OAuth2AuthenticationException("error"));
|
|
|
|
+
|
|
|
|
+ this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)
|
|
|
|
+ .param(OAuth2ParameterNames.CLIENT_ID, "invalid").with(jwt()));
|
|
|
|
+
|
|
|
|
+ verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
|
|
|
+ verifyNoInteractions(authenticationSuccessHandler);
|
|
|
|
+ }
|
|
|
|
+
|
|
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
|
private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
|
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
|
// ***** (1) Obtain the "initial" access token used for registering the client
|
|
|
|
|
|
@@ -353,6 +462,44 @@ public class OidcClientRegistrationTests {
|
|
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
|
return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ @EnableWebSecurity
|
|
|
|
+ @Configuration(proxyBeanMethods = false)
|
|
|
|
+ static class CustomClientRegistrationConfiguration extends AuthorizationServerConfiguration {
|
|
|
|
+
|
|
|
|
+ // @formatter:off
|
|
|
|
+ @Bean
|
|
|
|
+ @Override
|
|
|
|
+ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
|
|
|
+ OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
|
|
|
+ new OAuth2AuthorizationServerConfigurer();
|
|
|
|
+ authorizationServerConfigurer
|
|
|
|
+ .oidc(oidc ->
|
|
|
|
+ oidc
|
|
|
|
+ .clientRegistrationEndpoint(clientRegistration ->
|
|
|
|
+ clientRegistration
|
|
|
|
+ .clientRegistrationRequestConverter(authenticationConverter)
|
|
|
|
+ .clientRegistrationRequestConverters(authenticationConvertersConsumer)
|
|
|
|
+ .authenticationProvider(authenticationProvider)
|
|
|
|
+ .authenticationProviders(authenticationProvidersConsumer)
|
|
|
|
+ .clientRegistrationResponseHandler(authenticationSuccessHandler)
|
|
|
|
+ .errorResponseHandler(authenticationFailureHandler)
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
|
|
|
+
|
|
|
|
+ http
|
|
|
|
+ .securityMatcher(endpointsMatcher)
|
|
|
|
+ .authorizeHttpRequests(authorize ->
|
|
|
|
+ authorize.anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
|
|
|
+ .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
|
|
|
+ .apply(authorizationServerConfigurer);
|
|
|
|
+ return http.build();
|
|
|
|
+ }
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
@EnableWebSecurity
|
|
@EnableWebSecurity
|
|
@Configuration(proxyBeanMethods = false)
|
|
@Configuration(proxyBeanMethods = false)
|
|
static class AuthorizationServerConfiguration {
|
|
static class AuthorizationServerConfiguration {
|