Expose OidcBackChannelLogoutHandler
This component already uses by default a URI that doesn't require a CSRF token and aalready allows for configuring a cookie name. So, by making it public and configurable in the DSL, both of these tickets quite naturally close. Closes gh-13841 Closes gh-14904
This commit is contained in:
parent
2d4c498c3b
commit
8bb5875595
|
@ -20,6 +20,7 @@ import java.util.Collections;
|
|||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||
|
@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
|||
|
||||
private final OidcLogoutToken logoutToken;
|
||||
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||
*/
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
|
||||
super(Collections.emptyList());
|
||||
this.logoutToken = logoutToken;
|
||||
this.clientRegistration = clientRegistration;
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
|||
return this.logoutToken;
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio
|
|||
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
|
||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||
.build();
|
||||
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
|
||||
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -58,7 +58,7 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
|
|||
|
||||
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
private final LogoutHandler logoutHandler;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutFilter}
|
||||
|
@ -68,11 +68,13 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
|
|||
* Logout Tokens
|
||||
*/
|
||||
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
|
||||
AuthenticationManager authenticationManager) {
|
||||
AuthenticationManager authenticationManager, LogoutHandler logoutHandler) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,14 +128,4 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
|
|||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
/**
|
||||
* The strategy for expiring all Client sessions indicated by the logout request.
|
||||
* Defaults to {@link OidcBackChannelLogoutHandler}.
|
||||
* @param logoutHandler the {@link LogoutHandler} to use
|
||||
*/
|
||||
void setLogoutHandler(LogoutHandler logoutHandler) {
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,10 +29,9 @@ import org.apache.commons.logging.LogFactory;
|
|||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
|
@ -40,6 +39,8 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMe
|
|||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
@ -51,25 +52,29 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
* Back-Channel Logout Token and invalidates each one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @since 6.4
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
public final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
private final OidcSessionRegistry sessionRegistry;
|
||||
|
||||
private RestOperations restOperations = new RestTemplate();
|
||||
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
|
||||
|
||||
private String sessionCookieName = "JSESSIONID";
|
||||
|
||||
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||
|
||||
public OidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry) {
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||
|
@ -86,7 +91,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
|||
for (OidcSessionInformation session : sessions) {
|
||||
totalCount++;
|
||||
try {
|
||||
eachLogout(request, session);
|
||||
eachLogout(request, token, session);
|
||||
invalidatedCount++;
|
||||
}
|
||||
catch (RestClientException ex) {
|
||||
|
@ -103,18 +108,23 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
|
||||
private void eachLogout(HttpServletRequest request, OidcBackChannelLogoutAuthentication token,
|
||||
OidcSessionInformation session) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||
headers.add(credential.getKey(), credential.getValue());
|
||||
}
|
||||
String logout = computeLogoutEndpoint(request);
|
||||
HttpEntity<?> entity = new HttpEntity<>(null, headers);
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
String logout = computeLogoutEndpoint(request, token);
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap();
|
||||
body.add("logout_token", token.getPrincipal().getTokenValue());
|
||||
body.add("_spring_security_internal_logout", "true");
|
||||
HttpEntity<?> entity = new HttpEntity<>(body, headers);
|
||||
this.restOperations.postForEntity(logout, entity, Object.class);
|
||||
}
|
||||
|
||||
String computeLogoutEndpoint(HttpServletRequest request) {
|
||||
String computeLogoutEndpoint(HttpServletRequest request, OidcBackChannelLogoutAuthentication token) {
|
||||
// @formatter:off
|
||||
UriComponents uriComponents = UriComponentsBuilder
|
||||
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
|
@ -137,6 +147,9 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
|||
int port = uriComponents.getPort();
|
||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||
|
||||
String registrationId = token.getClientRegistration().getRegistrationId();
|
||||
uriVariables.put("registrationId", registrationId);
|
||||
|
||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
|
@ -158,34 +171,13 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
|
||||
* this class uses
|
||||
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
|
||||
* sessions.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link RestOperations} to perform the per-session back-channel logout
|
||||
* @param restOperations the {@link RestOperations} to use
|
||||
*/
|
||||
void setRestOperations(RestOperations restOperations) {
|
||||
Assert.notNull(restOperations, "restOperations cannot be null");
|
||||
this.restOperations = restOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||
* since that is the default URI for
|
||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||
* @param logoutUri the URI to use
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
public void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutUri = logoutUri;
|
||||
}
|
||||
|
@ -197,7 +189,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
|||
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||
* @param sessionCookieName the cookie name to use
|
||||
*/
|
||||
void setSessionCookieName(String sessionCookieName) {
|
||||
public void setSessionCookieName(String sessionCookieName) {
|
||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
}
|
||||
|
|
|
@ -19,16 +19,24 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
|
|||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
@ -140,8 +148,11 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
|||
}
|
||||
|
||||
private LogoutHandler logoutHandler(B http) {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
OidcBackChannelLogoutHandler logoutHandler = getBeanOrNull(OidcBackChannelLogoutHandler.class);
|
||||
if (logoutHandler != null) {
|
||||
return logoutHandler;
|
||||
}
|
||||
logoutHandler = new OidcBackChannelLogoutHandler(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
|
@ -176,21 +187,137 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
|||
*/
|
||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||
this.logoutHandler = (http) -> {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(
|
||||
OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||
logoutHandler.setLogoutUri(logoutUri);
|
||||
return logoutHandler;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure what and how per-session logout will be performed.
|
||||
*
|
||||
* <p>
|
||||
* This overrides any value given to {@link #logoutUri(String)}
|
||||
*
|
||||
* <p>
|
||||
* By default, the resulting {@link LogoutHandler} will {@code POST} the session
|
||||
* cookie and OIDC logout token back to the original back-channel logout endpoint.
|
||||
*
|
||||
* <p>
|
||||
* Using this method changes the underlying default that {@code POST}s the session
|
||||
* cookie and CSRF token to your application's {@code /logout} endpoint. As such,
|
||||
* it is recommended to call this instead of accepting the {@code /logout} default
|
||||
* as this does not require any special CSRF configuration, even if you don't
|
||||
* require other changes.
|
||||
*
|
||||
* <p>
|
||||
* For example, configuring Back-Channel Logout in the following way:
|
||||
*
|
||||
* <pre>
|
||||
* http
|
||||
* .oidcLogout((oidc) -> oidc
|
||||
* .backChannel((backChannel) -> backChannel
|
||||
* .logoutHandler(new OidcBackChannelLogoutHandler())
|
||||
* )
|
||||
* );
|
||||
* </pre>
|
||||
*
|
||||
* will make so that the per-session logout invocation no longer requires special
|
||||
* CSRF configurations.
|
||||
*
|
||||
* <p>
|
||||
* The default URI is
|
||||
* {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is simply
|
||||
* an internal version of the same endpoint exposed to your Back-Channel services.
|
||||
* You can use {@link OidcBackChannelLogoutHandler#setLogoutUri(String)} to alter
|
||||
* the scheme, server name, or port in the {@code Host} header to accommodate how
|
||||
* your application would address itself internally.
|
||||
*
|
||||
* <p>
|
||||
* For example, if the way your application would internally call itself is on a
|
||||
* different scheme and port than incoming traffic, you can configure the endpoint
|
||||
* in the following way:
|
||||
*
|
||||
* <pre>
|
||||
* http
|
||||
* .oidcLogout((oidc) -> oidc
|
||||
* .backChannel((backChannel) -> backChannel
|
||||
* .logoutHandler("http://localhost:9000/logout/connect/back-channel/{registrationId}")
|
||||
* )
|
||||
* );
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* You can also publish it as a {@code @Bean} as follows:
|
||||
*
|
||||
* <pre>
|
||||
* @Bean
|
||||
* OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
|
||||
* OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);
|
||||
* logoutHandler.setSessionCookieName("SESSION");
|
||||
* return logoutHandler;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* to have the same effect.
|
||||
* @param logoutHandler the {@link LogoutHandler} to use each individual session
|
||||
* @return {@link BackChannelLogoutConfigurer} for further customizations
|
||||
* @since 6.4
|
||||
*/
|
||||
public BackChannelLogoutConfigurer logoutHandler(LogoutHandler logoutHandler) {
|
||||
this.logoutHandler = (http) -> logoutHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(B http) {
|
||||
LogoutHandler oidcLogout = this.logoutHandler.apply(http);
|
||||
LogoutHandler sessionLogout = new SecurityContextLogoutHandler();
|
||||
LogoutConfigurer<B> logout = http.getConfigurer(LogoutConfigurer.class);
|
||||
if (logout != null) {
|
||||
sessionLogout = new CompositeLogoutHandler(logout.getLogoutHandlers());
|
||||
}
|
||||
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(this.logoutHandler.apply(http));
|
||||
authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
|
||||
http.addFilterBefore(filter, CsrfFilter.class);
|
||||
}
|
||||
|
||||
private <T> T getBeanOrNull(Class<?> clazz) {
|
||||
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||
if (context != null) {
|
||||
String[] names = context.getBeanNamesForType(clazz);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class EitherLogoutHandler implements LogoutHandler {
|
||||
|
||||
private final LogoutHandler left;
|
||||
|
||||
private final LogoutHandler right;
|
||||
|
||||
EitherLogoutHandler(LogoutHandler left, LogoutHandler right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
if (request.getParameter("_spring_security_internal_logout") == null) {
|
||||
this.left.logout(request, response, authentication);
|
||||
}
|
||||
else {
|
||||
this.right.logout(request, response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.util.Collections;
|
|||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||
|
@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
|||
|
||||
private final OidcLogoutToken logoutToken;
|
||||
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||
*/
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
|
||||
super(Collections.emptyList());
|
||||
this.logoutToken = logoutToken;
|
||||
this.clientRegistration = clientRegistration;
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
|
@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
|||
return this.logoutToken;
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
|
|||
.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
|
||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||
.build())
|
||||
.map(OidcBackChannelLogoutAuthentication::new);
|
||||
.map((oidcLogoutToken) -> new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration));
|
||||
}
|
||||
|
||||
private Mono<Jwt> decode(ClientRegistration registration, String token) {
|
||||
|
|
|
@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
|||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
|
@ -60,7 +59,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||
|
||||
private final ReactiveAuthenticationManager authenticationManager;
|
||||
|
||||
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
private final ServerLogoutHandler logoutHandler;
|
||||
|
||||
/**
|
||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||
|
@ -70,11 +69,13 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||
* Logout Tokens
|
||||
*/
|
||||
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
|
||||
ReactiveAuthenticationManager authenticationManager) {
|
||||
ReactiveAuthenticationManager authenticationManager, ServerLogoutHandler logoutHandler) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -124,14 +125,4 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
|||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
||||
}
|
||||
|
||||
/**
|
||||
* The strategy for expiring all Client sessions indicated by the logout request.
|
||||
* Defaults to {@link OidcBackChannelServerLogoutHandler}.
|
||||
* @param logoutHandler the {@link LogoutHandler} to use
|
||||
*/
|
||||
void setLogoutHandler(ServerLogoutHandler logoutHandler) {
|
||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,15 +34,15 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
@ -52,23 +52,27 @@ import org.springframework.web.util.UriComponentsBuilder;
|
|||
* Back-Channel Logout Token and invalidates each one.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 6.2
|
||||
* @since 6.4
|
||||
* @see <a target="_blank" href=
|
||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||
* Spec</a>
|
||||
*/
|
||||
final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
private final ReactiveOidcSessionRegistry sessionRegistry;
|
||||
|
||||
private WebClient web = WebClient.create();
|
||||
|
||||
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
|
||||
private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
|
||||
|
||||
private String sessionCookieName = "SESSION";
|
||||
|
||||
public OidcBackChannelServerLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||
|
@ -84,7 +88,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
|||
AtomicInteger invalidatedCount = new AtomicInteger(0);
|
||||
return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
|
||||
totalCount.incrementAndGet();
|
||||
return eachLogout(exchange, session).flatMap((response) -> {
|
||||
return eachLogout(exchange, session, token).flatMap((response) -> {
|
||||
invalidatedCount.incrementAndGet();
|
||||
return Mono.empty();
|
||||
}).onErrorResume((ex) -> {
|
||||
|
@ -105,17 +109,26 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
|||
});
|
||||
}
|
||||
|
||||
private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
|
||||
private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session,
|
||||
OidcBackChannelLogoutAuthentication token) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||
headers.add(credential.getKey(), credential.getValue());
|
||||
}
|
||||
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
|
||||
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
|
||||
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest(), token);
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("logout_token", token.getPrincipal().getTokenValue());
|
||||
body.add("_spring_security_internal_logout", "true");
|
||||
return this.web.post()
|
||||
.uri(logout)
|
||||
.headers((h) -> h.putAll(headers))
|
||||
.body(BodyInserters.fromFormData(body))
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
String computeLogoutEndpoint(ServerHttpRequest request) {
|
||||
String computeLogoutEndpoint(ServerHttpRequest request, OidcBackChannelLogoutAuthentication token) {
|
||||
// @formatter:off
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
|
||||
.replacePath(request.getPath().contextPath().value())
|
||||
|
@ -137,6 +150,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
|||
int port = uriComponents.getPort();
|
||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||
|
||||
String registrationId = token.getClientRegistration().getRegistrationId();
|
||||
uriVariables.put("registrationId", registrationId);
|
||||
|
||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||
.buildAndExpand(uriVariables)
|
||||
.toUriString();
|
||||
|
@ -161,34 +177,13 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
|||
return response.writeWith(Flux.just(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
|
||||
* this class uses
|
||||
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
|
||||
* sessions.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link WebClient} to perform the per-session back-channel logout
|
||||
* @param web the {@link WebClient} to use
|
||||
*/
|
||||
void setWebClient(WebClient web) {
|
||||
Assert.notNull(web, "web cannot be null");
|
||||
this.web = web;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||
* since that is the default URI for
|
||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||
* @param logoutUri the URI to use
|
||||
*/
|
||||
void setLogoutUri(String logoutUri) {
|
||||
public void setLogoutUri(String logoutUri) {
|
||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||
this.logoutUri = logoutUri;
|
||||
}
|
||||
|
@ -200,7 +195,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
|||
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||
* @param sessionCookieName the cookie name to use
|
||||
*/
|
||||
void setSessionCookieName(String sessionCookieName) {
|
||||
public void setSessionCookieName(String sessionCookieName) {
|
||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
|||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
@ -5529,8 +5528,12 @@ public class ServerHttpSecurity {
|
|||
}
|
||||
|
||||
private ServerLogoutHandler logoutHandler() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = getBeanOrNull(
|
||||
OidcBackChannelServerLogoutHandler.class);
|
||||
if (logoutHandler != null) {
|
||||
return logoutHandler;
|
||||
}
|
||||
logoutHandler = new OidcBackChannelServerLogoutHandler(OidcLogoutSpec.this.getSessionRegistry());
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
|
@ -5548,9 +5551,9 @@ public class ServerHttpSecurity {
|
|||
*
|
||||
* <p>
|
||||
* By default, the URI is set to
|
||||
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
|
||||
* and port of the original back-channel request is preserved, while the host
|
||||
* and endpoint are changed.
|
||||
* {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, meaning
|
||||
* that the scheme and port of the original back-channel request is preserved,
|
||||
* while the host and endpoint are changed.
|
||||
*
|
||||
* <p>
|
||||
* If you are using Spring Security for the logout endpoint, the path part of
|
||||
|
@ -5561,27 +5564,135 @@ public class ServerHttpSecurity {
|
|||
* that the scheme, server name, or port in the {@code Host} header are
|
||||
* different from how you would address the same server internally.
|
||||
* @param logoutUri the URI to request logout on the back-channel
|
||||
* @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
|
||||
* further customizations
|
||||
* @return the {@link BackChannelLogoutConfigurer} for further customizations
|
||||
* @since 6.2.4
|
||||
*/
|
||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||
this.logoutHandler = () -> {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
|
||||
OidcLogoutSpec.this.getSessionRegistry());
|
||||
logoutHandler.setLogoutUri(logoutUri);
|
||||
return logoutHandler;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure what and how per-session logout will be performed.
|
||||
*
|
||||
* <p>
|
||||
* This overrides any value given to {@link #logoutUri(String)}
|
||||
*
|
||||
* <p>
|
||||
* By default, the resulting {@link LogoutHandler} will {@code POST} the
|
||||
* session cookie and OIDC logout token back to the original back-channel
|
||||
* logout endpoint.
|
||||
*
|
||||
* <p>
|
||||
* Using this method changes the underlying default that {@code POST}s the
|
||||
* session cookie and CSRF token to your application's {@code /logout}
|
||||
* endpoint. As such, it is recommended to call this instead of accepting the
|
||||
* {@code /logout} default as this does not require any special CSRF
|
||||
* configuration, even if you don't require other changes.
|
||||
*
|
||||
* <p>
|
||||
* For example, configuring Back-Channel Logout in the following way:
|
||||
*
|
||||
* <pre>
|
||||
* http
|
||||
* .oidcLogout((oidc) -> oidc
|
||||
* .backChannel((backChannel) -> backChannel
|
||||
* .logoutHandler(new OidcBackChannelServerLogoutHandler())
|
||||
* )
|
||||
* );
|
||||
* </pre>
|
||||
*
|
||||
* will make so that the per-session logout invocation no longer requires
|
||||
* special CSRF configurations.
|
||||
*
|
||||
* <p>
|
||||
* The default URI is
|
||||
* {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is
|
||||
* simply an internal version of the same endpoint exposed to your
|
||||
* Back-Channel services. You can use
|
||||
* {@link OidcBackChannelServerLogoutHandler#setLogoutUri(String)} to alter
|
||||
* the scheme, server name, or port in the {@code Host} header to accommodate
|
||||
* how your application would address itself internally.
|
||||
*
|
||||
* <p>
|
||||
* For example, if the way your application would internally call itself is on
|
||||
* a different scheme and port than incoming traffic, you can configure the
|
||||
* endpoint in the following way:
|
||||
*
|
||||
* <pre>
|
||||
* http
|
||||
* .oidcLogout((oidc) -> oidc
|
||||
* .backChannel((backChannel) -> backChannel
|
||||
* .logoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}")
|
||||
* )
|
||||
* );
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* You can also publish it as a {@code @Bean} as follows:
|
||||
*
|
||||
* <pre>
|
||||
* @Bean
|
||||
* OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
|
||||
* OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
* logoutHandler.setLogoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}");
|
||||
* return logoutHandler;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* to have the same effect.
|
||||
* @param logoutHandler the {@link ServerLogoutHandler} to use each individual
|
||||
* session
|
||||
* @return {@link BackChannelLogoutConfigurer} for further customizations
|
||||
* @since 6.4
|
||||
*/
|
||||
public BackChannelLogoutConfigurer logoutHandler(ServerLogoutHandler logoutHandler) {
|
||||
this.logoutHandler = () -> logoutHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
void configure(ServerHttpSecurity http) {
|
||||
ServerLogoutHandler oidcLogout = this.logoutHandler.get();
|
||||
ServerLogoutHandler sessionLogout = new SecurityContextServerLogoutHandler();
|
||||
LogoutSpec logout = ServerHttpSecurity.this.logout;
|
||||
if (logout != null) {
|
||||
sessionLogout = new DelegatingServerLogoutHandler(logout.logoutHandlers);
|
||||
}
|
||||
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
|
||||
authenticationManager());
|
||||
filter.setLogoutHandler(this.logoutHandler.get());
|
||||
authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
|
||||
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
|
||||
}
|
||||
|
||||
private static final class EitherLogoutHandler implements ServerLogoutHandler {
|
||||
|
||||
private final ServerLogoutHandler left;
|
||||
|
||||
private final ServerLogoutHandler right;
|
||||
|
||||
EitherLogoutHandler(ServerLogoutHandler left, ServerLogoutHandler right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||
return exchange.getExchange().getFormData().flatMap((data) -> {
|
||||
if (data.getFirst("_spring_security_internal_logout") == null) {
|
||||
return this.left.logout(exchange, authentication);
|
||||
}
|
||||
else {
|
||||
return this.right.logout(exchange, authentication);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
|
@ -72,4 +73,5 @@ class OidcLogoutDsl {
|
|||
backChannel?.also { oidcLogout.backChannel(backChannel) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.oauth2.login
|
|||
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using
|
||||
|
@ -28,7 +29,26 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli
|
|||
*/
|
||||
@OAuth2LoginSecurityMarker
|
||||
class OidcBackChannelLogoutDsl {
|
||||
private var _logoutUri: String? = null
|
||||
private var _logoutHandler: LogoutHandler? = null
|
||||
|
||||
var logoutHandler: LogoutHandler?
|
||||
get() = _logoutHandler
|
||||
set(value) {
|
||||
_logoutHandler = value
|
||||
_logoutUri = null
|
||||
}
|
||||
var logoutUri: String?
|
||||
get() = _logoutUri
|
||||
set(value) {
|
||||
_logoutUri = value
|
||||
_logoutHandler = null
|
||||
}
|
||||
|
||||
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit {
|
||||
return { backChannel -> }
|
||||
return { backChannel ->
|
||||
logoutHandler?.also { backChannel.logoutHandler(logoutHandler) }
|
||||
logoutUri?.also { backChannel.logoutUri(logoutUri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.springframework.security.config.web.server
|
||||
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler
|
||||
|
||||
/**
|
||||
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code.
|
||||
*
|
||||
|
@ -24,7 +26,26 @@ package org.springframework.security.config.web.server
|
|||
*/
|
||||
@ServerSecurityMarker
|
||||
class ServerOidcBackChannelLogoutDsl {
|
||||
private var _logoutUri: String? = null
|
||||
private var _logoutHandler: ServerLogoutHandler? = null
|
||||
|
||||
var logoutHandler: ServerLogoutHandler?
|
||||
get() = _logoutHandler
|
||||
set(value) {
|
||||
_logoutHandler = value
|
||||
_logoutUri = null
|
||||
}
|
||||
var logoutUri: String?
|
||||
get() = _logoutUri
|
||||
set(value) {
|
||||
_logoutUri = value
|
||||
_logoutHandler = null
|
||||
}
|
||||
|
||||
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit {
|
||||
return { backChannel -> }
|
||||
return { backChannel ->
|
||||
logoutHandler?.also { backChannel.logoutHandler(logoutHandler) }
|
||||
logoutUri?.also { backChannel.logoutUri(logoutUri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,9 @@ class ServerOidcLogoutDsl {
|
|||
* return http {
|
||||
* oauth2Login { }
|
||||
* oidcLogout {
|
||||
* backChannel { }
|
||||
* backChannel {
|
||||
* sessionLogout { }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
|
|
|
@ -19,44 +19,55 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class OidcBackChannelLogoutHandlerTests {
|
||||
|
||||
private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication(
|
||||
TestOidcLogoutTokens.withSubject("issuer", "subject").build(),
|
||||
TestClientRegistrations.clientRegistration().build());
|
||||
|
||||
// gh-14553
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
|
||||
request.setServerName("host.docker.internal");
|
||||
request.setServerPort(8090);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
request.setServerName("host.docker.internal");
|
||||
request.setServerPort(8090);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
|
||||
}
|
||||
|
||||
// gh-14609
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||
request.setScheme("https");
|
||||
request.setServerName("server-one.com");
|
||||
request.setServerPort(80);
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.time.Instant;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
|
@ -91,6 +92,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
|
@ -218,6 +220,40 @@ public class OidcLogoutConfigurerTests {
|
|||
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenSelfRemoteLogoutUriThenUses() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession session = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenDifferentCookieNameThenUses() throws Exception {
|
||||
this.spring.register(OidcProviderConfig.class, CookieConfig.class).autowire();
|
||||
String registrationId = this.clientRegistration.getRegistrationId();
|
||||
MockHttpSession session = login();
|
||||
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
this.mvc
|
||||
.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||
.param("logout_token", logoutToken))
|
||||
.andExpect(status().isOk());
|
||||
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
||||
|
@ -355,6 +391,87 @@ public class OidcLogoutConfigurerTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class SelfLogoutUriConfig {
|
||||
|
||||
private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OidcBackChannelLogoutHandler oidcLogoutHandler() {
|
||||
return new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class CookieConfig {
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
private final OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||
.oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OidcBackChannelLogoutHandler oidcLogoutHandler() {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setSessionCookieName("SESSION");
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
MockWebServer web(ObjectProvider<MockMvc> mvc) {
|
||||
MockMvcDispatcher dispatcher = new MockMvcDispatcher(mvc);
|
||||
dispatcher.setAssertion((rr) -> {
|
||||
String cookie = rr.getHeaders().get("Cookie");
|
||||
if (cookie == null) {
|
||||
return;
|
||||
}
|
||||
assertThat(cookie).contains("SESSION").doesNotContain("JSESSIONID");
|
||||
});
|
||||
this.server.setDispatcher(dispatcher);
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
|
@ -559,12 +676,15 @@ public class OidcLogoutConfigurerTests {
|
|||
|
||||
private MockMvc mvc;
|
||||
|
||||
private Consumer<RecordedRequest> assertion = (rr) -> { };
|
||||
|
||||
MockMvcDispatcher(ObjectProvider<MockMvc> mvc) {
|
||||
this.mvcProvider = mvc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
||||
this.assertion.accept(request);
|
||||
this.mvc = this.mvcProvider.getObject();
|
||||
String method = request.getMethod();
|
||||
String path = request.getPath();
|
||||
|
@ -601,6 +721,10 @@ public class OidcLogoutConfigurerTests {
|
|||
this.session.put(session.getId(), session);
|
||||
}
|
||||
|
||||
void setAssertion(Consumer<RecordedRequest> assertion) {
|
||||
this.assertion = assertion;
|
||||
}
|
||||
|
||||
private MockHttpSession session(RecordedRequest request) {
|
||||
String cookieHeaderValue = request.getHeader("Cookie");
|
||||
if (cookieHeaderValue == null) {
|
||||
|
@ -613,6 +737,10 @@ public class OidcLogoutConfigurerTests {
|
|||
return this.session.computeIfAbsent(parts[1],
|
||||
(k) -> new MockHttpSession(new MockServletContext(), parts[1]));
|
||||
}
|
||||
if ("SESSION".equals(parts[0])) {
|
||||
return this.session.computeIfAbsent(parts[1],
|
||||
(k) -> new MockHttpSession(new MockServletContext(), parts[1]));
|
||||
}
|
||||
}
|
||||
return new MockHttpSession();
|
||||
}
|
||||
|
|
|
@ -19,6 +19,10 @@ package org.springframework.security.config.web.server;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
@ -27,36 +31,43 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
*/
|
||||
public class OidcBackChannelServerLogoutHandlerTests {
|
||||
|
||||
private final ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
|
||||
private final OidcBackChannelLogoutAuthentication token = new OidcBackChannelLogoutAuthentication(
|
||||
TestOidcLogoutTokens.withSubject("issuer", "subject").build(),
|
||||
TestClientRegistrations.clientRegistration().build());
|
||||
|
||||
// gh-14553
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("https://host.docker.internal:8090/back-channel/logout")
|
||||
.build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("https://localhost:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("https://localhost:8090/logout");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("http://host.docker.internal:8090/back-channel/logout")
|
||||
.build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
|
||||
}
|
||||
|
||||
// gh-14609
|
||||
@Test
|
||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build();
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
||||
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.time.Instant;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
|
@ -96,6 +97,7 @@ import org.springframework.web.reactive.function.BodyInserters;
|
|||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
@ -268,6 +270,52 @@ public class OidcLogoutSpecTests {
|
|||
this.test.get().uri("/token/logout").cookie("SESSION", one).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenSelfRemoteLogoutUriThenUses() {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, SelfLogoutUriConfig.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();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenDifferentCookieNameThenUses() {
|
||||
this.spring.register(OidcProviderConfig.class, CookieConfig.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();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() {
|
||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
||||
|
@ -444,6 +492,81 @@ public class OidcLogoutSpecTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class SelfLogoutUriConfig {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||
.oauth2Login(Customizer.withDefaults())
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
static class CookieConfig {
|
||||
|
||||
private final ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
||||
|
||||
private final MockWebServer server = new MockWebServer();
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||
.oauth2Login((oauth2) -> oauth2.oidcSessionRegistry(this.sessionRegistry))
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
|
||||
this.sessionRegistry);
|
||||
logoutHandler.setSessionCookieName("JSESSIONID");
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
MockWebServer web(ObjectProvider<WebTestClient> web) {
|
||||
WebTestClientDispatcher dispatcher = new WebTestClientDispatcher(web);
|
||||
dispatcher.setAssertion((rr) -> {
|
||||
String cookie = rr.getHeaders().get("Cookie");
|
||||
if (cookie == null) {
|
||||
return;
|
||||
}
|
||||
assertThat(cookie).contains("JSESSIONID");
|
||||
});
|
||||
this.server.setDispatcher(dispatcher);
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
void shutdown() throws IOException {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@Import(RegistrationConfig.class)
|
||||
|
@ -652,12 +775,15 @@ public class OidcLogoutSpecTests {
|
|||
|
||||
private WebTestClient web;
|
||||
|
||||
private Consumer<RecordedRequest> assertion = (rr) -> { };
|
||||
|
||||
WebTestClientDispatcher(ObjectProvider<WebTestClient> web) {
|
||||
this.webProvider = web;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
||||
this.assertion.accept(request);
|
||||
this.web = this.webProvider.getObject();
|
||||
String method = request.getMethod();
|
||||
String path = request.getPath();
|
||||
|
@ -700,6 +826,10 @@ public class OidcLogoutSpecTests {
|
|||
}
|
||||
}
|
||||
|
||||
void setAssertion(Consumer<RecordedRequest> assertion) {
|
||||
this.assertion = assertion;
|
||||
}
|
||||
|
||||
private String session(RecordedRequest request) {
|
||||
String cookieHeaderValue = request.getHeader("Cookie");
|
||||
if (cookieHeaderValue == null) {
|
||||
|
@ -711,6 +841,9 @@ public class OidcLogoutSpecTests {
|
|||
if (SESSION_COOKIE_NAME.equals(parts[0])) {
|
||||
return parts[1];
|
||||
}
|
||||
if ("JSESSIONID".equals(parts[0])) {
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -23,13 +23,19 @@ import org.springframework.context.annotation.Bean
|
|||
import org.springframework.context.annotation.Configuration
|
||||
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.OidcBackChannelLogoutHandler
|
||||
import org.springframework.security.config.annotation.web.oauth2.login.OidcBackChannelLogoutDsl
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.post
|
||||
|
||||
|
@ -53,12 +59,23 @@ class OidcLogoutDslTests {
|
|||
this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) {
|
||||
param("logout_token", "token")
|
||||
}.andExpect { status { isBadRequest() } }
|
||||
val chain: SecurityFilterChain = this.spring.context.getBean(SecurityFilterChain::class.java)
|
||||
for (filter in chain.filters) {
|
||||
if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutFilter")) {
|
||||
val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler
|
||||
val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler
|
||||
var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String
|
||||
assert(cookieName.equals("SESSION"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class ClientRepositoryConfig {
|
||||
|
||||
private val sessionRegistry = InMemoryOidcSessionRegistry()
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
|
@ -73,6 +90,13 @@ class OidcLogoutDslTests {
|
|||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||
val logoutHandler = OidcBackChannelLogoutHandler(this.sessionRegistry)
|
||||
logoutHandler.setSessionCookieName("SESSION");
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun clientRegistration(): ClientRegistration {
|
||||
return TestClientRegistrations.clientRegistration().build()
|
||||
|
|
|
@ -25,14 +25,18 @@ import org.springframework.context.annotation.Configuration
|
|||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
import org.springframework.web.reactive.function.BodyInserters
|
||||
import org.springframework.web.server.WebFilter
|
||||
|
||||
/**
|
||||
* Tests for [ServerOidcLogoutDsl]
|
||||
|
@ -63,6 +67,15 @@ class ServerOidcLogoutDslTests {
|
|||
.body(BodyInserters.fromFormData("logout_token", "token"))
|
||||
.exchange()
|
||||
.expectStatus().isBadRequest
|
||||
val chain: SecurityWebFilterChain = this.spring.context.getBean(SecurityWebFilterChain::class.java)
|
||||
chain.webFilters.doOnNext({ filter: WebFilter ->
|
||||
if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutWebFilter")) {
|
||||
val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler
|
||||
val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler
|
||||
var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String
|
||||
assert(cookieName.equals("SESSION"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Configuration
|
||||
|
@ -70,6 +83,8 @@ class ServerOidcLogoutDslTests {
|
|||
@EnableWebFluxSecurity
|
||||
open class ClientRepositoryConfig {
|
||||
|
||||
private val sessionRegistry = InMemoryReactiveOidcSessionRegistry()
|
||||
|
||||
@Bean
|
||||
open fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http {
|
||||
|
@ -83,6 +98,13 @@ class ServerOidcLogoutDslTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun oidcLogoutHandler(): OidcBackChannelServerLogoutHandler {
|
||||
val logoutHandler = OidcBackChannelServerLogoutHandler(this.sessionRegistry)
|
||||
logoutHandler.setSessionCookieName("SESSION");
|
||||
return logoutHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun clientRegistration(): ClientRegistration {
|
||||
return TestClientRegistrations.clientRegistration().build()
|
||||
|
|
|
@ -137,6 +137,11 @@ Java::
|
|||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
|
||||
return new OidcBackChannelServerLogoutHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
|
||||
http
|
||||
|
@ -155,6 +160,11 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||
return OidcBackChannelLogoutHandler()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
http {
|
||||
|
@ -197,6 +207,80 @@ The overall flow for a Back-Channel logout is like this:
|
|||
Remember that Spring Security's OIDC support is multi-tenant.
|
||||
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
|
||||
|
||||
=== Customizing the Session Logout Endpoint
|
||||
|
||||
With `OidcBackChannelServerLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
|
||||
|
||||
If `OidcBackChannelServerLogoutHandler` is not wired, then the URL is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`, which is not recommended since it requires passing a CSRF token, which can be challenging depending on the kind of repository your application uses.
|
||||
|
||||
In the event that you need to customize the endpoint, you can provide the URL as follows:
|
||||
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source=java,role="primary"]
|
||||
----
|
||||
http
|
||||
// ...
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel((backChannel) -> backChannel
|
||||
.logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
|
||||
)
|
||||
);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source=kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
oidcLogout {
|
||||
backChannel {
|
||||
logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Customizing the Session Logout Cookie Name
|
||||
|
||||
By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
|
||||
|
||||
However, the default cookie name in Spring Session is `SESSION`.
|
||||
|
||||
You can configure Spring Session's cookie name in the DSL like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source=java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
OidcBackChannelServerLogoutHandler oidcLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry);
|
||||
logoutHandler.setSessionCookieName("SESSION");
|
||||
return logoutHandler;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source=kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun oidcLogoutHandler(val sessionRegistry: ReactiveOidcSessionRegistry): OidcBackChannelServerLogoutHandler {
|
||||
val logoutHandler = OidcBackChannelServerLogoutHandler(sessionRegistry)
|
||||
logoutHandler.setSessionCookieName("SESSION")
|
||||
return logoutHandler
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[oidc-backchannel-logout-session-registry]]
|
||||
=== Customizing the OIDC Provider Session Registry
|
||||
|
||||
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
|
||||
|
|
|
@ -136,6 +136,11 @@ Java::
|
|||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
OidcBackChannelLogoutHandler oidcLogoutHandler() {
|
||||
return new OidcBackChannelLogoutHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
@ -154,6 +159,11 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||
return OidcBackChannelLogoutHandler()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
|
@ -223,6 +233,87 @@ The overall flow for a Back-Channel logout is like this:
|
|||
Remember that Spring Security's OIDC support is multi-tenant.
|
||||
This means that it will only terminate sessions whose Client matches the `aud` claim in the Logout Token.
|
||||
|
||||
One notable part of this architecture's implementation is that it propagates the incoming back-channel request internally for each corresponding session.
|
||||
Initially, this may seem unnecessary.
|
||||
However, recall that the Servlet API does not give direct access to the `HttpSession` store.
|
||||
By making an internal logout call, the corresponding session can now be validated.
|
||||
|
||||
Additionally, forging a logout call internally allows for each set of ``LogoutHandler``s to be run against that session and corresponding `SecurityContext`.
|
||||
|
||||
=== Customizing the Session Logout Endpoint
|
||||
|
||||
With `OidcBackChannelLogoutHandler` published, the session logout endpoint is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`.
|
||||
|
||||
If `OidcBackChannelLogoutHandler` is not wired, then the URL is `+{baseUrl}+/logout/connect/back-channel/+{registrationId}+`, which is not recommended since it requires passing a CSRF token, which can be challenging depending on the kind of repository your application uses.
|
||||
|
||||
In the event that you need to customize the endpoint, you can provide the URL as follows:
|
||||
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source=java,role="primary"]
|
||||
----
|
||||
http
|
||||
// ...
|
||||
.oidcLogout((oidc) -> oidc
|
||||
.backChannel((backChannel) -> backChannel
|
||||
.logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
|
||||
)
|
||||
);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source=kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
oidcLogout {
|
||||
backChannel {
|
||||
logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
=== Customizing the Session Logout Cookie Name
|
||||
|
||||
By default, the session logout endpoint uses the `JSESSIONID` cookie to correlate the session to the corresponding `OidcSessionInformation`.
|
||||
|
||||
However, the default cookie name in Spring Session is `SESSION`.
|
||||
|
||||
You can configure Spring Session's cookie name in the DSL like so:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source=java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
|
||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
|
||||
logoutHandler.setSessionCookieName("SESSION");
|
||||
return logoutHandler;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source=kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
|
||||
val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
|
||||
logoutHandler.setSessionCookieName("SESSION")
|
||||
return logoutHandler
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[oidc-backchannel-logout-session-registry]]
|
||||
=== Customizing the OIDC Provider Session Registry
|
||||
|
||||
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
|
||||
|
|
Loading…
Reference in New Issue