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 6dff6f207e7..96eb1a8ad12 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 @@ -297,6 +297,7 @@ * @author Ankur Pathak * @author Alexey Nesterov * @author Yanming Zhou + * @author Iain Henderson * @since 5.0 */ public class ServerHttpSecurity { @@ -4138,6 +4139,8 @@ public class OAuth2ResourceServerSpec { private ServerAuthenticationFailureHandler authenticationFailureHandler; + private ServerAuthenticationSuccessHandler authenticationSuccessHandler; + private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler(); private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter(); @@ -4186,6 +4189,20 @@ public OAuth2ResourceServerSpec authenticationFailureHandler( return this; } + /** + * Configures the {@link ServerAuthenticationSuccessHandler} to use. The default + * is {@link WebFilterChainServerAuthenticationSuccessHandler} + * @param authenticationSuccessHandler the + * {@link ServerAuthenticationSuccessHandler} to use + * @return the {@link OAuth2ClientSpec} to customize + * @since 7.1 + */ + public OAuth2ResourceServerSpec authenticationSuccessHandler( + ServerAuthenticationSuccessHandler authenticationSuccessHandler) { + this.authenticationSuccessHandler = authenticationSuccessHandler; + return this; + } + /** * Configures the {@link ServerAuthenticationConverter} to use for requests * authenticating with @@ -4254,6 +4271,7 @@ protected void configure(ServerHttpSecurity http) { AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver); oauth2.setServerAuthenticationConverter(this.bearerTokenConverter); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); + oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler()); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); } else if (this.jwt != null) { @@ -4313,6 +4331,13 @@ private ServerAuthenticationFailureHandler authenticationFailureHandler() { return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint); } + private ServerAuthenticationSuccessHandler authenticationSuccessHandler() { + if (this.authenticationSuccessHandler != null) { + return this.authenticationSuccessHandler; + } + return new WebFilterChainServerAuthenticationSuccessHandler(); + } + /** * Configures JWT Resource Server Support */ @@ -4387,6 +4412,7 @@ protected void configure(ServerHttpSecurity http) { AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); + oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler()); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); } @@ -4519,6 +4545,7 @@ protected void configure(ServerHttpSecurity http) { AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager); oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter); oauth2.setAuthenticationFailureHandler(authenticationFailureHandler()); + oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler()); http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index 40ed262a402..27bb7093a71 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -73,9 +73,11 @@ import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; @@ -371,6 +373,78 @@ public void getWhenUsingCustomAuthenticationFailureHandlerThenUsesIsAccordingly( verify(handler).onAuthenticationFailure(any(), any()); } + @Test + public void getWhenUsingCustomAuthenticationSuccessHandlerThenUsesIsAccordingly() { + this.spring.register(CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig.class).autowire(); + ServerAuthenticationSuccessHandler handler = this.spring.getContext() + .getBean(ServerAuthenticationSuccessHandler.class); + ReactiveAuthenticationManager authenticationManager = this.spring.getContext() + .getBean(ReactiveAuthenticationManager.class); + given(authenticationManager.authenticate(any())) + .willAnswer(input -> Mono.just(input.getArgument(0, Authentication.class))); + given(handler.onAuthenticationSuccess(any(), any())).willAnswer(input -> { + WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class); + return webFilterExchange.getChain().filter(webFilterExchange.getExchange()); + }); + // @formatter:off + this.client.get() + .headers((headers) -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + verify(handler).onAuthenticationSuccess(any(), any()); + } + + @Test + public void getWhenUsingCustomAuthenticationSuccessHandlerWithJwtThenUsesIsAccordingly() { + this.spring.register(CustomAuthenticationSuccessHandlerJwtConfig.class).autowire(); + ServerAuthenticationSuccessHandler handler = this.spring.getContext() + .getBean(ServerAuthenticationSuccessHandler.class); + ReactiveAuthenticationManager authenticationManager = this.spring.getContext() + .getBean(ReactiveAuthenticationManager.class); + given(authenticationManager.authenticate(any())) + .willAnswer(input -> Mono.just(input.getArgument(0, Authentication.class))); + given(handler.onAuthenticationSuccess(any(), any())).willAnswer(input -> { + WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class); + return webFilterExchange.getChain().filter(webFilterExchange.getExchange()); + }); + // @formatter:off + this.client.get() + .headers((headers) -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + verify(handler).onAuthenticationSuccess(any(), any()); + } + + @Test + public void getWhenUsingCustomAuthenticationSuccessHandlerWIthOpaqueTokenThenUsesIsAccordingly() { + this.spring.register(CustomAuthenticationSuccessHandlerOpaqueTokenConfig.class, RootController.class).autowire(); + this.spring.getContext() + .getBean(MockWebServer.class) + .setDispatcher(requiresAuth(this.clientId, this.clientSecret, this.active)); + ServerAuthenticationSuccessHandler handler = this.spring.getContext() + .getBean(ServerAuthenticationSuccessHandler.class); + ReactiveAuthenticationManager authenticationManager = this.spring.getContext() + .getBean(ReactiveAuthenticationManager.class); + given(authenticationManager.authenticate(any())) + .willAnswer(input -> Mono.just(input.getArgument(0, Authentication.class))); + given(handler.onAuthenticationSuccess(any(), any())).willAnswer(input -> { + WebFilterExchange webFilterExchange = input.getArgument(0, WebFilterExchange.class); + return webFilterExchange.getChain().filter(webFilterExchange.getExchange()); + }); + // @formatter:off + this.client.get() + .headers((headers) -> headers + .setBearerAuth(this.messageReadToken) + ) + .exchange() + .expectStatus().isOk(); + // @formatter:on + + verify(handler).onAuthenticationSuccess(any(), any()); + } + @Test public void postWhenSignedThenReturnsOk() { this.spring.register(PublicKeyConfig.class, RootController.class).autowire(); @@ -950,6 +1024,110 @@ ServerAuthenticationFailureHandler authenticationFailureHandler() { } + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class CustomAuthenticationSuccessHandlerAuthenticationManagerResolverConfig { + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2ResourceServer((oauth2) -> oauth2 + .authenticationSuccessHandler(authenticationSuccessHandler()) + .authenticationManagerResolver(exchange -> Mono.just(authenticationManager())) + ); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveAuthenticationManager authenticationManager() { + return mock(ReactiveAuthenticationManager.class); + } + + @Bean + ServerAuthenticationSuccessHandler authenticationSuccessHandler() { + return mock(ServerAuthenticationSuccessHandler.class); + } + + } + + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class CustomAuthenticationSuccessHandlerJwtConfig { + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2ResourceServer((oauth2) -> oauth2 + .authenticationSuccessHandler(authenticationSuccessHandler()) + .jwt((jwt) -> jwt.authenticationManager(authenticationManager())) + ); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveAuthenticationManager authenticationManager() { + return mock(ReactiveAuthenticationManager.class); + } + + @Bean + ServerAuthenticationSuccessHandler authenticationSuccessHandler() { + return mock(ServerAuthenticationSuccessHandler.class); + } + + } + + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class CustomAuthenticationSuccessHandlerOpaqueTokenConfig { + + private MockWebServer mockWebServer = new MockWebServer(); + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + String introspectionUri = mockWebServer().url("/introspect").toString(); + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2ResourceServer((oauth2) -> oauth2 + .authenticationSuccessHandler(authenticationSuccessHandler()) + .opaqueToken((opaqueToken) -> opaqueToken + .introspectionUri(introspectionUri) + .introspectionClientCredentials("client", "secret")) + ); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveAuthenticationManager authenticationManager() { + return mock(ReactiveAuthenticationManager.class); + } + + @Bean + ServerAuthenticationSuccessHandler authenticationSuccessHandler() { + return mock(ServerAuthenticationSuccessHandler.class); + } + + @Bean + MockWebServer mockWebServer() { + return this.mockWebServer; + } + + @PreDestroy + void shutdown() throws IOException { + this.mockWebServer.shutdown(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class CustomBearerTokenServerAuthenticationConverter {