Allow customization of redirect strategy

The default redirect strategy will provide authorization redirect
URI within HTTP 302 response Location header.
Allowing the configuration of custom redirect strategy will provide
an option for the clients to obtain the authorization URI from e.g.
HTTP response body as JSON payload, without a need to handle
automatic redirection initiated by the HTTP Location header.

Closes gh-11373
This commit is contained in:
Igor Bolic
2022-06-17 09:42:50 +02:00
committed by Rob Winch
parent c9f8d2b111
commit efaee4e56b
27 changed files with 712 additions and 2 deletions

View File

@@ -59,6 +59,8 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -69,6 +71,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -96,6 +99,8 @@ public class OAuth2ClientConfigurerTests {
private static OAuth2AuthorizationRequestResolver authorizationRequestResolver;
private static RedirectStrategy authorizationRedirectStrategy;
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private static RequestCache requestCache;
@@ -131,6 +136,7 @@ public class OAuth2ClientConfigurerTests {
authorizedClientService);
authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
"/oauth2/authorization");
authorizationRedirectStrategy = new DefaultRedirectStrategy();
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234")
.tokenType(OAuth2AccessToken.TokenType.BEARER).expiresIn(300).build();
accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
@@ -262,6 +268,19 @@ public class OAuth2ClientConfigurerTests {
verify(authorizationRequestResolver).resolve(any());
}
@Test
public void configureWhenCustomAuthorizationRedirectStrategySetThenAuthorizationRedirectStrategyUsed()
throws Exception {
authorizationRedirectStrategy = mock(RedirectStrategy.class);
this.spring.register(OAuth2ClientConfig.class).autowire();
// @formatter:off
this.mockMvc.perform(get("/oauth2/authorization/registration-1"))
.andExpect(status().isOk())
.andReturn();
// @formatter:on
verify(authorizationRedirectStrategy).sendRedirect(any(), any(), anyString());
}
@EnableWebSecurity
@EnableWebMvc
static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
@@ -279,6 +298,7 @@ public class OAuth2ClientConfigurerTests {
.oauth2Client()
.authorizationCodeGrant()
.authorizationRequestResolver(authorizationRequestResolver)
.authorizationRedirectStrategy(authorizationRedirectStrategy)
.accessTokenResponseClient(accessTokenResponseClient);
// @formatter:on
}

View File

