Forráskód Böngészése

Add loginPage() to DSL in reactive oauth2Login()

Closes gh-15674
Steve Riesenberg 11 hónapja
szülő
commit
51c226f24c

+ 44 - 21
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<String, String> 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);
-
-				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));
+			String loginPage = "/login";
+			if (StringUtils.hasText(this.loginPage)) {
+				loginPage = this.loginPage;
+			}
+			else {
+				Map<String, String> 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));
+				}
 			}
 			RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
-					defaultLoginPage);
+					loginPage);
 			defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
 			http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
 		}

+ 3 - 0
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) }
         }
     }
 }

+ 100 - 0
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 {
 

+ 24 - 0
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()