Skip to content

Commit 51c226f

Browse files
committed
Add loginPage() to DSL in reactive oauth2Login()
Closes gh-15674
1 parent 9e5cc5f commit 51c226f

File tree

4 files changed

+171
-21
lines changed

4 files changed

+171
-21
lines changed

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

+44-21
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
208208
import org.springframework.util.Assert;
209209
import org.springframework.util.ClassUtils;
210+
import org.springframework.util.StringUtils;
210211
import org.springframework.web.cors.reactive.CorsConfigurationSource;
211212
import org.springframework.web.cors.reactive.CorsProcessor;
212213
import org.springframework.web.cors.reactive.CorsWebFilter;
@@ -2958,7 +2959,8 @@ protected void configure(ServerHttpSecurity http) {
29582959
if (http.authenticationEntryPoint != null) {
29592960
return;
29602961
}
2961-
if (http.formLogin != null && http.formLogin.isEntryPointExplicit) {
2962+
if (http.formLogin != null && http.formLogin.isEntryPointExplicit
2963+
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) {
29622964
return;
29632965
}
29642966
LoginPageGeneratingWebFilter loginPage = null;
@@ -4135,6 +4137,8 @@ public final class OAuth2LoginSpec {
41354137

41364138
private ServerAuthenticationFailureHandler authenticationFailureHandler;
41374139

4140+
private String loginPage;
4141+
41384142
private OAuth2LoginSpec() {
41394143
}
41404144

@@ -4364,6 +4368,19 @@ private ServerWebExchangeMatcher getAuthenticationMatcher() {
43644368
return this.authenticationMatcher;
43654369
}
43664370

4371+
/**
4372+
* Specifies the URL to send users to if login is required. A default login page
4373+
* will be generated when this attribute is not specified.
4374+
* @param loginPage the URL to send users to if login is required
4375+
* @return the {@link OAuth2LoginSpec} for further configuration
4376+
* @since 6.4
4377+
*/
4378+
public OAuth2LoginSpec loginPage(String loginPage) {
4379+
Assert.hasText(loginPage, "loginPage cannot be empty");
4380+
this.loginPage = loginPage;
4381+
return this;
4382+
}
4383+
43674384
/**
43684385
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
43694386
* @return the {@link ServerHttpSecurity} to continue configuring
@@ -4410,12 +4427,6 @@ protected void configure(ServerHttpSecurity http) {
44104427
}
44114428

44124429
private void setDefaultEntryPoints(ServerHttpSecurity http) {
4413-
String defaultLoginPage = "/login";
4414-
Map<String, String> urlToText = http.oauth2Login.getLinks();
4415-
String providerLoginPage = null;
4416-
if (urlToText.size() == 1) {
4417-
providerLoginPage = urlToText.keySet().iterator().next();
4418-
}
44194430
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
44204431
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
44214432
MediaType.TEXT_PLAIN);
@@ -4429,22 +4440,34 @@ MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTM
44294440
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
44304441
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
44314442
htmlMatcher);
4432-
if (providerLoginPage != null) {
4433-
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(
4434-
defaultLoginPage);
4435-
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
4436-
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
4437-
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
4438-
4439-
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher,
4440-
new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
4441-
RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(
4442-
providerLoginPage);
4443-
entryPoint.setRequestCache(http.requestCache.requestCache);
4444-
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
4443+
String loginPage = "/login";
4444+
if (StringUtils.hasText(this.loginPage)) {
4445+
loginPage = this.loginPage;
4446+
}
4447+
else {
4448+
Map<String, String> urlToText = http.oauth2Login.getLinks();
4449+
String providerLoginPage = null;
4450+
if (urlToText.size() == 1) {
4451+
providerLoginPage = urlToText.keySet().iterator().next();
4452+
}
4453+
if (providerLoginPage != null) {
4454+
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(
4455+
loginPage);
4456+
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher(
4457+
"/favicon.ico");
4458+
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
4459+
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
4460+
4461+
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher,
4462+
new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
4463+
RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(
4464+
providerLoginPage);
4465+
entryPoint.setRequestCache(http.requestCache.requestCache);
4466+
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
4467+
}
44454468
}
44464469
RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
4447-
defaultLoginPage);
4470+
loginPage);
44484471
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
44494472
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
44504473
}

config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import org.springframework.web.server.ServerWebExchange
5353
* @property authorizationRedirectStrategy the redirect strategy for Authorization Endpoint redirect URI.
5454
* @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an
5555
* authentication request.
56+
* @property loginPage the URL to send users to if login is required.
5657
*/
5758
@ServerSecurityMarker
5859
class ServerOAuth2LoginDsl {
@@ -68,6 +69,7 @@ class ServerOAuth2LoginDsl {
6869
var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null
6970
var authorizationRedirectStrategy: ServerRedirectStrategy? = null
7071
var authenticationMatcher: ServerWebExchangeMatcher? = null
72+
var loginPage: String? = null
7173

7274
internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit {
7375
return { oauth2Login ->
@@ -83,6 +85,7 @@ class ServerOAuth2LoginDsl {
8385
authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) }
8486
authorizationRedirectStrategy?.also { oauth2Login.authorizationRedirectStrategy(authorizationRedirectStrategy) }
8587
authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) }
88+
loginPage?.also { oauth2Login.loginPage(loginPage) }
8689
}
8790
}
8891
}

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

+100
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.context.annotation.Bean;
3232
import org.springframework.context.annotation.Configuration;
3333
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpMethod;
3435
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3536
import org.springframework.security.authentication.TestingAuthenticationToken;
3637
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
@@ -257,6 +258,65 @@ public void defaultLoginPageWithOAuth2LoginHttpBasicAndXhrRequestThenUnauthorize
257258
// @formatter:on
258259
}
259260

