Merge branch 0.4.x into main
The following commits are merged using the default merge strategy.8d7f8b3420Improve customizing OIDC UserInfo endpoint2ba711c83aPolish gh-929efbfdc234cImprove customizing OIDC Client Registration endpointbfd7a09c3bPolish gh-94611ce8ef201Polish gh-929356d669a78Fix URL encoding for authorization request state parameter4eb25c163fPolish gh-9206dc3944eefAdd OidcClientRegistrationAuthenticationProvider.setRegisteredClientConverter()
This commit is contained in:
@@ -84,7 +84,7 @@ public class TestOAuth2Authorizations {
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizedScopes(authorizationRequest.getScopes())
|
||||
.token(authorizationCode)
|
||||
.attribute(OAuth2ParameterNames.STATE, "state")
|
||||
.attribute(OAuth2ParameterNames.STATE, "consent-state")
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
|
||||
.attribute(Principal.class.getName(),
|
||||
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"));
|
||||
|
||||
@@ -70,6 +70,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
@@ -160,6 +161,9 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
|
||||
private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
|
||||
private static final String AUTHORITIES_CLAIM = "authorities";
|
||||
private static final String STATE_URL_UNENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ+004pwm9j55li7BoydXYysH4enZMF21Q";
|
||||
private static final String STATE_URL_ENCODED = "awrD0fCnEcTUPFgmyy2SU89HZNcnAJ60ZW6l39YI0KyVjmIZ%2B004pwm9j55li7BoydXYysH4enZMF21Q";
|
||||
|
||||
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
|
||||
@@ -291,7 +295,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -383,7 +387,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -427,7 +431,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -503,19 +507,27 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName("user")
|
||||
.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
OAuth2AuthorizationRequest updatedAuthorizationRequest =
|
||||
OAuth2AuthorizationRequest.from(authorizationRequest)
|
||||
.state(STATE_URL_UNENCODED)
|
||||
.build();
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.read")
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.write")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -583,18 +595,26 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.build();
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
OAuth2AuthorizationRequest updatedAuthorizationRequest =
|
||||
OAuth2AuthorizationRequest.from(authorizationRequest)
|
||||
.state(STATE_URL_UNENCODED)
|
||||
.build();
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), updatedAuthorizationRequest)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param("authority", "authority-1 authority-2")
|
||||
.param(OAuth2ParameterNames.STATE, "state")
|
||||
.param(OAuth2ParameterNames.STATE, authorization.<String>getAttribute(OAuth2ParameterNames.STATE))
|
||||
.with(user("principal")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
assertThat(redirectedUrl).matches("https://example.com\\?code=.{15,}&state=" + STATE_URL_ENCODED);
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
@@ -632,7 +652,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
"https://provider.com/oauth2/authorize", registeredClient.getClientId(), principal, authorizationCode,
|
||||
registeredClient.getRedirectUris().iterator().next(), "state", registeredClient.getScopes());
|
||||
registeredClient.getRedirectUris().iterator().next(), STATE_URL_UNENCODED, registeredClient.getScopes());
|
||||
when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
|
||||
when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true);
|
||||
when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
|
||||
@@ -718,7 +738,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
parameters.set(OAuth2ParameterNames.STATE, "state");
|
||||
parameters.set(OAuth2ParameterNames.STATE, STATE_URL_UNENCODED);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ package org.springframework.security.oauth2.server.authorization.config.annotati
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
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.source.JWKSource;
|
||||
@@ -30,6 +34,7 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -38,6 +43,7 @@ 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;
|
||||
@@ -46,6 +52,7 @@ 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;
|
||||
@@ -55,6 +62,7 @@ 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;
|
||||
@@ -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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
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.web.authentication.OidcClientRegistrationAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
||||
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.test.web.servlet.MockMvc;
|
||||
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.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.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
@@ -128,6 +151,18 @@ public class OidcClientRegistrationTests {
|
||||
@Autowired
|
||||
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 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/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);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -158,6 +199,7 @@ public class OidcClientRegistrationTests {
|
||||
.setBody(clientJwkSet.toString());
|
||||
// @formatter:on
|
||||
this.server.enqueue(response);
|
||||
when(authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).thenReturn(true);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -165,6 +207,12 @@ public class OidcClientRegistrationTests {
|
||||
this.server.shutdown();
|
||||
jdbcOperations.update("truncate table oauth2_authorization");
|
||||
jdbcOperations.update("truncate table oauth2_registered_client");
|
||||
reset(authenticationConverter);
|
||||
reset(authenticationConvertersConsumer);
|
||||
reset(authenticationProvider);
|
||||
reset(authenticationProvidersConsumer);
|
||||
reset(authenticationSuccessHandler);
|
||||
reset(authenticationFailureHandler);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@@ -261,6 +309,67 @@ public class OidcClientRegistrationTests {
|
||||
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 {
|
||||
// ***** (1) Obtain the "initial" access token used for registering the client
|
||||
|
||||
@@ -353,6 +462,44 @@ public class OidcClientRegistrationTests {
|
||||
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
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class AuthorizationServerConfiguration {
|
||||
|
||||
@@ -19,9 +19,13 @@ import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
@@ -30,11 +34,14 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
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.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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;
|
||||
@@ -62,11 +69,15 @@ 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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
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.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
@@ -75,8 +86,15 @@ import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
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.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -100,17 +118,48 @@ public class OidcUserInfoTests {
|
||||
@Autowired
|
||||
private JwtEncoder jwtEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtDecoder jwtDecoder;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
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 static Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
securityContextRepository = spy(new HttpSessionSecurityContextRepository());
|
||||
authenticationConverter = mock(AuthenticationConverter.class);
|
||||
authenticationConvertersConsumer = mock(Consumer.class);
|
||||
authenticationProvider = mock(AuthenticationProvider.class);
|
||||
authenticationProvidersConsumer = mock(Consumer.class);
|
||||
authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
|
||||
authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
|
||||
userInfoMapper = mock(Function.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
reset(securityContextRepository);
|
||||
reset(authenticationConverter);
|
||||
reset(authenticationConvertersConsumer);
|
||||
reset(authenticationProvider);
|
||||
reset(authenticationProvidersConsumer);
|
||||
reset(authenticationSuccessHandler);
|
||||
reset(authenticationFailureHandler);
|
||||
reset(userInfoMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,19 +195,91 @@ public class OidcUserInfoTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenSignedJwtAndCustomUserInfoMapperThenMapJwtClaimsToUserInfoResponse() throws Exception {
|
||||
public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpectAll(userInfoResponse());
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
// @formatter:on
|
||||
|
||||
verify(userInfoMapper).apply(any());
|
||||
verify(authenticationConverter).convert(any());
|
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
verifyNoInteractions(authenticationFailureHandler);
|
||||
|
||||
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 OidcUserInfoAuthenticationProvider
|
||||
);
|
||||
|
||||
ArgumentCaptor<List<AuthenticationConverter>> authenticationConvertersCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(authenticationConvertersConsumer).accept(authenticationConvertersCaptor.capture());
|
||||
List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
|
||||
assertThat(authenticationConverters).hasSize(2).allMatch(AuthenticationConverter.class::isInstance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationProviderThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
OAuth2Authorization authorization = createAuthorization();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
when(authenticationProvider.supports(eq(OidcUserInfoAuthenticationToken.class))).thenReturn(true);
|
||||
String tokenValue = authorization.getAccessToken().getToken().getTokenValue();
|
||||
Jwt jwt = this.jwtDecoder.decode(tokenValue);
|
||||
OidcUserInfoAuthenticationToken oidcUserInfoAuthentication = new OidcUserInfoAuthenticationToken(
|
||||
new JwtAuthenticationToken(jwt), createUserInfo());
|
||||
when(authenticationProvider.authenticate(any())).thenReturn(oidcUserInfoAuthentication);
|
||||
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
// @formatter:on
|
||||
|
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), any());
|
||||
verify(authenticationProvider).authenticate(any());
|
||||
verifyNoInteractions(authenticationFailureHandler);
|
||||
verifyNoInteractions(userInfoMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenUserInfoEndpointCustomizedWithAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
this.spring.register(CustomUserInfoConfiguration.class).autowire();
|
||||
|
||||
when(userInfoMapper.apply(any())).thenReturn(createUserInfo());
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
HttpServletResponse response = invocation.getArgument(1);
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("unauthorized");
|
||||
return null;
|
||||
}
|
||||
).when(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
||||
|
||||
OAuth2AccessToken accessToken = createAuthorization().getAccessToken().getToken();
|
||||
// @formatter:off
|
||||
this.mvc.perform(get(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
|
||||
.andExpect(status().is4xxClientError());
|
||||
// @formatter:on
|
||||
|
||||
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
|
||||
verifyNoInteractions(authenticationSuccessHandler);
|
||||
verifyNoInteractions(userInfoMapper);
|
||||
}
|
||||
|
||||
// gh-482
|
||||
@@ -273,14 +394,6 @@ public class OidcUserInfoTests {
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
// Custom User Info Mapper that retrieves claims from a signed JWT
|
||||
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = context -> {
|
||||
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
|
||||
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
|
||||
|
||||
return new OidcUserInfo(principal.getToken().getClaims());
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.securityMatcher(endpointsMatcher)
|
||||
@@ -292,6 +405,12 @@ public class OidcUserInfoTests {
|
||||
.apply(authorizationServerConfigurer)
|
||||
.oidc(oidc -> oidc
|
||||
.userInfoEndpoint(userInfo -> userInfo
|
||||
.userInfoRequestConverter(authenticationConverter)
|
||||
.userInfoRequestConverters(authenticationConvertersConsumer)
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.authenticationProviders(authenticationProvidersConsumer)
|
||||
.userInfoResponseHandler(authenticationSuccessHandler)
|
||||
.errorResponseHandler(authenticationFailureHandler)
|
||||
.userInfoMapper(userInfoMapper)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -134,6 +134,13 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
.withMessage("tokenGenerator cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRegisteredClientConverterWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.authenticationProvider.setRegisteredClientConverter(null))
|
||||
.withMessage("registeredClientConverter cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWhenTypeOidcClientRegistrationAuthenticationTokenThenReturnTrue() {
|
||||
assertThat(this.authenticationProvider.supports(OidcClientRegistrationAuthenticationToken.class)).isTrue();
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.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;
|
||||
|
||||
@@ -33,6 +35,8 @@ 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;
|
||||
@@ -54,10 +58,14 @@ import org.springframework.security.oauth2.server.authorization.oidc.OidcClientR
|
||||
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.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.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -68,6 +76,7 @@ import static org.mockito.Mockito.when;
|
||||
*
|
||||
* @author Ovidiu Popa
|
||||
* @author Joe Grandja
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
public class OidcClientRegistrationEndpointFilterTests {
|
||||
private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
|
||||
@@ -103,6 +112,27 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
.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";
|
||||
@@ -203,25 +233,13 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
@Test
|
||||
public void doFilterWhenClientRegistrationRequestValidThenSuccessResponse() throws Exception {
|
||||
// @formatter:off
|
||||
OidcClientRegistration.Builder clientRegistrationBuilder = OidcClientRegistration.builder()
|
||||
.clientName("client-name")
|
||||
.redirectUri("https://client.example.com")
|
||||
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
|
||||
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
|
||||
.scope("scope1")
|
||||
.scope("scope2");
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
|
||||
OidcClientRegistration clientRegistrationRequest = clientRegistrationBuilder.build();
|
||||
|
||||
OidcClientRegistration expectedClientRegistrationResponse = clientRegistrationBuilder
|
||||
.clientId("client-id")
|
||||
.clientIdIssuedAt(Instant.now())
|
||||
.clientSecret("client-secret")
|
||||
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
|
||||
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.registrationAccessToken("registration-access-token")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
OidcClientRegistration clientRegistrationRequest = OidcClientRegistration.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
|
||||
|
||||
@@ -384,23 +402,7 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenClientConfigurationRequestValidThenSuccessResponse() throws Exception {
|
||||
// @formatter:off
|
||||
OidcClientRegistration expectedClientRegistrationResponse = OidcClientRegistration.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())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope("scope1")
|
||||
.scope("scope2")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
.build();
|
||||
// @formatter:on
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
|
||||
Jwt jwt = createJwt("client.read");
|
||||
JwtAuthenticationToken principal = new JwtAuthenticationToken(
|
||||
@@ -452,6 +454,74 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
.isEqualTo(expectedClientRegistrationResponse.getRegistrationClientUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws ServletException, IOException {
|
||||
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
|
||||
this.filter.setAuthenticationConverter(authenticationConverter);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client-id");
|
||||
|
||||
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 {
|
||||
OidcClientRegistration expectedClientRegistrationResponse = createClientRegistration();
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "Credentials");
|
||||
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthenticationResult =
|
||||
new OidcClientRegistrationAuthenticationToken(principal, expectedClientRegistrationResponse);
|
||||
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(clientRegistrationAuthenticationResult);
|
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||
this.filter.setAuthenticationSuccessHandler(successHandler);
|
||||
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(principal);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, expectedClientRegistrationResponse.getClientId());
|
||||
|
||||
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);
|
||||
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
|
||||
String requestUri = DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
request.setParameter(OAuth2ParameterNames.CLIENT_ID, "client1");
|
||||
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()));
|
||||
@@ -471,6 +541,27 @@ public class OidcClientRegistrationEndpointFilterTests {
|
||||
return this.clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
|
||||
}
|
||||
|
||||
private static OidcClientRegistration createClientRegistration() {
|
||||
// @formatter:off
|
||||
OidcClientRegistration clientRegistration = OidcClientRegistration.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())
|
||||
.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
|
||||
.scope("scope1")
|
||||
.scope("scope2")
|
||||
.registrationClientUrl("https://auth-server:9000/connect/register?client_id=client-id")
|
||||
.build();
|
||||
return clientRegistration;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static Jwt createJwt(String scope) {
|
||||
// @formatter:off
|
||||
JwsHeader jwsHeader = TestJwsHeaders.jwsHeader()
|
||||
|
||||
@@ -44,6 +44,9 @@ import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
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;
|
||||
@@ -84,6 +87,27 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
.withMessage("userInfoEndpointUri 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 doFilterWhenNotUserInfoRequestThenNotProcessed() throws Exception {
|
||||
String requestUri = "/path";
|
||||
@@ -145,11 +169,21 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestInvalidTokenThenUnauthorizedError() throws Exception {
|
||||
doFilterWhenAuthenticationExceptionThenError(OAuth2ErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserInfoRequestInsufficientScopeThenForbiddenError() throws Exception {
|
||||
doFilterWhenAuthenticationExceptionThenError(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private void doFilterWhenAuthenticationExceptionThenError(String oauth2ErrorCode, HttpStatus httpStatus)
|
||||
throws Exception {
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
when(this.authenticationManager.authenticate(any()))
|
||||
.thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN));
|
||||
.thenThrow(new OAuth2AuthenticationException(oauth2ErrorCode));
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
@@ -161,9 +195,82 @@ public class OidcUserInfoEndpointFilterTests {
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||
assertThat(response.getStatus()).isEqualTo(httpStatus.value());
|
||||
OAuth2Error error = readError(response);
|
||||
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
assertThat(error.getErrorCode()).isEqualTo(oauth2ErrorCode);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal);
|
||||
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
|
||||
this.filter.setAuthenticationConverter(authenticationConverter);
|
||||
|
||||
when(authenticationConverter.convert(any())).thenReturn(authentication);
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(
|
||||
new OidcUserInfoAuthenticationToken(principal, createUserInfo())
|
||||
);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_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);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(authenticationConverter).convert(request);
|
||||
verify(this.authenticationManager).authenticate(authentication);
|
||||
assertUserInfoResponse(response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
|
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||
this.filter.setAuthenticationSuccessHandler(successHandler);
|
||||
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OidcUserInfoAuthenticationToken authentication = new OidcUserInfoAuthenticationToken(principal, createUserInfo());
|
||||
when(this.authenticationManager.authenticate(any())).thenReturn(authentication);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_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);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(successHandler).onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
this.filter.setAuthenticationFailureHandler(failureHandler);
|
||||
|
||||
Authentication principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
SecurityContextHolder.getContext().setAuthentication(principal);
|
||||
|
||||
OAuth2AuthenticationException authenticationException =
|
||||
new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
|
||||
when(this.authenticationManager.authenticate(any())).thenThrow(authenticationException);
|
||||
|
||||
String requestUri = DEFAULT_OIDC_USER_INFO_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);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
verify(failureHandler).onAuthenticationFailure(request, response, authenticationException);
|
||||
}
|
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user