getLogoutProperties(String clientRegistrationId) {
+ return Optional.ofNullable(oauth2Logout.get(clientRegistrationId));
+ }
}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsBackChannelLogoutBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsBackChannelLogoutBeans.java
deleted file mode 100644
index a6173f373..000000000
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsBackChannelLogoutBeans.java
+++ /dev/null
@@ -1,206 +0,0 @@
-package com.c4_soft.springaddons.security.oidc.starter.reactive.client;
-
-import static org.springframework.security.config.Customizer.withDefaults;
-
-import java.util.Collection;
-import java.util.Set;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.AfterReturning;
-import org.aspectj.lang.annotation.Aspect;
-import org.aspectj.lang.annotation.Before;
-import org.aspectj.lang.annotation.Pointcut;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
-import org.springframework.security.config.web.server.ServerHttpSecurity;
-import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
-import org.springframework.security.web.server.SecurityWebFilterChain;
-import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
-import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
-import org.springframework.stereotype.Component;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.server.WebSession;
-
-import lombok.RequiredArgsConstructor;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-/**
- *
- * This provides with a client side implementation of the OIDC Back-Channel Logout
- * specification. Keycloak conforms to this OP side of the spec.
- * Auth0 could some day.
- *
- *
- * Implementation is made with a security filter-chain intercepting just the "/backchannel_logout" route and a controller handling requests to that end-point.
- *
- *
- * This beans are defined only if "com.c4-soft.springaddons.oidc.client.back-channel-logout-enabled" property is true.
- *
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-// @Conditional(IsNotServlet.class)
-// @ConditionalOnProperty("com.c4-soft.springaddons.oidc.client.back-channel-logout-enabled")
-// @AutoConfiguration
-// @ImportAutoConfiguration(ReactiveSpringAddonsOidcBeans.class)
-public class ReactiveSpringAddonsBackChannelLogoutBeans {
-
- private static final String BACKCHANNEL_LOGOUT_PATH = "/backchannel_logout";
-
- /**
- * Requests from the OP are anonymous, are not part of a session, and have no CSRF token. It contains a logout JWT which serves both to authenticate the
- * request and protect against CSRF.
- *
- * @param http
- * @param serverProperties Spring Boot server properties
- * @return a security filter-chain dedicated to back-channel logout handling
- * @throws Exception
- */
- @Order(Ordered.HIGHEST_PRECEDENCE)
- @Bean
- SecurityWebFilterChain springAddonsBackChannelLogoutClientFilterChain(ServerHttpSecurity http, ServerProperties serverProperties) throws Exception {
- http.securityMatcher(new PathPatternParserServerWebExchangeMatcher(BACKCHANNEL_LOGOUT_PATH));
- http.authorizeExchange(exchange -> exchange.anyExchange().permitAll());
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.redirectToHttps(withDefaults());
- }
- http.cors(cors -> cors.disable());
- http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
- http.csrf(csrf -> csrf.disable());
- return http.build();
- }
-
- /**
- *
- * Handles a POST request containing a JWT logout token provided as application/x-www-form-urlencoded as specified in
- * Back-Channel Logout specification.
- *
- *
- * This end-point will:
- *
- * - remove the relevant authorized client (based on issuer URI) for the relevant user (based on the subject)
- * - maybe invalidate user session: only if the removed authorized client was the last one the user had
- *
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
- @Component
- @RestController
- public static class BackChannelLogoutController {
- // private final AbstractReactiveAuthorizedSessionRepository authorizedSessionRepository;
- // private final Map issuersData = new ConcurrentHashMap();
- // private final ServerLogoutHandler logoutHandler;
- // private final ReactiveClientRegistrationRepository clientRegistrationRepo;
- //
- // public BackChannelLogoutController(
- // AbstractReactiveAuthorizedSessionRepository authorizedClientRepository,
- // InMemoryReactiveClientRegistrationRepository registrationRepo,
- // ServerLogoutHandler logoutHandler,
- // ReactiveClientRegistrationRepository clientRegistrationRepo) {
- // this.authorizedSessionRepository = authorizedClientRepository;
- // this.logoutHandler = logoutHandler;
- // this.clientRegistrationRepo = clientRegistrationRepo;
- // StreamSupport.stream(registrationRepo.spliterator(), false)
- // .filter(reg -> AuthorizationGrantType.AUTHORIZATION_CODE.equals(reg.getAuthorizationGrantType())).forEach(reg -> {
- // final var issuer = reg.getProviderDetails().getIssuerUri();
- // if (!this.issuersData.containsKey(issuer)) {
- // this.issuersData.put(
- // issuer,
- // new IssuerData(
- // issuer,
- // new HashSet<>(),
- // NimbusReactiveJwtDecoder.withJwkSetUri(reg.getProviderDetails().getJwkSetUri()).build()));
- // }
- // issuersData.get(issuer).clientRegistrationIds().add(reg.getRegistrationId());
- // });
- // }
- //
- // @PostMapping(path = BACKCHANNEL_LOGOUT_PATH, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
- // public Mono> backChannelLogout(ServerWebExchange serverWebExchange) {
- // serverWebExchange.getFormData().subscribe(body -> {
- // final var tokenString = body.get("logout_token");
- // if (tokenString == null || tokenString.size() != 1) {
- // throw new BadLogoutRequestException();
- // }
- // issuersData.forEach((issuer, data) -> {
- // data.jwtDecoder().decode(tokenString.get(0)).onErrorComplete().subscribe(jwt -> {
- // final var isLogoutToken = Optional.ofNullable(jwt.getClaims().get("events")).map(Object::toString)
- // .map(evt -> evt.contains("http://schemas.openid.net/event/backchannel-logout")).orElse(false);
- // if (!isLogoutToken) {
- // throw new BadLogoutRequestException();
- // }
- // final var logoutIss = Optional.ofNullable(jwt.getIssuer()).map(URL::toString).orElse(null);
- // if (!Objects.equals(issuer, logoutIss)) {
- // throw new BadLogoutRequestException();
- // }
- // for (var id : data.clientRegistrationIds()) {
- // clientRegistrationRepo.findByRegistrationId(id).subscribe(reg -> {
- // final var usernameClaim = reg.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
- // final var principalName = jwt.getClaimAsString(usernameClaim);
- // authorizedSessionRepository.delete(new OAuth2AuthorizedClientId(id, principalName)).subscribe(sessionId -> {
- // authorizedSessionRepository.findAuthorizedClientIdsBySessionId(sessionId).collectList().subscribe(authorizedClientIds -> {
- // if (authorizedClientIds.size() == 0) {
- // logoutHandler.logout(null, null);
- // }
- // });
- // });
- // });
- // }
- // });
- // });
- // });
- // return Mono.just(ResponseEntity.ok().build());
- // }
- //
- // @ResponseStatus(HttpStatus.BAD_REQUEST)
- // static final class BadLogoutRequestException extends RuntimeException {
- // private static final long serialVersionUID = -1803794467531166681L;
- // }
- }
-
- @Aspect
- @Component
- @RequiredArgsConstructor
- public static class ReactiveSessionRepositoryAspect implements SessionLifecycleEventNotifier {
- private static final Collection listeners = new ConcurrentLinkedQueue<>();
-
- @Override
- public void register(ReactiveSessionListener listener) {
- listeners.add(listener);
- }
-
- @Pointcut("within(org.springframework.session.ReactiveSessionRepository+) && execution(* *.createSession(..))")
- public void createSession() {
- }
-
- @Pointcut("within(org.springframework.session.ReactiveSessionRepository+) && execution(* *.deleteById(..))")
- public void deleteById() {
- }
-
- @AfterReturning(value = "createSession()", returning = "session")
- public void afterSessionCreated(Mono session) {
- session.flatMap(s -> Flux.fromIterable(listeners).doOnNext(l -> l.sessionCreated(s)).then(Mono.just(s))).subscribe();
- }
-
- @Before(value = "deleteById()")
- public void beforeDeleteById(JoinPoint jp) {
- var sessionId = (String) jp.getArgs()[0];
- listeners.forEach(l -> {
- l.sessionRemoved(sessionId);
- });
- }
- }
-
- // @ConditionalOnMissingBean
- // @Bean
- // AbstractReactiveAuthorizedSessionRepository authorizedSessionRepository(SessionLifecycleEventNotifier sessionEventNotifier) {
- // return new InMemoryReactiveAuthorizedSessionRepository(sessionEventNotifier);
- // }
-
- private static record IssuerData(String issuer, Set clientRegistrationIds, ReactiveJwtDecoder jwtDecoder) {
- }
-}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java
index d99c08c7c..ba39c95d7 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/ReactiveSpringAddonsOidcClientBeans.java
@@ -12,6 +12,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
+import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
@@ -113,6 +114,7 @@ public class ReactiveSpringAddonsOidcClientBeans {
* was not matched)
* @param httpPostProcessor post process the "http" builder just before it is returned (enables to override anything from the auto-configuration)
* spring-addons client properties}
+ * @param oidcLogoutCustomizer a configurer for Spring Security Back-Channel Logout implementation
* @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 clients
* @throws Exception in case of miss-configuration
*/
@@ -129,7 +131,8 @@ SecurityWebFilterChain clientFilterChain(
ServerLogoutSuccessHandler logoutSuccessHandler,
ClientAuthorizeExchangeSpecPostProcessor authorizePostProcessor,
ClientHttpSecurityPostProcessor httpPostProcessor,
- ServerLogoutHandler logoutHandler)
+ ServerLogoutHandler logoutHandler,
+ Customizer oidcLogoutCustomizer)
throws Exception {
final var clientRoutes = Stream
@@ -158,6 +161,10 @@ SecurityWebFilterChain clientFilterChain(
logout.logoutSuccessHandler(logoutSuccessHandler);
});
+ if(addonsProperties.getClient().getBackChannelLogout().isEnabled()) {
+ http.oidcLogout(oidcLogoutCustomizer);
+ }
+
ReactiveConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor);
return http.build();
@@ -280,4 +287,10 @@ public SpringAddonsPreAuthorizationCodeServerRedirectStrategy(HttpStatus default
}
}
+
+ @ConditionalOnMissingBean
+ @Bean
+ Customizer oidcLogoutSpec() {
+ return Customizer.withDefaults();
+ }
}
\ No newline at end of file
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsBackChannelLogoutBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsBackChannelLogoutBeans.java
deleted file mode 100644
index 9d9289ba3..000000000
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsBackChannelLogoutBeans.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.c4_soft.springaddons.security.oidc.starter.synchronised.client;
-
-import org.springframework.boot.autoconfigure.web.ServerProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.core.Ordered;
-import org.springframework.core.annotation.Order;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-
-/**
- *
- * This provides with a client side implementation of the OIDC Back-Channel Logout
- * specification. Keycloak conforms to this OP side of the spec.
- * Auth0 could some day.
- *
- *
- * Implementation is made with a security filter-chain intercepting just the "/backchannel_logout" route and a controller handling requests to that end-point.
- *
- *
- * This beans are defined only if "com.c4-soft.springaddons.oidc.client.back-channel-logout-enabled" property is true.
- *
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
-// @ConditionalOnWebApplication(type = Type.SERVLET)
-// @ConditionalOnProperty("com.c4-soft.springaddons.oidc.client.back-channel-logout-enabled")
-// @AutoConfiguration
-// @ImportAutoConfiguration(SpringAddonsOidcBeans.class)
-public class SpringAddonsBackChannelLogoutBeans {
-
- private static final String BACKCHANNEL_LOGOUT_PATH = "/backchannel_logout";
-
- /**
- * Requests from the OP are anonymous, are not part of a session, and have no CSRF token. It contains a logout JWT which serves both to authenticate the
- * request and protect against CSRF.
- *
- * @param http
- * @param serverProperties Spring Boot server properties
- * @return a security filter-chain dedicated to back-channel logout handling
- * @throws Exception
- */
- @Order(Ordered.HIGHEST_PRECEDENCE)
- @Bean
- SecurityFilterChain springAddonsBackChannelLogoutClientFilterChain(HttpSecurity http, ServerProperties serverProperties) throws Exception {
- http.securityMatcher(new AntPathRequestMatcher(BACKCHANNEL_LOGOUT_PATH));
- http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests.anyRequest().permitAll());
- if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
- http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
- }
- http.cors(cors -> cors.disable());
- http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
- http.csrf(csrf -> csrf.disable());
- return http.build();
- }
-
- /**
- *
- * Handles a POST request containing a JWT logout token provided as application/x-www-form-urlencoded as specified in
- * Back-Channel Logout specification.
- *
- *
- * This end-point will:
- *
- * - remove the relevant authorized client (based on issuer URI) for the relevant user (based on the subject)
- * - maybe invalidate user session: only if the removed authorized client was the last one the user had
- *
- *
- * @author Jerome Wacongne ch4mp@c4-soft.com
- */
- // @Component
- // @RestController
- // public static class BackChannelLogoutController {
- // private final AuthorizedSessionRepository authorizedSessionRepository;
- // private final Map jwtDecoders;
- //
- // public BackChannelLogoutController(AuthorizedSessionRepository authorizedClientRepository, InMemoryClientRegistrationRepository registrationRepo) {
- // this.authorizedSessionRepository = authorizedClientRepository;
- // this.jwtDecoders = StreamSupport.stream(registrationRepo.spliterator(), false)
- // .filter(reg -> AuthorizationGrantType.AUTHORIZATION_CODE.equals(reg.getAuthorizationGrantType()))
- // .map(ClientRegistration::getProviderDetails).collect(
- // Collectors.toMap(provider -> provider.getIssuerUri(), provider -> NimbusJwtDecoder.withJwkSetUri(provider.getJwkSetUri()).build()));
- // }
- //
- // @PostMapping(path = BACKCHANNEL_LOGOUT_PATH, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
- // public ResponseEntity backChannelLogout(@RequestParam MultiValueMap body) {
- // final var tokenString = body.get("logout_token");
- // if (tokenString == null || tokenString.size() != 1) {
- // throw new BadLogoutRequestException();
- // }
- // jwtDecoders.forEach((issuer, decoder) -> {
- // try {
- // final var jwt = decoder.decode(tokenString.get(0));
- // final var isLogoutToken = Optional.ofNullable(jwt.getClaims().get("events")).map(Object::toString)
- // .map(evt -> evt.contains("http://schemas.openid.net/event/backchannel-logout")).orElse(false);
- // if (!isLogoutToken) {
- // throw new BadLogoutRequestException();
- // }
- // final var logoutIss = Optional.ofNullable(jwt.getIssuer()).map(URL::toString).orElse(null);
- // if (!Objects.equals(issuer, logoutIss)) {
- // throw new BadLogoutRequestException();
- // }
- // final var logoutSub = jwt.getSubject();
- // final var sessionToInvalidate = authorizedSessionRepository.findById(new OAuth2AuthorizedClientId(logoutIss, logoutSub));
- // sessionToInvalidate.ifPresent(HttpSession::invalidate);
- // } catch (JwtException e) {
- // }
- // });
- // return ResponseEntity.ok().build();
- // }
- //
- // @ResponseStatus(HttpStatus.BAD_REQUEST)
- // static final class BadLogoutRequestException extends RuntimeException {
- // private static final long serialVersionUID = -8703279699142477824L;
- // }
- // }
-
-}
diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java
index feefb838f..7cfd56fda 100644
--- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java
+++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOidcClientBeans.java
@@ -13,8 +13,10 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
+import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
@@ -100,6 +102,7 @@ public class SpringAddonsOidcClientBeans {
* was not matched)
* @param httpPostProcessor post process the "http" builder just before it is returned (enables to override anything from the auto-configuration)
* spring-addons client properties}
+ * @param oidcLogoutCustomizer a configurer for Spring Security Back-Channel Logout implementation
* @return a security filter-chain scoped to specified security-matchers and adapted to OAuth2 clients
* @throws Exception in case of miss-configuration
*/
@@ -115,7 +118,8 @@ SecurityFilterChain springAddonsClientFilterChain(
LogoutSuccessHandler logoutSuccessHandler,
SpringAddonsOidcProperties addonsProperties,
ClientExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor,
- ClientHttpSecurityPostProcessor httpPostProcessor)
+ ClientHttpSecurityPostProcessor httpPostProcessor,
+ Customizer> oidcLogoutCustomizer)
throws Exception {
// @formatter:off
log.info("Applying client OAuth2 configuration for: {}", (Object[]) addonsProperties.getClient().getSecurityMatchers());
@@ -136,6 +140,10 @@ SecurityFilterChain springAddonsClientFilterChain(
});
// @formatter:on
+ if (addonsProperties.getClient().getBackChannelLogout().isEnabled()) {
+ http.oidcLogout(oidcLogoutCustomizer);
+ }
+
ServletConfigurationSupport.configureClient(http, serverProperties, addonsProperties.getClient(), authorizePostProcessor, httpPostProcessor);
return http.build();
@@ -234,4 +242,10 @@ AuthenticationSuccessHandler authenticationSuccessHandler(SpringAddonsOidcProper
AuthenticationFailureHandler authenticationFailureHandler(SpringAddonsOidcProperties addonsProperties) {
return new SpringAddonsOauth2AuthenticationFailureHandler(addonsProperties);
}
+
+ @ConditionalOnMissingBean
+ @Bean
+ Customizer> oidcLogoutCustomizer() {
+ return Customizer.withDefaults();
+ }
}