@@ -90,6 +90,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@@ -101,7 +102,9 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -382,6 +385,32 @@ public class OAuth2LoginConfigurerTests {
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1");
}
@Test
public void oauth2LoginWithAuthorizationRedirectStrategyThenCustomAuthorizationRedirectStrategyUsed()
throws Exception {
loadConfig(OAuth2LoginConfigCustomAuthorizationRedirectStrategy.class);
RedirectStrategy redirectStrategy = this.context
.getBean(OAuth2LoginConfigCustomAuthorizationRedirectStrategy.class).redirectStrategy;
String requestUri = "/oauth2/authorization/google";
this.request = new MockHttpServletRequest("GET", requestUri);
this.request.setServletPath(requestUri);
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
then(redirectStrategy).should().sendRedirect(any(), any(), anyString());
}
@Test
public void requestWhenOauth2LoginWithCustomAuthorizationRedirectStrategyThenCustomAuthorizationRedirectStrategyUsed()
throws Exception {
loadConfig(OAuth2LoginConfigCustomAuthorizationRedirectStrategyInLambda.class);
RedirectStrategy redirectStrategy = this.context
.getBean(OAuth2LoginConfigCustomAuthorizationRedirectStrategyInLambda.class).redirectStrategy;
String requestUri = "/oauth2/authorization/google";
this.request = new MockHttpServletRequest("GET", requestUri);
this.request.setServletPath(requestUri);
this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain);
then(redirectStrategy).should().sendRedirect(any(), any(), anyString());
}
// gh-5347
@Test
public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception {
@@ -883,6 +912,59 @@ public class OAuth2LoginConfigurerTests {
}
@EnableWebSecurity
static class OAuth2LoginConfigCustomAuthorizationRedirectStrategy extends CommonWebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository = new InMemoryClientRegistrationRepository(
GOOGLE_CLIENT_REGISTRATION);
RedirectStrategy redirectStrategy = mock(RedirectStrategy.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.oauth2Login((oauth2Login) ->
oauth2Login
.clientRegistrationRepository(this.clientRegistrationRepository)
.authorizationEndpoint((authorizationEndpoint) ->
authorizationEndpoint
.authorizationRedirectStrategy(this.redirectStrategy)
)
);
// @formatter:on
super.configure(http);
}
}
@EnableWebSecurity
static class OAuth2LoginConfigCustomAuthorizationRedirectStrategyInLambda
extends CommonLambdaWebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository = new InMemoryClientRegistrationRepository(
GOOGLE_CLIENT_REGISTRATION);
RedirectStrategy redirectStrategy = mock(RedirectStrategy.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.oauth2Login((oauth2Login) ->
oauth2Login
.clientRegistrationRepository(this.clientRegistrationRepository)
.authorizationEndpoint((authorizationEndpoint) ->
authorizationEndpoint
.authorizationRedirectStrategy(this.redirectStrategy)
)
);
// @formatter:on
super.configure(http);
}
}
@EnableWebSecurity
static class OAuth2LoginConfigMultipleClients extends CommonWebSecurityConfigurerAdapter {

View File

@@ -44,6 +44,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -55,6 +56,7 @@ import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -90,6 +92,9 @@ public class OAuth2ClientBeanDefinitionParserTests {
@Autowired(required = false)
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
@Autowired(required = false)
private RedirectStrategy authorizationRedirectStrategy;
@Autowired(required = false)
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@@ -148,6 +153,16 @@ public class OAuth2ClientBeanDefinitionParserTests {
verify(this.authorizationRequestResolver).resolve(any());
}
@Test
public void requestWhenCustomAuthorizationRedirectStrategyThenCalled() throws Exception {
this.spring.configLocations(xml("CustomAuthorizationRedirectStrategy")).autowire();
// @formatter:off
this.mvc.perform(get("/oauth2/authorization/google"))
.andExpect(status().isOk());
// @formatter:on
verify(this.authorizationRedirectStrategy).sendRedirect(any(), any(), anyString());
}
@Test
public void requestWhenAuthorizationResponseMatchThenProcess() throws Exception {
this.spring.configLocations(xml("CustomConfiguration")).autowire();

View File

@@ -64,6 +64,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.TestJwts;
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.RequestCache;
@@ -78,6 +79,7 @@ import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
@@ -118,6 +120,9 @@ public class OAuth2LoginBeanDefinitionParserTests {
@Autowired(required = false)
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
@Autowired(required = false)
private RedirectStrategy authorizationRedirectStrategy;
@Autowired(required = false)
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@@ -378,6 +383,17 @@ public class OAuth2LoginBeanDefinitionParserTests {
verify(this.authorizationRequestResolver).resolve(any());
}
@Test
public void requestWhenCustomAuthorizationRedirectStrategyThenCalled() throws Exception {
this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthorizationRedirectStrategy"))
.autowire();
// @formatter:off
this.mvc.perform(get("/oauth2/authorization/google-login"))
.andExpect(status().isOk());
// @formatter:on
verify(this.authorizationRedirectStrategy).sendRedirect(any(), any(), anyString());
}
// gh-5347
@Test
public void requestWhenMultiClientRegistrationThenRedirectDefaultLoginPage() throws Exception {

View File

@@ -39,14 +39,18 @@ import org.springframework.security.config.annotation.web.reactive.ServerHttpSec
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
@@ -76,6 +80,7 @@ import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -531,6 +536,90 @@ public class ServerHttpSecurityTests {
verify(authorizationRequestRepository).removeAuthorizationRequest(any());
}
@Test
public void shouldUseDefaultAuthorizationRedirectStrategyForOAuth2Login() {
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
ReactiveClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(anyString()))
.willReturn(Mono.just(TestClientRegistrations.clientRegistration().build()));
SecurityWebFilterChain securityFilterChain = this.http.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository).and().build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/oauth2/authorization/registration-id").exchange().expectStatus().is3xxRedirection();
OAuth2AuthorizationRequestRedirectWebFilter filter = getWebFilter(securityFilterChain,
OAuth2AuthorizationRequestRedirectWebFilter.class).get();
assertThat(ReflectionTestUtils.getField(filter, "authorizationRedirectStrategy"))
.isInstanceOf(DefaultServerRedirectStrategy.class);
}
@Test
public void shouldConfigureAuthorizationRedirectStrategyForOAuth2Login() {
ServerRedirectStrategy authorizationRedirectStrategy = mock(ServerRedirectStrategy.class);
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
ReactiveClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(anyString()))
.willReturn(Mono.just(TestClientRegistrations.clientRegistration().build()));
given(authorizationRedirectStrategy.sendRedirect(any(), any())).willReturn(Mono.empty());
SecurityWebFilterChain securityFilterChain = this.http.oauth2Login()
.clientRegistrationRepository(clientRegistrationRepository)
.authorizationRedirectStrategy(authorizationRedirectStrategy).and().build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/oauth2/authorization/registration-id").exchange();
verify(authorizationRedirectStrategy).sendRedirect(any(), any());
OAuth2AuthorizationRequestRedirectWebFilter filter = getWebFilter(securityFilterChain,
OAuth2AuthorizationRequestRedirectWebFilter.class).get();
assertThat(ReflectionTestUtils.getField(filter, "authorizationRedirectStrategy"))
.isSameAs(authorizationRedirectStrategy);
}
@Test
public void shouldUseDefaultAuthorizationRedirectStrategyForOAuth2Client() {
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
ReactiveClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(anyString()))
.willReturn(Mono.just(TestClientRegistrations.clientRegistration().build()));
SecurityWebFilterChain securityFilterChain = this.http.oauth2Client()
.clientRegistrationRepository(clientRegistrationRepository).and().build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/oauth2/authorization/registration-id").exchange().expectStatus().is3xxRedirection();
OAuth2AuthorizationRequestRedirectWebFilter filter = getWebFilter(securityFilterChain,
OAuth2AuthorizationRequestRedirectWebFilter.class).get();
assertThat(ReflectionTestUtils.getField(filter, "authorizationRedirectStrategy"))
.isInstanceOf(DefaultServerRedirectStrategy.class);
}
@Test
public void shouldConfigureAuthorizationRedirectStrategyForOAuth2Client() {
ServerRedirectStrategy authorizationRedirectStrategy = mock(ServerRedirectStrategy.class);
ReactiveClientRegistrationRepository clientRegistrationRepository = mock(
ReactiveClientRegistrationRepository.class);
given(clientRegistrationRepository.findByRegistrationId(anyString()))
.willReturn(Mono.just(TestClientRegistrations.clientRegistration().build()));
given(authorizationRedirectStrategy.sendRedirect(any(), any())).willReturn(Mono.empty());
SecurityWebFilterChain securityFilterChain = this.http.oauth2Client()
.clientRegistrationRepository(clientRegistrationRepository)
.authorizationRedirectStrategy(authorizationRedirectStrategy).and().build();
WebTestClient client = WebTestClientBuilder.bindToWebFilters(securityFilterChain).build();
client.get().uri("/oauth2/authorization/registration-id").exchange();
verify(authorizationRedirectStrategy).sendRedirect(any(), any());
OAuth2AuthorizationRequestRedirectWebFilter filter = getWebFilter(securityFilterChain,
OAuth2AuthorizationRequestRedirectWebFilter.class).get();
assertThat(ReflectionTestUtils.getField(filter, "authorizationRedirectStrategy"))
.isSameAs(authorizationRedirectStrategy);
}
private boolean isX509Filter(WebFilter filter) {
try {
Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter");