From 51c226f24cd4d8550c4918b46603cb3c3129f4c2 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:53:35 -0500 Subject: [PATCH] Add loginPage() to DSL in reactive oauth2Login() Closes gh-15674 --- .../config/web/server/ServerHttpSecurity.java | 63 +++++++---- .../config/web/server/ServerOAuth2LoginDsl.kt | 3 + .../config/web/server/OAuth2LoginTests.java | 100 ++++++++++++++++++ .../web/server/ServerOAuth2LoginDslTests.kt | 24 +++++ 4 files changed, 170 insertions(+), 20 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index e2bb5ebb7f..ebec42a6ef 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -207,6 +207,7 @@ import org.springframework.security.web.server.util.matcher.ServerWebExchangeMat import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.CorsProcessor; import org.springframework.web.cors.reactive.CorsWebFilter; @@ -2958,7 +2959,8 @@ public class ServerHttpSecurity { if (http.authenticationEntryPoint != null) { return; } - if (http.formLogin != null && http.formLogin.isEntryPointExplicit) { + if (http.formLogin != null && http.formLogin.isEntryPointExplicit + || http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) { return; } LoginPageGeneratingWebFilter loginPage = null; @@ -4135,6 +4137,8 @@ public class ServerHttpSecurity { private ServerAuthenticationFailureHandler authenticationFailureHandler; + private String loginPage; + private OAuth2LoginSpec() { } @@ -4364,6 +4368,19 @@ public class ServerHttpSecurity { return this.authenticationMatcher; } + /** + * Specifies the URL to send users to if login is required. A default login page + * will be generated when this attribute is not specified. + * @param loginPage the URL to send users to if login is required + * @return the {@link OAuth2LoginSpec} for further configuration + * @since 6.4 + */ + public OAuth2LoginSpec loginPage(String loginPage) { + Assert.hasText(loginPage, "loginPage cannot be empty"); + this.loginPage = loginPage; + return this; + } + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -4410,12 +4427,6 @@ public class ServerHttpSecurity { } private void setDefaultEntryPoints(ServerHttpSecurity http) { - String defaultLoginPage = "/login"; - Map urlToText = http.oauth2Login.getLinks(); - String providerLoginPage = null; - if (urlToText.size() == 1) { - providerLoginPage = urlToText.keySet().iterator().next(); - } MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); @@ -4429,22 +4440,34 @@ public class ServerHttpSecurity { ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher); ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher, htmlMatcher); - if (providerLoginPage != null) { - ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher( - defaultLoginPage); - ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico"); - ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher( - new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + String loginPage = "/login"; + if (StringUtils.hasText(this.loginPage)) { + loginPage = this.loginPage; + } + else { + Map urlToText = http.oauth2Login.getLinks(); + String providerLoginPage = null; + if (urlToText.size() == 1) { + providerLoginPage = urlToText.keySet().iterator().next(); + } + if (providerLoginPage != null) { + ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher( + loginPage); + ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher( + "/favicon.ico"); + ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher( + new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); - ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher, - new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher)); - RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint( - providerLoginPage); - entryPoint.setRequestCache(http.requestCache.requestCache); - http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint)); + ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher, + new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher)); + RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint( + providerLoginPage); + entryPoint.setRequestCache(http.requestCache.requestCache); + http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint)); + } } RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint( - defaultLoginPage); + loginPage); defaultEntryPoint.setRequestCache(http.requestCache.requestCache); http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint)); } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt index 4ab8fcb0e4..0aa91e48d5 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt @@ -53,6 +53,7 @@ import org.springframework.web.server.ServerWebExchange * @property authorizationRedirectStrategy the redirect strategy for Authorization Endpoint redirect URI. * @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an * authentication request. + * @property loginPage the URL to send users to if login is required. */ @ServerSecurityMarker class ServerOAuth2LoginDsl { @@ -68,6 +69,7 @@ class ServerOAuth2LoginDsl { var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null var authorizationRedirectStrategy: ServerRedirectStrategy? = null var authenticationMatcher: ServerWebExchangeMatcher? = null + var loginPage: String? = null internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit { return { oauth2Login -> @@ -83,6 +85,7 @@ class ServerOAuth2LoginDsl { authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) } authorizationRedirectStrategy?.also { oauth2Login.authorizationRedirectStrategy(authorizationRedirectStrategy) } authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) } + loginPage?.also { oauth2Login.loginPage(loginPage) } } } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index c21e42a3ed..67d7a816f0 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -31,6 +31,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; @@ -257,6 +258,65 @@ public class OAuth2LoginTests { // @formatter:on } + @Test + public void defaultLoginPageWhenCustomLoginPageThenGeneratedLoginPageDoesNotExist() { + this.spring + .register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/login") + .exchange() + .expectStatus().isNotFound(); + // @formatter:on + } + + @Test + public void oauth2LoginWhenCustomLoginPageAndSingleClientRegistrationThenRedirectsToLoginPage() { + this.spring + .register(OAuth2LoginWithSingleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/login"); + // @formatter:on + } + + @Test + public void oauth2LoginWhenCustomLoginPageAndMultipleClientRegistrationsThenRedirectsToLoginPage() { + this.spring + .register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithCustomLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/login"); + // @formatter:on + } + + @Test + public void oauth2LoginWhenProviderLoginPageAndMultipleClientRegistrationsThenRedirectsToProvider() { + this.spring + .register(OAuth2LoginWithMultipleClientRegistrations.class, OAuth2LoginWithProviderLoginPage.class, + WebFluxConfig.class) + .autowire(); + // @formatter:off + this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/oauth2/authorization/github"); + // @formatter:on + } + @Test public void oauth2AuthorizeWhenCustomObjectsThenUsed() { this.spring @@ -756,6 +816,46 @@ public class OAuth2LoginTests { } + @Configuration + @EnableWebFluxSecurity + static class OAuth2LoginWithCustomLoginPage { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize + .pathMatchers(HttpMethod.GET, "/login").permitAll() + .anyExchange().authenticated() + ) + .oauth2Login((oauth2) -> oauth2 + .loginPage("/login") + ); + // @formatter:on + return http.build(); + } + + } + + @Configuration + @EnableWebFluxSecurity + static class OAuth2LoginWithProviderLoginPage { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http.authorizeExchange((authorize) -> authorize + .anyExchange().authenticated() + ) + .oauth2Login((oauth2) -> oauth2 + .loginPage("/oauth2/authorization/github") + ); + // @formatter:on + return http.build(); + } + + } + @Configuration static class OAuth2LoginMockAuthenticationManagerConfig { diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt index c69bdf7a42..5f335d7490 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt @@ -113,6 +113,30 @@ class ServerOAuth2LoginDslTests { } } + @Test + fun `login page when OAuth2 login configured with login page then default login page does not exist`() { + this.spring.register(OAuth2LoginConfigWithLoginPage::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login") + .exchange() + .expectStatus().isNotFound + } + + @Configuration + @EnableWebFluxSecurity + @EnableWebFlux + open class OAuth2LoginConfigWithLoginPage { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + loginPage = "/login" + } + } + } + } + @Test fun `OAuth2 login when authorization request repository configured then custom repository used`() { this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()