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.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
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
|
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||||
|
@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private final OidcLogoutToken logoutToken;
|
private final OidcLogoutToken logoutToken;
|
||||||
|
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||||
*/
|
*/
|
||||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
|
||||||
super(Collections.emptyList());
|
super(Collections.emptyList());
|
||||||
this.logoutToken = logoutToken;
|
this.logoutToken = logoutToken;
|
||||||
|
this.clientRegistration = clientRegistration;
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||||
return this.logoutToken;
|
return this.logoutToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientRegistration getClientRegistration() {
|
||||||
|
return this.clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ final class OidcBackChannelLogoutAuthenticationProvider implements Authenticatio
|
||||||
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
|
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
|
||||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||||
.build();
|
.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 final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||||
|
|
||||||
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
private final LogoutHandler logoutHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an {@link OidcBackChannelLogoutFilter}
|
* Construct an {@link OidcBackChannelLogoutFilter}
|
||||||
|
@ -68,11 +68,13 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
|
||||||
* Logout Tokens
|
* Logout Tokens
|
||||||
*/
|
*/
|
||||||
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
|
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
|
||||||
AuthenticationManager authenticationManager) {
|
AuthenticationManager authenticationManager, LogoutHandler logoutHandler) {
|
||||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||||
|
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||||
this.authenticationConverter = authenticationConverter;
|
this.authenticationConverter = authenticationConverter;
|
||||||
this.authenticationManager = authenticationManager;
|
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");
|
"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.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.server.ServletServerHttpResponse;
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
import org.springframework.security.core.Authentication;
|
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.OidcSessionInformation;
|
||||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
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.authentication.logout.LogoutHandler;
|
||||||
import org.springframework.security.web.util.UrlUtils;
|
import org.springframework.security.web.util.UrlUtils;
|
||||||
import org.springframework.util.Assert;
|
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.RestClientException;
|
||||||
import org.springframework.web.client.RestOperations;
|
import org.springframework.web.client.RestOperations;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
@ -51,25 +52,29 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||||
* Back-Channel Logout Token and invalidates each one.
|
* Back-Channel Logout Token and invalidates each one.
|
||||||
*
|
*
|
||||||
* @author Josh Cummings
|
* @author Josh Cummings
|
||||||
* @since 6.2
|
* @since 6.4
|
||||||
* @see <a target="_blank" href=
|
* @see <a target="_blank" href=
|
||||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||||
* Spec</a>
|
* Spec</a>
|
||||||
*/
|
*/
|
||||||
final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
public final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass());
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
private final OidcSessionRegistry sessionRegistry;
|
||||||
|
|
||||||
private RestOperations restOperations = new RestTemplate();
|
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 String sessionCookieName = "JSESSIONID";
|
||||||
|
|
||||||
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
|
||||||
|
|
||||||
|
public OidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||||
|
@ -86,7 +91,7 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||||
for (OidcSessionInformation session : sessions) {
|
for (OidcSessionInformation session : sessions) {
|
||||||
totalCount++;
|
totalCount++;
|
||||||
try {
|
try {
|
||||||
eachLogout(request, session);
|
eachLogout(request, token, session);
|
||||||
invalidatedCount++;
|
invalidatedCount++;
|
||||||
}
|
}
|
||||||
catch (RestClientException ex) {
|
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();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||||
headers.add(credential.getKey(), credential.getValue());
|
headers.add(credential.getKey(), credential.getValue());
|
||||||
}
|
}
|
||||||
String logout = computeLogoutEndpoint(request);
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
HttpEntity<?> entity = new HttpEntity<>(null, headers);
|
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);
|
this.restOperations.postForEntity(logout, entity, Object.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
String computeLogoutEndpoint(HttpServletRequest request) {
|
String computeLogoutEndpoint(HttpServletRequest request, OidcBackChannelLogoutAuthentication token) {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
UriComponents uriComponents = UriComponentsBuilder
|
UriComponents uriComponents = UriComponentsBuilder
|
||||||
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||||
|
@ -137,6 +147,9 @@ final class OidcBackChannelLogoutHandler implements LogoutHandler {
|
||||||
int port = uriComponents.getPort();
|
int port = uriComponents.getPort();
|
||||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||||
|
|
||||||
|
String registrationId = token.getClientRegistration().getRegistrationId();
|
||||||
|
uriVariables.put("registrationId", registrationId);
|
||||||
|
|
||||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||||
.buildAndExpand(uriVariables)
|
.buildAndExpand(uriVariables)
|
||||||
.toUriString();
|
.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}
|
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||||
* since that is the default URI for
|
* since that is the default URI for
|
||||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||||
* @param logoutUri the URI to use
|
* @param logoutUri the URI to use
|
||||||
*/
|
*/
|
||||||
void setLogoutUri(String logoutUri) {
|
public void setLogoutUri(String logoutUri) {
|
||||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||||
this.logoutUri = logoutUri;
|
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.
|
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||||
* @param sessionCookieName the cookie name to use
|
* @param sessionCookieName the cookie name to use
|
||||||
*/
|
*/
|
||||||
void setSessionCookieName(String sessionCookieName) {
|
public void setSessionCookieName(String sessionCookieName) {
|
||||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||||
this.sessionCookieName = sessionCookieName;
|
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.Consumer;
|
||||||
import java.util.function.Function;
|
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.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.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.oidc.session.OidcSessionRegistry;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
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.LogoutHandler;
|
||||||
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.security.web.csrf.CsrfFilter;
|
import org.springframework.security.web.csrf.CsrfFilter;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
@ -140,8 +148,11 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogoutHandler logoutHandler(B http) {
|
private LogoutHandler logoutHandler(B http) {
|
||||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
OidcBackChannelLogoutHandler logoutHandler = getBeanOrNull(OidcBackChannelLogoutHandler.class);
|
||||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
if (logoutHandler != null) {
|
||||||
|
return logoutHandler;
|
||||||
|
}
|
||||||
|
logoutHandler = new OidcBackChannelLogoutHandler(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||||
return logoutHandler;
|
return logoutHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,21 +187,137 @@ public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
|
||||||
*/
|
*/
|
||||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||||
this.logoutHandler = (http) -> {
|
this.logoutHandler = (http) -> {
|
||||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(
|
||||||
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
|
||||||
logoutHandler.setLogoutUri(logoutUri);
|
logoutHandler.setLogoutUri(logoutUri);
|
||||||
return logoutHandler;
|
return logoutHandler;
|
||||||
};
|
};
|
||||||
return this;
|
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) {
|
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),
|
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
|
||||||
authenticationManager());
|
authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
|
||||||
filter.setLogoutHandler(this.logoutHandler.apply(http));
|
|
||||||
http.addFilterBefore(filter, CsrfFilter.class);
|
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.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
|
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
|
* An {@link org.springframework.security.core.Authentication} implementation that
|
||||||
|
@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private final OidcLogoutToken logoutToken;
|
private final OidcLogoutToken logoutToken;
|
||||||
|
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
* Construct an {@link OidcBackChannelLogoutAuthentication}
|
||||||
* @param logoutToken a deserialized, verified OIDC Logout Token
|
* @param logoutToken a deserialized, verified OIDC Logout Token
|
||||||
*/
|
*/
|
||||||
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
|
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
|
||||||
super(Collections.emptyList());
|
super(Collections.emptyList());
|
||||||
this.logoutToken = logoutToken;
|
this.logoutToken = logoutToken;
|
||||||
|
this.clientRegistration = clientRegistration;
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,4 +67,8 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
|
||||||
return this.logoutToken;
|
return this.logoutToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientRegistration getClientRegistration() {
|
||||||
|
return this.clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ final class OidcBackChannelLogoutReactiveAuthenticationManager implements Reacti
|
||||||
.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
|
.map((jwt) -> OidcLogoutToken.withTokenValue(logoutToken)
|
||||||
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
.claims((claims) -> claims.putAll(jwt.getClaims()))
|
||||||
.build())
|
.build())
|
||||||
.map(OidcBackChannelLogoutAuthentication::new);
|
.map((oidcLogoutToken) -> new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Jwt> decode(ClientRegistration registration, String token) {
|
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.OAuth2Error;
|
||||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
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.WebFilterExchange;
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
||||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||||
|
@ -60,7 +59,7 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||||
|
|
||||||
private final ReactiveAuthenticationManager authenticationManager;
|
private final ReactiveAuthenticationManager authenticationManager;
|
||||||
|
|
||||||
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
private final ServerLogoutHandler logoutHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
* Construct an {@link OidcBackChannelLogoutWebFilter}
|
||||||
|
@ -70,11 +69,13 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||||
* Logout Tokens
|
* Logout Tokens
|
||||||
*/
|
*/
|
||||||
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
|
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
|
||||||
ReactiveAuthenticationManager authenticationManager) {
|
ReactiveAuthenticationManager authenticationManager, ServerLogoutHandler logoutHandler) {
|
||||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
|
||||||
|
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
||||||
this.authenticationConverter = authenticationConverter;
|
this.authenticationConverter = authenticationConverter;
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
|
this.logoutHandler = logoutHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -124,14 +125,4 @@ class OidcBackChannelLogoutWebFilter implements WebFilter {
|
||||||
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
|
"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.ServerHttpRequest;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.security.core.Authentication;
|
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.server.session.ReactiveOidcSessionRegistry;
|
||||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
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.oauth2.core.OAuth2Error;
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
import org.springframework.security.web.server.WebFilterExchange;
|
||||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||||
import org.springframework.util.Assert;
|
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.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.util.UriComponents;
|
import org.springframework.web.util.UriComponents;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
@ -52,23 +52,27 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||||
* Back-Channel Logout Token and invalidates each one.
|
* Back-Channel Logout Token and invalidates each one.
|
||||||
*
|
*
|
||||||
* @author Josh Cummings
|
* @author Josh Cummings
|
||||||
* @since 6.2
|
* @since 6.4
|
||||||
* @see <a target="_blank" href=
|
* @see <a target="_blank" href=
|
||||||
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
|
||||||
* Spec</a>
|
* Spec</a>
|
||||||
*/
|
*/
|
||||||
final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
public final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass());
|
private final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
|
private final ReactiveOidcSessionRegistry sessionRegistry;
|
||||||
|
|
||||||
private WebClient web = WebClient.create();
|
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";
|
private String sessionCookieName = "SESSION";
|
||||||
|
|
||||||
|
public OidcBackChannelServerLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
|
||||||
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
|
||||||
|
@ -84,7 +88,7 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||||
AtomicInteger invalidatedCount = new AtomicInteger(0);
|
AtomicInteger invalidatedCount = new AtomicInteger(0);
|
||||||
return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
|
return this.sessionRegistry.removeSessionInformation(token.getPrincipal()).concatMap((session) -> {
|
||||||
totalCount.incrementAndGet();
|
totalCount.incrementAndGet();
|
||||||
return eachLogout(exchange, session).flatMap((response) -> {
|
return eachLogout(exchange, session, token).flatMap((response) -> {
|
||||||
invalidatedCount.incrementAndGet();
|
invalidatedCount.incrementAndGet();
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}).onErrorResume((ex) -> {
|
}).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();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
|
||||||
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
|
||||||
headers.add(credential.getKey(), credential.getValue());
|
headers.add(credential.getKey(), credential.getValue());
|
||||||
}
|
}
|
||||||
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest());
|
String logout = computeLogoutEndpoint(exchange.getExchange().getRequest(), token);
|
||||||
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
|
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
|
// @formatter:off
|
||||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
|
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI())
|
||||||
.replacePath(request.getPath().contextPath().value())
|
.replacePath(request.getPath().contextPath().value())
|
||||||
|
@ -137,6 +150,9 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||||
int port = uriComponents.getPort();
|
int port = uriComponents.getPort();
|
||||||
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
|
||||||
|
|
||||||
|
String registrationId = token.getClientRegistration().getRegistrationId();
|
||||||
|
uriVariables.put("registrationId", registrationId);
|
||||||
|
|
||||||
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
return UriComponentsBuilder.fromUriString(this.logoutUri)
|
||||||
.buildAndExpand(uriVariables)
|
.buildAndExpand(uriVariables)
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
@ -161,34 +177,13 @@ final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
|
||||||
return response.writeWith(Flux.just(buffer));
|
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}
|
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
|
||||||
* since that is the default URI for
|
* since that is the default URI for
|
||||||
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
|
||||||
* @param logoutUri the URI to use
|
* @param logoutUri the URI to use
|
||||||
*/
|
*/
|
||||||
void setLogoutUri(String logoutUri) {
|
public void setLogoutUri(String logoutUri) {
|
||||||
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
Assert.hasText(logoutUri, "logoutUri cannot be empty");
|
||||||
this.logoutUri = logoutUri;
|
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.
|
* Note that if you are using Spring Session, this likely needs to change to SESSION.
|
||||||
* @param sessionCookieName the cookie name to use
|
* @param sessionCookieName the cookie name to use
|
||||||
*/
|
*/
|
||||||
void setSessionCookieName(String sessionCookieName) {
|
public void setSessionCookieName(String sessionCookieName) {
|
||||||
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
|
||||||
this.sessionCookieName = sessionCookieName;
|
this.sessionCookieName = sessionCookieName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,6 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
|
||||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||||
import org.springframework.security.config.Customizer;
|
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.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.AuthorityUtils;
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
@ -5529,8 +5528,12 @@ public class ServerHttpSecurity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerLogoutHandler logoutHandler() {
|
private ServerLogoutHandler logoutHandler() {
|
||||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
OidcBackChannelServerLogoutHandler logoutHandler = getBeanOrNull(
|
||||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
OidcBackChannelServerLogoutHandler.class);
|
||||||
|
if (logoutHandler != null) {
|
||||||
|
return logoutHandler;
|
||||||
|
}
|
||||||
|
logoutHandler = new OidcBackChannelServerLogoutHandler(OidcLogoutSpec.this.getSessionRegistry());
|
||||||
return logoutHandler;
|
return logoutHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5548,9 +5551,9 @@ public class ServerHttpSecurity {
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* By default, the URI is set to
|
* By default, the URI is set to
|
||||||
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme
|
* {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, meaning
|
||||||
* and port of the original back-channel request is preserved, while the host
|
* that the scheme and port of the original back-channel request is preserved,
|
||||||
* and endpoint are changed.
|
* while the host and endpoint are changed.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* If you are using Spring Security for the logout endpoint, the path part of
|
* 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
|
* that the scheme, server name, or port in the {@code Host} header are
|
||||||
* different from how you would address the same server internally.
|
* different from how you would address the same server internally.
|
||||||
* @param logoutUri the URI to request logout on the back-channel
|
* @param logoutUri the URI to request logout on the back-channel
|
||||||
* @return the {@link OidcLogoutConfigurer.BackChannelLogoutConfigurer} for
|
* @return the {@link BackChannelLogoutConfigurer} for further customizations
|
||||||
* further customizations
|
|
||||||
* @since 6.2.4
|
* @since 6.2.4
|
||||||
*/
|
*/
|
||||||
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
|
||||||
this.logoutHandler = () -> {
|
this.logoutHandler = () -> {
|
||||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
|
||||||
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
|
OidcLogoutSpec.this.getSessionRegistry());
|
||||||
logoutHandler.setLogoutUri(logoutUri);
|
logoutHandler.setLogoutUri(logoutUri);
|
||||||
return logoutHandler;
|
return logoutHandler;
|
||||||
};
|
};
|
||||||
return this;
|
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) {
|
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(),
|
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
|
||||||
authenticationManager());
|
authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
|
||||||
filter.setLogoutHandler(this.logoutHandler.get());
|
|
||||||
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
|
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.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
|
@ -72,4 +73,5 @@ class OidcLogoutDsl {
|
||||||
backChannel?.also { oidcLogout.backChannel(backChannel) }
|
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.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
|
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
|
* 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
|
@OAuth2LoginSecurityMarker
|
||||||
class OidcBackChannelLogoutDsl {
|
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 {
|
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
|
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.
|
* 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
|
@ServerSecurityMarker
|
||||||
class ServerOidcBackChannelLogoutDsl {
|
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 {
|
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 {
|
* return http {
|
||||||
* oauth2Login { }
|
* oauth2Login { }
|
||||||
* oidcLogout {
|
* 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.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
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;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
public class OidcBackChannelLogoutHandlerTests {
|
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
|
// gh-14553
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
||||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||||
|
logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
|
||||||
request.setServerName("host.docker.internal");
|
request.setServerName("host.docker.internal");
|
||||||
request.setServerPort(8090);
|
request.setServerPort(8090);
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||||
request.setServerName("host.docker.internal");
|
request.setServerName("host.docker.internal");
|
||||||
request.setServerPort(8090);
|
request.setServerPort(8090);
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
// gh-14609
|
// gh-14609
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||||
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
|
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(this.sessionRegistry);
|
||||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/back-channel/logout");
|
||||||
request.setScheme("https");
|
request.setScheme("https");
|
||||||
request.setServerName("server-one.com");
|
request.setServerName("server-one.com");
|
||||||
request.setServerPort(80);
|
request.setServerPort(80);
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.JWKSet;
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
import com.nimbusds.jose.jwk.RSAKey;
|
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.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
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.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.BDDMockito.willThrow;
|
import static org.mockito.BDDMockito.willThrow;
|
||||||
|
@ -218,6 +220,40 @@ public class OidcLogoutConfigurerTests {
|
||||||
this.mvc.perform(get("/token/logout").session(one)).andExpect(status().isOk());
|
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
|
@Test
|
||||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
|
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
|
||||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Import(RegistrationConfig.class)
|
@Import(RegistrationConfig.class)
|
||||||
|
@ -559,12 +676,15 @@ public class OidcLogoutConfigurerTests {
|
||||||
|
|
||||||
private MockMvc mvc;
|
private MockMvc mvc;
|
||||||
|
|
||||||
|
private Consumer<RecordedRequest> assertion = (rr) -> { };
|
||||||
|
|
||||||
MockMvcDispatcher(ObjectProvider<MockMvc> mvc) {
|
MockMvcDispatcher(ObjectProvider<MockMvc> mvc) {
|
||||||
this.mvcProvider = mvc;
|
this.mvcProvider = mvc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
||||||
|
this.assertion.accept(request);
|
||||||
this.mvc = this.mvcProvider.getObject();
|
this.mvc = this.mvcProvider.getObject();
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
String path = request.getPath();
|
String path = request.getPath();
|
||||||
|
@ -601,6 +721,10 @@ public class OidcLogoutConfigurerTests {
|
||||||
this.session.put(session.getId(), session);
|
this.session.put(session.getId(), session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAssertion(Consumer<RecordedRequest> assertion) {
|
||||||
|
this.assertion = assertion;
|
||||||
|
}
|
||||||
|
|
||||||
private MockHttpSession session(RecordedRequest request) {
|
private MockHttpSession session(RecordedRequest request) {
|
||||||
String cookieHeaderValue = request.getHeader("Cookie");
|
String cookieHeaderValue = request.getHeader("Cookie");
|
||||||
if (cookieHeaderValue == null) {
|
if (cookieHeaderValue == null) {
|
||||||
|
@ -613,6 +737,10 @@ public class OidcLogoutConfigurerTests {
|
||||||
return this.session.computeIfAbsent(parts[1],
|
return this.session.computeIfAbsent(parts[1],
|
||||||
(k) -> new MockHttpSession(new MockServletContext(), 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();
|
return new MockHttpSession();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ package org.springframework.security.config.web.server;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
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;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ -27,36 +31,43 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
*/
|
*/
|
||||||
public class OidcBackChannelServerLogoutHandlerTests {
|
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
|
// gh-14553
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
public void computeLogoutEndpointWhenDifferentHostnameThenLocalhost() {
|
||||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||||
|
logoutHandler.setLogoutUri("{baseScheme}://localhost{basePort}/logout");
|
||||||
MockServerHttpRequest request = MockServerHttpRequest
|
MockServerHttpRequest request = MockServerHttpRequest
|
||||||
.get("https://host.docker.internal:8090/back-channel/logout")
|
.get("https://host.docker.internal:8090/back-channel/logout")
|
||||||
.build();
|
.build();
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("https://localhost:8090/logout");
|
assertThat(endpoint).startsWith("https://localhost:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
public void computeLogoutEndpointWhenUsingBaseUrlTemplateThenServerName() {
|
||||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||||
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
logoutHandler.setLogoutUri("{baseUrl}/logout");
|
||||||
MockServerHttpRequest request = MockServerHttpRequest
|
MockServerHttpRequest request = MockServerHttpRequest
|
||||||
.get("http://host.docker.internal:8090/back-channel/logout")
|
.get("http://host.docker.internal:8090/back-channel/logout")
|
||||||
.build();
|
.build();
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("http://host.docker.internal:8090/logout");
|
assertThat(endpoint).startsWith("http://host.docker.internal:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
// gh-14609
|
// gh-14609
|
||||||
@Test
|
@Test
|
||||||
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
public void computeLogoutEndpointWhenLogoutUriThenUses() {
|
||||||
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
|
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(this.sessionRegistry);
|
||||||
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
logoutHandler.setLogoutUri("http://localhost:8090/logout");
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("https://server-one.com/back-channel/logout").build();
|
||||||
String endpoint = logoutHandler.computeLogoutEndpoint(request);
|
String endpoint = logoutHandler.computeLogoutEndpoint(request, this.token);
|
||||||
assertThat(endpoint).isEqualTo("http://localhost:8090/logout");
|
assertThat(endpoint).startsWith("http://localhost:8090/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.JWKSet;
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
import com.nimbusds.jose.jwk.RSAKey;
|
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.WebSession;
|
||||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.BDDMockito.given;
|
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();
|
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
|
@Test
|
||||||
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() {
|
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() {
|
||||||
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
|
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
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@Import(RegistrationConfig.class)
|
@Import(RegistrationConfig.class)
|
||||||
|
@ -652,12 +775,15 @@ public class OidcLogoutSpecTests {
|
||||||
|
|
||||||
private WebTestClient web;
|
private WebTestClient web;
|
||||||
|
|
||||||
|
private Consumer<RecordedRequest> assertion = (rr) -> { };
|
||||||
|
|
||||||
WebTestClientDispatcher(ObjectProvider<WebTestClient> web) {
|
WebTestClientDispatcher(ObjectProvider<WebTestClient> web) {
|
||||||
this.webProvider = web;
|
this.webProvider = web;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
|
||||||
|
this.assertion.accept(request);
|
||||||
this.web = this.webProvider.getObject();
|
this.web = this.webProvider.getObject();
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
String path = request.getPath();
|
String path = request.getPath();
|
||||||
|
@ -700,6 +826,10 @@ public class OidcLogoutSpecTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAssertion(Consumer<RecordedRequest> assertion) {
|
||||||
|
this.assertion = assertion;
|
||||||
|
}
|
||||||
|
|
||||||
private String session(RecordedRequest request) {
|
private String session(RecordedRequest request) {
|
||||||
String cookieHeaderValue = request.getHeader("Cookie");
|
String cookieHeaderValue = request.getHeader("Cookie");
|
||||||
if (cookieHeaderValue == null) {
|
if (cookieHeaderValue == null) {
|
||||||
|
@ -711,6 +841,9 @@ public class OidcLogoutSpecTests {
|
||||||
if (SESSION_COOKIE_NAME.equals(parts[0])) {
|
if (SESSION_COOKIE_NAME.equals(parts[0])) {
|
||||||
return parts[1];
|
return parts[1];
|
||||||
}
|
}
|
||||||
|
if ("JSESSIONID".equals(parts[0])) {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,19 @@ import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
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.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.SpringTestContext
|
||||||
import org.springframework.security.config.test.SpringTestContextExtension
|
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.ClientRegistration
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository
|
||||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
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.MockMvc
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
|
|
||||||
|
@ -53,12 +59,23 @@ class OidcLogoutDslTests {
|
||||||
this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) {
|
this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) {
|
||||||
param("logout_token", "token")
|
param("logout_token", "token")
|
||||||
}.andExpect { status { isBadRequest() } }
|
}.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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
open class ClientRepositoryConfig {
|
open class ClientRepositoryConfig {
|
||||||
|
|
||||||
|
private val sessionRegistry = InMemoryOidcSessionRegistry()
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
|
@ -73,6 +90,13 @@ class OidcLogoutDslTests {
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||||
|
val logoutHandler = OidcBackChannelLogoutHandler(this.sessionRegistry)
|
||||||
|
logoutHandler.setSessionCookieName("SESSION");
|
||||||
|
return logoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun clientRegistration(): ClientRegistration {
|
open fun clientRegistration(): ClientRegistration {
|
||||||
return TestClientRegistrations.clientRegistration().build()
|
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.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
import org.springframework.security.config.test.SpringTestContext
|
import org.springframework.security.config.test.SpringTestContext
|
||||||
import org.springframework.security.config.test.SpringTestContextExtension
|
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.ClientRegistration
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository
|
||||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
|
||||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations
|
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.security.web.server.SecurityWebFilterChain
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.web.reactive.config.EnableWebFlux
|
import org.springframework.web.reactive.config.EnableWebFlux
|
||||||
import org.springframework.web.reactive.function.BodyInserters
|
import org.springframework.web.reactive.function.BodyInserters
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for [ServerOidcLogoutDsl]
|
* Tests for [ServerOidcLogoutDsl]
|
||||||
|
@ -63,6 +67,15 @@ class ServerOidcLogoutDslTests {
|
||||||
.body(BodyInserters.fromFormData("logout_token", "token"))
|
.body(BodyInserters.fromFormData("logout_token", "token"))
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isBadRequest
|
.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
|
@Configuration
|
||||||
|
@ -70,6 +83,8 @@ class ServerOidcLogoutDslTests {
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
open class ClientRepositoryConfig {
|
open class ClientRepositoryConfig {
|
||||||
|
|
||||||
|
private val sessionRegistry = InMemoryReactiveOidcSessionRegistry()
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
open fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
return http {
|
return http {
|
||||||
|
@ -83,6 +98,13 @@ class ServerOidcLogoutDslTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun oidcLogoutHandler(): OidcBackChannelServerLogoutHandler {
|
||||||
|
val logoutHandler = OidcBackChannelServerLogoutHandler(this.sessionRegistry)
|
||||||
|
logoutHandler.setSessionCookieName("SESSION");
|
||||||
|
return logoutHandler;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun clientRegistration(): ClientRegistration {
|
open fun clientRegistration(): ClientRegistration {
|
||||||
return TestClientRegistrations.clientRegistration().build()
|
return TestClientRegistrations.clientRegistration().build()
|
||||||
|
|
|
@ -137,6 +137,11 @@ Java::
|
||||||
+
|
+
|
||||||
[source,java,role="primary"]
|
[source,java,role="primary"]
|
||||||
----
|
----
|
||||||
|
@Bean
|
||||||
|
OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
|
||||||
|
return new OidcBackChannelServerLogoutHandler();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
|
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
|
@ -155,6 +160,11 @@ Kotlin::
|
||||||
+
|
+
|
||||||
[source,kotlin,role="secondary"]
|
[source,kotlin,role="secondary"]
|
||||||
----
|
----
|
||||||
|
@Bean
|
||||||
|
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||||
|
return OidcBackChannelLogoutHandler()
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
http {
|
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.
|
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.
|
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
|
=== Customizing the OIDC Provider Session Registry
|
||||||
|
|
||||||
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
|
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"]
|
[source,java,role="primary"]
|
||||||
----
|
----
|
||||||
|
@Bean
|
||||||
|
OidcBackChannelLogoutHandler oidcLogoutHandler() {
|
||||||
|
return new OidcBackChannelLogoutHandler();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
|
@ -154,6 +159,11 @@ Kotlin::
|
||||||
+
|
+
|
||||||
[source,kotlin,role="secondary"]
|
[source,kotlin,role="secondary"]
|
||||||
----
|
----
|
||||||
|
@Bean
|
||||||
|
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
|
||||||
|
return OidcBackChannelLogoutHandler()
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http {
|
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.
|
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.
|
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
|
=== Customizing the OIDC Provider Session Registry
|
||||||
|
|
||||||
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
|
By default, Spring Security stores in-memory all links between the OIDC Provider session and the Client session.
|
||||||
|
|
Loading…
Reference in New Issue