diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java index 04334be1bfd..a441e6f15f1 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutReactiveAuthenticationManager.java @@ -64,14 +64,14 @@ * "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel * Logout */ -final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager { +public final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager { private ReactiveJwtDecoderFactory logoutTokenDecoderFactory; /** * Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager} */ - OidcBackChannelLogoutReactiveAuthenticationManager() { + public OidcBackChannelLogoutReactiveAuthenticationManager() { Function> jwtValidator = (clientRegistration) -> JwtValidators .createDefaultWithValidators(new OidcBackChannelLogoutTokenValidator(clientRegistration)); this.logoutTokenDecoderFactory = (clientRegistration) -> { @@ -130,7 +130,7 @@ private Mono decode(ClientRegistration registration, String token) { * correspond to the {@link ClientRegistration} associated with the OIDC logout token. * @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use */ - void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory logoutTokenDecoderFactory) { + public void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory logoutTokenDecoderFactory) { Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null"); this.logoutTokenDecoderFactory = logoutTokenDecoderFactory; } diff --git a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java index c1709e1ec77..af5485f9ee0 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java +++ b/config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java @@ -46,7 +46,7 @@ * "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC * Back-Channel Logout spec */ -final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator { +public final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator { private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation"; @@ -56,7 +56,7 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator< private final String issuer; - OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { + public OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) { this.audience = clientRegistration.getClientId(); String issuer = clientRegistration.getProviderDetails().getIssuerUri(); Assert.hasText(issuer, "Provider issuer cannot be null"); 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 ac72b75eb9a..41b36b62bcb 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 @@ -5600,7 +5600,7 @@ public final class BackChannelLogoutConfigurer { private ServerAuthenticationConverter authenticationConverter; - private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager(); + private ReactiveAuthenticationManager authenticationManager; private Supplier logoutHandler = this::logoutHandler; @@ -5613,6 +5613,9 @@ private ServerAuthenticationConverter authenticationConverter() { } private ReactiveAuthenticationManager authenticationManager() { + if (this.authenticationManager == null) { + this.authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager(); + } return this.authenticationManager; } @@ -5745,6 +5748,41 @@ public BackChannelLogoutConfigurer logoutHandler(ServerLogoutHandler logoutHandl return this; } + /** + * Configure a custom instance of the authentication manager used for + * back-channel logout. + * + *

+ * By default, a new instance of + * {@link OidcBackChannelLogoutReactiveAuthenticationManager} will be created. + * If you want to customize the authentication manager, you can use this + * method. + * + *

+ * For example, if you want to customize the WebClient instance for fetching + * the JWKS keys in the logout process, you can configure Back-Channel Logout + * in the following way: + * + *

+			 * 	http
+			 *     	.oidcLogout((oidc) -> oidc
+			 *     		.backChannel(config -> {
+			 *     			var logoutTokenDecoderFactory = new CustomOidcLogoutTokenDecoderFactory();
+			 * 				var manager = new OidcBackChannelLogoutReactiveAuthenticationManager();
+			 * 				manager.setLogoutTokenDecoderFactory(logoutTokenDecoderFactory);
+			 * 				config.authenticationManager(manager);
+			 *			}))
+			 *     	);
+			 * 
+ * @param authenticationManager the {@link ReactiveAuthenticationManager} to + * use as replacement of the default used authentication manager + * @return {@link BackChannelLogoutConfigurer} for further customizations + * @since 6.5 + */ + public void setAuthenticationManager(ReactiveAuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + void configure(ServerHttpSecurity http) { ServerLogoutHandler oidcLogout = this.logoutHandler.get(); ServerLogoutHandler sessionLogout = new SecurityContextServerLogoutHandler(); diff --git a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java index ebff5d102b8..6ea21d72fd5 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java @@ -379,6 +379,35 @@ void logoutWhenCustomComponentsThenUses() { verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class)); } + @Test + void logoutCustomAuthenticationManagerThenUses() { + this.spring + .register(WebServerConfig.class, OidcProviderConfig.class, WithCustomAuthenticationManagerConfig.class) + .autowire(); + String registrationId = this.clientRegistration.getRegistrationId(); + String sessionId = login(); + String logoutToken = this.test.get() + .uri("/token/logout") + .cookie("SESSION", sessionId) + .exchange() + .expectStatus() + .isOk() + .returnResult(String.class) + .getResponseBody() + .blockFirst(); + this.test.post() + .uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString()) + .body(BodyInserters.fromFormData("logout_token", logoutToken)) + .exchange() + .expectStatus() + .isOk(); + this.test.get().uri("/token/logout").cookie("SESSION", sessionId).exchange().expectStatus().isUnauthorized(); + ReactiveOidcSessionRegistry sessionRegistry = this.spring.getContext() + .getBean(ReactiveOidcSessionRegistry.class); + verify(sessionRegistry, atLeastOnce()).saveSessionInformation(any()); + verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class)); + } + @Test void logoutWhenProviderIssuerMissingThen5xxServerError() { this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class) @@ -628,6 +657,35 @@ ReactiveOidcSessionRegistry sessionRegistry() { } + @Configuration + @EnableWebFluxSecurity + @Import(RegistrationConfig.class) + static class WithCustomAuthenticationManagerConfig { + + ReactiveOidcSessionRegistry sessionRegistry = spy(new InMemoryReactiveOidcSessionRegistry()); + + @Bean + @Order(1) + SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout((oidc) -> oidc.backChannel((backChannel) -> { + backChannel.setAuthenticationManager(new OidcBackChannelLogoutReactiveAuthenticationManager()); + })); + // @formatter:on + + return http.build(); + } + + @Bean + ReactiveOidcSessionRegistry sessionRegistry() { + return this.sessionRegistry; + } + + } + @Configuration @EnableWebFluxSecurity @Import(RegistrationConfig.class)