Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3070c96594 | |||
| 6eb7ef6e6d | |||
| 988ba6b33c | |||
| d1fb6141c1 | |||
| 43f5ae5b21 | |||
| b2a6bbf314 | |||
| a161421409 | |||
| 36aa2ad744 | |||
| ed4e0e2ea8 | |||
| 978eb4396a | |||
| 770c860d9d | |||
| 11890f2f49 | |||
| 5809451de7 | |||
| 31d379a7bf | |||
| 825370f8e3 |
@@ -4,7 +4,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
- 'docs-build'
|
||||
|
||||
run-name: Merge Dependabot PR ${{ github.ref_name }}
|
||||
@@ -14,4 +13,4 @@ jobs:
|
||||
permissions: write-all
|
||||
uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@0d3f15bb384839966a1ff5c4383731a2b747f24b # v7
|
||||
with:
|
||||
mergeArguments: --auto --rebase
|
||||
mergeArguments: --auto --rebase
|
||||
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
needs: [ build ]
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@b92832ecbc7cbe969201e6beafbde0ee400cf095 # v1.0.15
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||
with:
|
||||
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
|
||||
java-version: 25
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
milestone-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Compute Next Version
|
||||
id: next-version
|
||||
uses: spring-io/spring-release-actions/compute-next-version@a1f321783a0769dd2aea4fad6c2ae2f95a52b885 # 0.0.5
|
||||
uses: spring-io/spring-release-actions/compute-next-version@1b8671612c3eb3d9b9763e2d7b66f1a80d00ee95 # 0.0.6
|
||||
with:
|
||||
version: ${{ steps.todays-release.outputs.release-version }}
|
||||
- name: Schedule Next Milestone
|
||||
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
jobs:
|
||||
deploy-schema:
|
||||
name: Deploy Schema
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@d6c65d3013c0888e2c9cbae9f4beda610994776c # v1.0.16
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||
with:
|
||||
should-deploy-schema: true
|
||||
secrets: inherit
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
stage-release-announcements:
|
||||
name: Stage Release Announcements
|
||||
if: ${{ !github.event.repository.fork }}
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml@d6c65d3013c0888e2c9cbae9f4beda610994776c # v1.0.16
|
||||
uses: spring-io/spring-security-release-tools/.github/workflows/release-announcements-stage.yml@3f6cc7ffc137ca160061749d5f34dc30d5f36986 # v1.0.17
|
||||
with:
|
||||
version: ${{ inputs.version || github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
+27
@@ -297,6 +297,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
* @author Ankur Pathak
|
||||
* @author Alexey Nesterov
|
||||
* @author Yanming Zhou
|
||||
* @author Iain Henderson
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ServerHttpSecurity {
|
||||
@@ -4138,6 +4139,8 @@ public class ServerHttpSecurity {
|
||||
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
|
||||
|
||||
private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
|
||||
@@ -4186,6 +4189,20 @@ public class ServerHttpSecurity {
|
||||
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.2
|
||||
*/
|
||||
public OAuth2ResourceServerSpec authenticationSuccessHandler(
|
||||
ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
|
||||
this.authenticationSuccessHandler = authenticationSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link ServerAuthenticationConverter} to use for requests
|
||||
* authenticating with
|
||||
@@ -4254,6 +4271,7 @@ public class ServerHttpSecurity {
|
||||
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 @@ public class ServerHttpSecurity {
|
||||
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 @@ public class ServerHttpSecurity {
|
||||
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 @@ public class ServerHttpSecurity {
|
||||
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
|
||||
oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
|
||||
oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
|
||||
oauth2.setAuthenticationSuccessHandler(authenticationSuccessHandler());
|
||||
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
+180
@@ -73,9 +73,11 @@ import org.springframework.security.oauth2.server.resource.authentication.Reacti
|
||||
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,79 @@ public class OAuth2ResourceServerSpecTests {
|
||||
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 +1025,111 @@ public class OAuth2ResourceServerSpecTests {
|
||||
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
+42
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -8,3 +8,8 @@
|
||||
== Web
|
||||
|
||||
* Since Spring Framework's `HttpMethod#valueOf` now normalizes casing, `StrictServerWebExchangeFirewall` no longer detects a non-canonical-case HTTP method (for example, `get` instead of `GET`) as a distinct value; such requests are processed as the canonical method instead of being rejected. Applications with a customized `ServerExchangeRejectedHandler` should be aware it is no longer invoked for this case.
|
||||
|
||||
== OAuth 2.0
|
||||
|
||||
* https://github.com/spring-projects/spring-security/pull/18895[gh-18895] - Add `authenticationSuccessHandler` to the Reactive Resource Server DSL
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ io-spring-javaformat-spring-javaformat-checkstyle = { module = "io.spring.javafo
|
||||
io-spring-javaformat-spring-javaformat-gradle-plugin = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "io-spring-javaformat" }
|
||||
io-spring-nohttp-nohttp-checkstyle = { module = "io.spring.nohttp:nohttp-checkstyle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-nohttp-nohttp-gradle = { module = "io.spring.nohttp:nohttp-gradle", version.ref = "io-spring-nohttp" }
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.15"
|
||||
io-spring-security-release-plugin = "io.spring.gradle:spring-security-release-plugin:1.0.17"
|
||||
jakarta-annotation-jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0"
|
||||
jakarta-inject-jakarta-inject-api = "jakarta.inject:jakarta.inject-api:2.0.1"
|
||||
jakarta-persistence-jakarta-persistence-api = "jakarta.persistence:jakarta.persistence-api:3.2.0"
|
||||
|
||||
+29
-3
@@ -24,9 +24,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
@@ -79,7 +82,7 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
|
||||
.map((c) -> c.get(ServerWebExchange.class));
|
||||
// @formatter:on
|
||||
|
||||
private ServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
|
||||
private ServerSecurityContextRepository serverSecurityContextRepository = new NonRotatingWebSessionServerSecurityContextRepository();
|
||||
|
||||
private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
|
||||
|
||||
@@ -141,8 +144,7 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
|
||||
|
||||
/**
|
||||
* Sets a {@link ServerSecurityContextRepository} to use for refreshing a
|
||||
* {@link SecurityContext}, defaults to
|
||||
* {@link WebSessionServerSecurityContextRepository}.
|
||||
* {@link SecurityContext}.
|
||||
* @param serverSecurityContextRepository the {@link ServerSecurityContextRepository}
|
||||
* to use
|
||||
*/
|
||||
@@ -316,4 +318,28 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler
|
||||
return this.serverSecurityContextRepository.save(exchange, securityContext);
|
||||
}
|
||||
|
||||
private static final class NonRotatingWebSessionServerSecurityContextRepository
|
||||
implements ServerSecurityContextRepository {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(NonRotatingWebSessionServerSecurityContextRepository.class);
|
||||
|
||||
@Override
|
||||
public Mono<SecurityContext> load(ServerWebExchange exchange) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> save(ServerWebExchange exchange, @Nullable SecurityContext context) {
|
||||
Assert.notNull(context, "context cannot be null");
|
||||
// Save SecurityContext in WebSession without rotating session id.
|
||||
return exchange.getSession().doOnNext((session) -> {
|
||||
session.getAttributes()
|
||||
.put(WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
|
||||
logger.debug(LogMessage.format("Saved SecurityContext '%s' in WebSession: '%s'", context, session));
|
||||
}).then();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+47
@@ -50,7 +50,9 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
@@ -140,6 +142,51 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
// gh-19424
|
||||
@Test
|
||||
void onAuthorizationSuccessWhenDefaultServerSecurityContextRepositoryThenWebSessionIdNotChanged() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
Instant authTime = Instant.now();
|
||||
DefaultOidcUser principal = createOidcUser(authTime);
|
||||
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
|
||||
principal.getAuthorities(), clientRegistration.getRegistrationId());
|
||||
OAuth2AccessToken accessToken = createAccessToken();
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
|
||||
accessToken, null);
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
|
||||
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
|
||||
OidcParameterNames.ID_TOKEN, "id-token-1234");
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("iss", principal.getIssuer());
|
||||
claims.put("sub", principal.getSubject());
|
||||
claims.put("aud", principal.getAudience());
|
||||
claims.put("auth_time", authTime);
|
||||
claims.put("nonce", principal.getNonce());
|
||||
Jwt jwt = mock(Jwt.class);
|
||||
given(jwt.getTokenValue()).willReturn("id-token-1234");
|
||||
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
|
||||
given(jwt.getClaims()).willReturn(claims);
|
||||
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
|
||||
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
|
||||
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
|
||||
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
|
||||
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
|
||||
given(userService.loadUser(any())).willReturn(Mono.just(principal));
|
||||
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
|
||||
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
|
||||
handler.setUserService(userService);
|
||||
String originalSessionId = exchange.getSession().map(WebSession::getId).block();
|
||||
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
|
||||
.verifyComplete();
|
||||
StepVerifier.create(exchange.getSession())
|
||||
.assertNext((session) -> assertThat(session.getId()).isEqualTo(originalSessionId))
|
||||
.verifyComplete();
|
||||
WebSessionServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository();
|
||||
StepVerifier.create(securityContextRepository.load(exchange).mapNotNull(SecurityContext::getAuthentication))
|
||||
.expectNext(authenticationToken)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onAuthorizationSuccessWhenIdTokenIssuerNotSameThenException() {
|
||||
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
|
||||
|
||||
+8
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -77,6 +78,7 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -320,6 +322,8 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionITests {
|
||||
doReturn(Mono.just(authorizedClient)).when(this.authorizedClientRepository)
|
||||
.loadAuthorizedClient(eq(clientRegistration.getRegistrationId()), eq(this.authentication),
|
||||
eq(this.exchange));
|
||||
// Capture the original session id.
|
||||
String originalSessionId = this.exchange.getSession().map(WebSession::getId).block();
|
||||
this.webClient.get()
|
||||
.uri(this.serverUrl)
|
||||
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction
|
||||
@@ -356,6 +360,10 @@ public class ServerOAuth2AuthorizedClientExchangeFilterFunctionITests {
|
||||
assertThat(oidcUser.getSubject()).isEqualTo("subject-1234");
|
||||
assertThat(oidcUser.getName()).isEqualTo("refreshed-username");
|
||||
});
|
||||
// Verify that session id was not changed.
|
||||
StepVerifier.create(this.exchange.getSession())
|
||||
.assertNext((session) -> assertThat(session.getId()).isEqualTo(originalSessionId))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user