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.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.reactive.CorsProcessor;
|
import org.springframework.web.cors.reactive.CorsProcessor;
|
||||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||||
|
@ -2958,7 +2959,8 @@ public class ServerHttpSecurity {
|
||||||
if (http.authenticationEntryPoint != null) {
|
if (http.authenticationEntryPoint != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (http.formLogin != null && http.formLogin.isEntryPointExplicit) {
|
if (http.formLogin != null && http.formLogin.isEntryPointExplicit
|
||||||
|
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LoginPageGeneratingWebFilter loginPage = null;
|
LoginPageGeneratingWebFilter loginPage = null;
|
||||||
|
@ -4135,6 +4137,8 @@ public class ServerHttpSecurity {
|
||||||
|
|
||||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||||
|
|
||||||
|
private String loginPage;
|
||||||
|
|
||||||
private OAuth2LoginSpec() {
|
private OAuth2LoginSpec() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4364,6 +4368,19 @@ public class ServerHttpSecurity {
|
||||||
return this.authenticationMatcher;
|
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}
|
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
|
||||||
* @return the {@link ServerHttpSecurity} to continue configuring
|
* @return the {@link ServerHttpSecurity} to continue configuring
|
||||||
|
@ -4410,12 +4427,6 @@ public class ServerHttpSecurity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDefaultEntryPoints(ServerHttpSecurity http) {
|
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(
|
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
|
||||||
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
|
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
|
||||||
MediaType.TEXT_PLAIN);
|
MediaType.TEXT_PLAIN);
|
||||||
|
@ -4429,22 +4440,34 @@ public class ServerHttpSecurity {
|
||||||
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
|
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
|
||||||
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
|
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
|
||||||
htmlMatcher);
|
htmlMatcher);
|
||||||
if (providerLoginPage != null) {
|
String loginPage = "/login";
|
||||||
ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(
|
if (StringUtils.hasText(this.loginPage)) {
|
||||||
defaultLoginPage);
|
loginPage = this.loginPage;
|
||||||
ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico");
|
}
|
||||||
ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
|
else {
|
||||||
new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
|
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,
|
ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher,
|
||||||
new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
|
new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
|
||||||
RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(
|
RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(
|
||||||
providerLoginPage);
|
providerLoginPage);
|
||||||
entryPoint.setRequestCache(http.requestCache.requestCache);
|
entryPoint.setRequestCache(http.requestCache.requestCache);
|
||||||
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
|
http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
|
RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
|
||||||
defaultLoginPage);
|
loginPage);
|
||||||
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
|
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
|
||||||
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
|
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 authorizationRedirectStrategy the redirect strategy for Authorization Endpoint redirect URI.
|
||||||
* @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an
|
* @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an
|
||||||
* authentication request.
|
* authentication request.
|
||||||
|
* @property loginPage the URL to send users to if login is required.
|
||||||
*/
|
*/
|
||||||
@ServerSecurityMarker
|
@ServerSecurityMarker
|
||||||
class ServerOAuth2LoginDsl {
|
class ServerOAuth2LoginDsl {
|
||||||
|
@ -68,6 +69,7 @@ class ServerOAuth2LoginDsl {
|
||||||
var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null
|
var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null
|
||||||
var authorizationRedirectStrategy: ServerRedirectStrategy? = null
|
var authorizationRedirectStrategy: ServerRedirectStrategy? = null
|
||||||
var authenticationMatcher: ServerWebExchangeMatcher? = null
|
var authenticationMatcher: ServerWebExchangeMatcher? = null
|
||||||
|
var loginPage: String? = null
|
||||||
|
|
||||||
internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit {
|
internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit {
|
||||||
return { oauth2Login ->
|
return { oauth2Login ->
|
||||||
|
@ -83,6 +85,7 @@ class ServerOAuth2LoginDsl {
|
||||||
authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) }
|
authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) }
|
||||||
authorizationRedirectStrategy?.also { oauth2Login.authorizationRedirectStrategy(authorizationRedirectStrategy) }
|
authorizationRedirectStrategy?.also { oauth2Login.authorizationRedirectStrategy(authorizationRedirectStrategy) }
|
||||||
authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) }
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||||
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
|
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
|
||||||
|
@ -257,6 +258,65 @@ public class OAuth2LoginTests {
|
||||||
// @formatter:on
|
// @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
|
@Test
|
||||||
public void oauth2AuthorizeWhenCustomObjectsThenUsed() {
|
public void oauth2AuthorizeWhenCustomObjectsThenUsed() {
|
||||||
this.spring
|
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
|
@Configuration
|
||||||
static class OAuth2LoginMockAuthenticationManagerConfig {
|
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
|
@Test
|
||||||
fun `OAuth2 login when authorization request repository configured then custom repository used`() {
|
fun `OAuth2 login when authorization request repository configured then custom repository used`() {
|
||||||
this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
|
this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire()
|
||||||
|
|
Loading…
Reference in New Issue