Skip to content

Commit c40a17b

Browse files
committed
WebFlux oauth2Login() redirects on failed authentication
Fixes gh-5562 gh-6484
1 parent d102cae commit c40a17b

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ public class OAuth2LoginSpec {
986986

987987
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();
988988

989-
private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception);
989+
private ServerAuthenticationFailureHandler authenticationFailureHandler;
990990

991991
/**
992992
* Configures the {@link ReactiveAuthenticationManager} to use. The default is
@@ -1028,6 +1028,7 @@ public OAuth2LoginSpec authenticationSuccessHandler(ServerAuthenticationSuccessH
10281028

10291029
/**
10301030
* The {@link ServerAuthenticationFailureHandler} used after authentication failure.
1031+
* Defaults to {@link RedirectServerAuthenticationFailureHandler} redirecting to "/login?error".
10311032
*
10321033
* @since 5.2
10331034
* @param authenticationFailureHandler the failure handler to use
@@ -1175,7 +1176,7 @@ protected void configure(ServerHttpSecurity http) {
11751176
authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
11761177

11771178
authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
1178-
authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
1179+
authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
11791180
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
11801181

11811182
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
@@ -1192,6 +1193,13 @@ protected void configure(ServerHttpSecurity http) {
11921193
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
11931194
}
11941195

1196+
private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
1197+
if (this.authenticationFailureHandler == null) {
1198+
this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
1199+
}
1200+
return this.authenticationFailureHandler;
1201+
}
1202+
11951203
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
11961204
return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
11971205
}

config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.springframework.security.oauth2.core.user.OAuth2User;
7171
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
7272
import org.springframework.security.oauth2.jwt.Jwt;
73+
import org.springframework.security.oauth2.jwt.JwtValidationException;
7374
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
7475
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
7576
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -518,6 +519,85 @@ public void oauth2LoginWhenCustomBeansThenUsed() {
518519
verify(securityContextRepository).save(any(), any());
519520
}
520521

522+
// gh-5562
523+
@Test
524+
public void oauth2LoginWhenAccessTokenRequestFailsThenDefaultRedirectToLogin() {
525+
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
526+
OAuth2LoginWithCustomBeansConfig.class).autowire();
527+
528+
WebTestClient webTestClient = WebTestClientBuilder
529+
.bindToWebFilters(this.springSecurity)
530+
.build();
531+
532+
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
533+
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
534+
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
535+
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
536+
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
537+
538+
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
539+
540+
ServerAuthenticationConverter converter = config.authenticationConverter;
541+
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
542+
543+
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
544+
OAuth2Error oauth2Error = new OAuth2Error("invalid_request", "Invalid request", null);
545+
when(tokenResponseClient.getTokenResponse(any())).thenThrow(new OAuth2AuthenticationException(oauth2Error));
546+
547+
webTestClient.get()
548+
.uri("/login/oauth2/code/google")
549+
.exchange()
550+
.expectStatus()
551+
.is3xxRedirection()
552+
.expectHeader()
553+
.valueEquals("Location", "/login?error");
554+
}
555+
556+
// gh-6484
557+
@Test
558+
public void oauth2LoginWhenIdTokenValidationFailsThenDefaultRedirectToLogin() {
559+
this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class,
560+
OAuth2LoginWithCustomBeansConfig.class).autowire();
561+
562+
WebTestClient webTestClient = WebTestClientBuilder
563+
.bindToWebFilters(this.springSecurity)
564+
.build();
565+
566+
OAuth2LoginWithCustomBeansConfig config = this.spring.getContext().getBean(OAuth2LoginWithCustomBeansConfig.class);
567+
568+
OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().scope("openid").build();
569+
OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build();
570+
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(request, response);
571+
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
572+
OAuth2AuthorizationCodeAuthenticationToken authenticationToken = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
573+
574+
ServerAuthenticationConverter converter = config.authenticationConverter;
575+
when(converter.convert(any())).thenReturn(Mono.just(authenticationToken));
576+
577+
Map<String, Object> additionalParameters = new HashMap<>();
578+
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
579+
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
580+
.tokenType(accessToken.getTokenType())
581+
.scopes(accessToken.getScopes())
582+
.additionalParameters(additionalParameters)
583+
.build();
584+
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
585+
when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
586+
587+
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = config.jwtDecoderFactory;
588+
OAuth2Error oauth2Error = new OAuth2Error("invalid_id_token", "Invalid ID Token", null);
589+
when(jwtDecoderFactory.createDecoder(any())).thenReturn(token ->
590+
Mono.error(new JwtValidationException("ID Token validation failed", Collections.singleton(oauth2Error))));
591+
592+
webTestClient.get()
593+
.uri("/login/oauth2/code/google")
594+
.exchange()
595+
.expectStatus()
596+
.is3xxRedirection()
597+
.expectHeader()
598+
.valueEquals("Location", "/login?error");
599+
}
600+
521601
@Configuration
522602
static class OAuth2LoginWithCustomBeansConfig {
523603

0 commit comments

Comments
 (0)