261+
@Test
262+
public void defaultLoginPageWhenCustomLoginPageThenGeneratedLoginPageDoesNotExist() {
263+
this.spring
264+
.register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class,
265+
WebFluxConfig.class)
266+
.autowire();
267+
// @formatter:off
268+
this.client.get()
269+
.uri("/login")
270+
.exchange()
271+
.expectStatus().isNotFound();
272+
// @formatter:on
273+
}
274+
275+
@Test
276+
public void oauth2LoginWhenCustomLoginPageAndSingleClientRegistrationThenRedirectsToLoginPage() {
277+
this.spring
278+
.register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class,
279+
WebFluxConfig.class)
280+
.autowire();
281+
// @formatter:off
282+
this.client.get()
283+
.uri("/")
284+
.exchange()
285+
.expectStatus().is3xxRedirection()
286+
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
287+
// @formatter:on
288+
}
289+
290+
@Test
291+
public void oauth2LoginWhenCustomLoginPageAndMultipleClientRegistrationsThenRedirectsToLoginPage() {
292+
this.spring
293+
.register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class,
294+
WebFluxConfig.class)
295+
.autowire();
296+
// @formatter:off
297+
this.client.get()
298+
.uri("/")
299+
.exchange()
300+
.expectStatus().is3xxRedirection()
301+
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/login");
302+
// @formatter:on
303+
}
304+
305+
@Test
306+
public void oauth2LoginWhenProviderLoginPageAndMultipleClientRegistrationsThenRedirectsToProvider() {
307+
this.spring
308+
.register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithProviderLoginPage.class,
309+
WebFluxConfig.class)
310+
.autowire();
311+
// @formatter:off
312+
this.client.get()
313+
.uri("/")
314+
.exchange()
315+
.expectStatus().is3xxRedirection()
316+
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/oauth2/authorization/github");
317+
// @formatter:on
318+
}
319+
260320
@Test
261321
public void oauth2AuthorizeWhenCustomObjectsThenUsed() {
262322
this.spring
@@ -756,6 +816,46 @@ SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
756816

757817
}
758818

819+
@Configuration
820+
@EnableWebFluxSecurity
821+
static class OAuth2LoginWithCustomLoginPage {
822+
823+
@Bean
824+
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
825+
// @formatter:off
826+
http
827+
.authorizeExchange((authorize) -> authorize
828+
.pathMatchers(HttpMethod.GET, "/login").permitAll()
829+
.anyExchange().authenticated()
830+
)
831+
.oauth2Login((oauth2) -> oauth2
832+
.loginPage("/login")
833+
);
834+
// @formatter:on
835+
return http.build();
836+
}
837+
838+
}
839+
840+
@Configuration
841+
@EnableWebFluxSecurity
842+
static class OAuth2LoginWithProviderLoginPage {
843+
844+
@Bean
845+
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
846+
// @formatter:off
847+
http.authorizeExchange((authorize) -> authorize
848+
.anyExchange().authenticated()
849+
)
850+
.oauth2Login((oauth2) -> oauth2
851+
.loginPage("/oauth2/authorization/github")
852+
);
853+
// @formatter:on
854+
return http.build();
855+
}
856+
857+
}
858+
759859
@Configuration
760860
static class OAuth2LoginMockAuthenticationManagerConfig {
761861

config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt

+24
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,30 @@ class ServerOAuth2LoginDslTests {
113113
}
114114
}
115115

116+
@Test
117+
fun `login page when OAuth2 login configured with login page then default login page does not exist`() {
118+
this.spring.register(OAuth2LoginConfigWithLoginPage::class.java, ClientConfig::class.java).autowire()
119+
120+
this.client.get()
121+
.uri("/login")
122+
.exchange()
123+
.expectStatus().isNotFound
124+
}
125+
126+
@Configuration
127+
@EnableWebFluxSecurity
128+
@EnableWebFlux
129+
open class OAuth2LoginConfigWithLoginPage {
130+
@Bean
131+
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
132+
return http {
133+
oauth2Login {
134+
loginPage = "/login"
135+
}
136+
}
137+
}
138+
}
139+
116140
@Test
117141
fun `OAuth2 login when authorization request repository configured then custom repository used`() {
118142
this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()

0 commit comments

Comments
 (0)