Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
* @author Ankur Pathak
* @author Alexey Nesterov
* @author Yanming Zhou
* @author Iain Henderson
* @since 5.0
*/
public class ServerHttpSecurity {
Expand Down Expand Up @@ -4138,6 +4139,8 @@ public class OAuth2ResourceServerSpec {

private ServerAuthenticationFailureHandler authenticationFailureHandler;

private ServerAuthenticationSuccessHandler authenticationSuccessHandler;

private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();

private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
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.ServerAccessDeniedHandler
import org.springframework.web.server.ServerWebExchange

Expand All @@ -35,6 +36,8 @@ import org.springframework.web.server.ServerWebExchange
* @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with
* Bearer Tokens.
* @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use.
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to use after
* authentication success.
*/
@ServerSecurityMarker
class ServerOAuth2ResourceServerDsl {
Expand All @@ -43,6 +46,7 @@ class ServerOAuth2ResourceServerDsl {
var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null
var bearerTokenConverter: ServerAuthenticationConverter? = null
var authenticationManagerResolver: ReactiveAuthenticationManagerResolver<ServerWebExchange>? = null
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null

private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null
private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null
Expand Down Expand Up @@ -115,6 +119,7 @@ class ServerOAuth2ResourceServerDsl {
authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) }
bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) }
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) }
authenticationSuccessHandler?.also { oauth2ResourceServer.authenticationSuccessHandler(authenticationSuccessHandler) }
jwt?.also { oauth2ResourceServer.jwt(jwt) }
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -371,6 +373,79 @@ 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();
Expand Down Expand Up @@ -950,6 +1025,111 @@ 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.security.config.web.server

import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import org.junit.jupiter.api.Test
Expand All @@ -37,6 +38,7 @@ 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.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.reactive.config.EnableWebFlux
Expand Down Expand Up @@ -183,6 +185,46 @@ class ServerOAuth2ResourceServerDslTests {

}

@Test
fun `request when custom authentication success handler then success handler used`() {
this.spring.register(AuthenticationSuccessHandlerConfig::class.java).autowire()
every {
AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any())
} returns Mono.empty()

this.client.get()
.uri("/")
.headers { it.setBearerAuth(validJwt) }
.exchange()

verify(exactly = 1) { AuthenticationSuccessHandlerConfig.SUCCESS_HANDLER.onAuthenticationSuccess(any(), any()) }
}

@Configuration
@EnableWebFluxSecurity
@EnableWebFlux
open class AuthenticationSuccessHandlerConfig {

companion object {
val SUCCESS_HANDLER: ServerAuthenticationSuccessHandler = mockk()
}

@Bean
open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
authenticationSuccessHandler = SUCCESS_HANDLER
jwt {
publicKey = publicKey()
}
}
}
}
}

@Test
fun `request when custom bearer token converter configured then custom converter used`() {
this.spring.register(BearerTokenConverterConfig::class.java).autowire()
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/whats-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
== OAuth 2.0

* https://github.com/spring-projects/spring-security/issues/18745[gh-18745] - Add RestClientOpaqueTokenIntrospector
* https://github.com/spring-projects/spring-security/pull/18895[gh-18895] - Add `authenticationSuccessHandler` to the Reactive Resource Server DSL

== WebAuthn

Expand Down