Add loginPage() to DSL in reactive oauth2Login()
Closes gh-15674
This commit is contained in:
parent
9e5cc5f267
commit
51c226f24c
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue