Add loginPage() to DSL in reactive oauth2Login()

Closes gh-15674
This commit is contained in:
Steve Riesenberg 2024-09-11 15:53:35 -05:00
parent 9e5cc5f267
commit 51c226f24c
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9
4 changed files with 170 additions and 20 deletions

View File

@ -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);
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));
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));
}

View File

@ -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) }
}
}
}

View File

@ -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 {

View File

@ -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()