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 0f6688659b..d708855cac 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 @@ -54,7 +54,6 @@ import org.springframework.security.authorization.AuthorityReactiveAuthorization import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; @@ -103,7 +102,6 @@ import org.springframework.security.web.server.DelegatingServerAuthenticationEnt import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; -import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; @@ -233,6 +231,7 @@ import static org.springframework.security.web.server.util.matcher.ServerWebExch * @author Rob Winch * @author Vedran Pavic * @author Rafiullah Hamedy + * @author Eddú Meléndez * @since 5.0 */ public class ServerHttpSecurity { @@ -981,6 +980,8 @@ public class ServerHttpSecurity { private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler(); + private ServerAuthenticationFailureHandler authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception); + /** * Configures the {@link ReactiveAuthenticationManager} to use. The default is * {@link OAuth2AuthorizationCodeReactiveAuthenticationManager} @@ -1006,6 +1007,19 @@ public class ServerHttpSecurity { return this; } + /** + * The {@link ServerAuthenticationFailureHandler} used after authentication failure. + * + * @since 5.2 + * @param authenticationFailureHandler the failure handler to use + * @return the {@link OAuth2LoginSpec} to customize + */ + public OAuth2LoginSpec authenticationFailureHandler(ServerAuthenticationFailureHandler authenticationFailureHandler) { + Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); + this.authenticationFailureHandler = authenticationFailureHandler; + return this; + } + /** * Gets the {@link ReactiveAuthenticationManager} to use. First tries an explicitly configured manager, and * defaults to {@link OAuth2AuthorizationCodeReactiveAuthenticationManager} @@ -1123,13 +1137,7 @@ public class ServerHttpSecurity { authenticationFilter.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository)); authenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler); - authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationFailureHandler() { - @Override - public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, - AuthenticationException exception) { - return Mono.error(exception); - } - }); + authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler); authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( 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 bc3bc5a45d..147a1ea1e2 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 @@ -25,8 +25,14 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.stubbing.Answer; import org.openqa.selenium.WebDriver; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import reactor.core.publisher.Mono; @@ -97,6 +103,7 @@ import static org.mockito.Mockito.when; /** * @author Rob Winch + * @author Eddú Meléndez * @since 5.1 */ public class OAuth2LoginTests { @@ -233,6 +240,59 @@ public class OAuth2LoginTests { verify(successHandler).onAuthenticationSuccess(any(), any()); } + @Test + public void oauth2LoginFailsWhenCustomObjectsThenUsed() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, + OAuth2LoginMockAuthenticationManagerConfig.class).autowire(); + + String redirectLocation = "/custom-redirect-location"; + String failureRedirectLocation = "/failure-redirect-location"; + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(this.springSecurity) + .build(); + + OAuth2LoginMockAuthenticationManagerConfig config = this.spring.getContext() + .getBean(OAuth2LoginMockAuthenticationManagerConfig.class); + ServerAuthenticationConverter converter = config.authenticationConverter; + ReactiveAuthenticationManager manager = config.manager; + ServerWebExchangeMatcher matcher = config.matcher; + ServerOAuth2AuthorizationRequestResolver resolver = config.resolver; + ServerAuthenticationSuccessHandler successHandler = config.successHandler; + ServerAuthenticationFailureHandler failureHandler = config.failureHandler; + + when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); + when(manager.authenticate(any())).thenReturn(Mono.error(new OAuth2AuthenticationException(new OAuth2Error("error"), "message"))); + when(matcher.matches(any())).thenReturn(ServerWebExchangeMatcher.MatchResult.match()); + when(resolver.resolve(any())).thenReturn(Mono.empty()); + when(successHandler.onAuthenticationSuccess(any(), any())).thenAnswer((Answer>) invocation -> { + WebFilterExchange webFilterExchange = invocation.getArgument(0); + Authentication authentication = invocation.getArgument(1); + + return new RedirectServerAuthenticationSuccessHandler(redirectLocation) + .onAuthenticationSuccess(webFilterExchange, authentication); + }); + when(failureHandler.onAuthenticationFailure(any(), any())).thenAnswer((Answer>) invocation -> { + WebFilterExchange webFilterExchange = invocation.getArgument(0); + AuthenticationException authenticationException = invocation.getArgument(1); + + return new RedirectServerAuthenticationFailureHandler(failureRedirectLocation) + .onAuthenticationFailure(webFilterExchange, authenticationException); + }); + + webTestClient.get() + .uri("/login/oauth2/code/github") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals("Location", failureRedirectLocation); + + verify(converter).convert(any()); + verify(manager).authenticate(any()); + verify(matcher).matches(any()); + verify(resolver).resolve(any()); + verify(failureHandler).onAuthenticationFailure(any(), any()); + } + @Configuration static class OAuth2LoginMockAuthenticationManagerConfig { ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class); @@ -245,6 +305,8 @@ public class OAuth2LoginTests { ServerAuthenticationSuccessHandler successHandler = mock(ServerAuthenticationSuccessHandler.class); + ServerAuthenticationFailureHandler failureHandler = mock(ServerAuthenticationFailureHandler.class); + @Bean public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { http @@ -256,7 +318,8 @@ public class OAuth2LoginTests { .authenticationManager(manager) .authenticationMatcher(matcher) .authorizationRequestResolver(resolver) - .authenticationSuccessHandler(successHandler); + .authenticationSuccessHandler(successHandler) + .authenticationFailureHandler(failureHandler); return http.build(); } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/TestOAuth2AuthorizationExchanges.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/TestOAuth2AuthorizationExchanges.java index c353afd9d7..98fdbec1dc 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/TestOAuth2AuthorizationExchanges.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/TestOAuth2AuthorizationExchanges.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.core.endpoint; /** * @author Rob Winch + * @author Eddú Meléndez * @since 5.1 */ public class TestOAuth2AuthorizationExchanges { @@ -27,4 +28,10 @@ public class TestOAuth2AuthorizationExchanges { OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.success().build(); return new OAuth2AuthorizationExchange(request, response); } + + public static OAuth2AuthorizationExchange failure() { + OAuth2AuthorizationRequest request = TestOAuth2AuthorizationRequests.request().build(); + OAuth2AuthorizationResponse response = TestOAuth2AuthorizationResponses.error().build(); + return new OAuth2AuthorizationExchange(request, response); + } }