Skip to content

Commit a24d673

Browse files
committed
oauth2Login WebFlux does not auto-redirect for XHR request
Fixes gh-8118
1 parent 3a46ba8 commit a24d673

File tree

2 files changed

+76
-10
lines changed

2 files changed

+76
-10
lines changed

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

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -621,18 +621,54 @@ protected void configure(ServerHttpSecurity http) {
621621
authenticationFilter.setAuthenticationFailureHandler(new RedirectServerAuthenticationFailureHandler("/login?error"));
622622
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
623623

624-
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
625-
MediaType.TEXT_HTML);
626-
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
624+
setDefaultEntryPoints(http);
625+
626+
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
627+
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
628+
}
629+
630+
private void setDefaultEntryPoints(ServerHttpSecurity http) {
631+
String defaultLoginPage = "/login";
627632
Map<String, String> urlToText = http.oauth2Login.getLinks();
633+
String providerLoginPage = null;
628634
if (urlToText.size() == 1) {
629-
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint(urlToText.keySet().iterator().next())));
630-
} else {
631-
http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, new RedirectServerAuthenticationEntryPoint("/login")));
635+
providerLoginPage = urlToText.keySet().iterator().next();
632636
}
633637

634-
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
635-
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
638+
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
639+
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"),
640+
MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
641+
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
642+
643+
ServerWebExchangeMatcher xhrMatcher = exchange -> {
644+
if (exchange.getRequest().getHeaders().getOrDefault("X-Requested-With", Collections.emptyList()).contains("XMLHttpRequest")) {
645+
return ServerWebExchangeMatcher.MatchResult.match();
646+
}
647+
return ServerWebExchangeMatcher.MatchResult.notMatch();
648+
};
649+
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
650+
651+
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(
652+
notXhrMatcher, htmlMatcher);
653+
654+
if (providerLoginPage != null) {
655+
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage);
656+
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
657+
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
658+
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
659+
660+
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
661+
notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
662+
RedirectServerAuthenticationEntryPoint entryPoint =
663+
new RedirectServerAuthenticationEntryPoint(providerLoginPage);
664+
entryPoint.setRequestCache(http.requestCache.requestCache);
665+
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
666+
}
667+
668+
RedirectServerAuthenticationEntryPoint defaultEntryPoint =
669+
new RedirectServerAuthenticationEntryPoint(defaultLoginPage);
670+
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
671+
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
636672
}
637673

638674
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,8 +26,10 @@
2626
import org.junit.Test;
2727
import org.openqa.selenium.WebDriver;
2828
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.ApplicationContext;
2930
import org.springframework.context.annotation.Bean;
3031
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.http.HttpHeaders;
3133
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3234
import org.springframework.security.authentication.TestingAuthenticationToken;
3335
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -61,10 +63,12 @@
6163
import org.springframework.security.web.server.WebFilterChainProxy;
6264
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
6365
import org.springframework.test.web.reactive.server.WebTestClient;
66+
import org.springframework.web.reactive.config.EnableWebFlux;
6467
import org.springframework.web.server.ServerWebExchange;
6568
import org.springframework.web.server.WebFilter;
6669
import org.springframework.web.server.WebFilterChain;
6770

71+
import org.springframework.web.server.WebHandler;
6872
import reactor.core.publisher.Mono;
6973

7074
import java.time.Duration;
@@ -79,6 +83,8 @@ public class OAuth2LoginTests {
7983
@Rule
8084
public final SpringTestRule spring = new SpringTestRule();
8185

86+
private WebTestClient client;
87+
8288
@Autowired
8389
private WebFilterChainProxy springSecurity;
8490

@@ -94,6 +100,14 @@ public class OAuth2LoginTests {
94100
.clientSecret("secret")
95101
.build();
96102

103+
@Autowired
104+
public void setApplicationContext(ApplicationContext context) {
105+
if (context.getBeanNamesForType(WebHandler.class).length > 0) {
106+
this.client = WebTestClient.bindToApplicationContext(context)
107+
.build();
108+
}
109+
}
110+
97111
@Test
98112
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
99113
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
@@ -140,6 +154,22 @@ public void defaultLoginPageWithSingleClientRegistrationThenRedirect() {
140154
assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize");
141155
}
142156

157+
// gh-8118
158+
@Test
159+
public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() {
160+
this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire();
161+
162+
this.client.get()
163+
.uri("/")
164+
.header("X-Requested-With", "XMLHttpRequest")
165+
.exchange()
166+
.expectStatus().is3xxRedirection()
167+
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
168+
}
169+
170+
@EnableWebFlux
171+
static class WebFluxConfig { }
172+
143173
@EnableWebFluxSecurity
144174
static class OAuth2LoginWithSingleClientRegistrations {
145175
@Bean

0 commit comments

Comments
 (0)