Add OIDC Back-Channel Logout Support

Closes gh-12570
This commit is contained in:
Josh Cummings
2023-01-17 17:25:16 -07:00
parent 1461c0f648
commit cb33fd7850
51 changed files with 5397 additions and 114 deletions

View File

@@ -70,6 +70,7 @@ import org.springframework.security.config.annotation.web.configurers.SessionMan
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
@@ -2835,6 +2836,16 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
return HttpSecurity.this;
}
public OidcLogoutConfigurer<HttpSecurity> oidcLogout() throws Exception {
return getOrApply(new OidcLogoutConfigurer<>());
}
public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcLogoutCustomizer)
throws Exception {
oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>()));
return HttpSecurity.this;
}
/**
* Configures OAuth 2.0 Client support.
* @return the {@link OAuth2ClientConfigurer} for further customizations

View File

@@ -296,7 +296,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
* @param sessionAuthenticationStrategy
* @return the {@link SessionManagementConfigurer} for further customizations
*/
SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
public SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
SessionAuthenticationStrategy sessionAuthenticationStrategy) {
this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
return this;

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}

View File

@@ -25,6 +25,8 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
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.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
@@ -112,4 +114,13 @@ final class OAuth2ClientConfigurerUtils {
return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
}
static <B extends HttpSecurityBuilder<B>> OidcSessionRegistry getOidcSessionRegistry(B builder) {
OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class);
if (sessionRegistry == null) {
sessionRegistry = new InMemoryOidcSessionRegistry();
builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry);
}
return sessionRegistry;
}
}

View File

@@ -22,9 +22,18 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
@@ -32,9 +41,14 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.AbstractSessionEvent;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionIdChangedEvent;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
@@ -42,6 +56,9 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -67,7 +84,10 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -124,6 +144,7 @@ import org.springframework.util.ReflectionUtils;
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
* login page will be made available</li>
* <li>{@link OidcSessionRegistry}</li>
* </ul>
*
* @author Joe Grandja
@@ -202,6 +223,18 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return this;
}
/**
* Sets the registry for managing the OIDC client-provider session link
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
* @return the {@link OAuth2LoginConfigurer} for further configuration
* @since 6.2
*/
public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
return this;
}
/**
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
* Server's Authorization Endpoint.
@@ -397,6 +430,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
authenticationFilter
.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
}
configureOidcSessionRegistry(http);
super.configure(http);
}
@@ -546,6 +580,29 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return AnyRequestMatcher.INSTANCE;
}
private void configureOidcSessionRegistry(B http) {
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
if (sessionConfigurer != null) {
OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy();
sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry);
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
listener.setSessionRegistry(sessionRegistry);
registerDelegateApplicationListener(listener);
}
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
DelegatingApplicationListener delegating = getBeanOrNull(
ResolvableType.forType(DelegatingApplicationListener.class));
if (delegating == null) {
return;
}
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
delegating.addListener(smartListener);
}
/**
* Configuration options for the Authorization Server's Authorization Endpoint.
*/
@@ -793,4 +850,83 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
}
private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
/**
* {@inheritDoc}
*/
@Override
public void onApplicationEvent(AbstractSessionEvent event) {
if (event instanceof SessionDestroyedEvent destroyed) {
this.logger.debug("Received SessionDestroyedEvent");
this.sessionRegistry.removeSessionInformation(destroyed.getId());
return;
}
if (event instanceof SessionIdChangedEvent changed) {
this.logger.debug("Received SessionIdChangedEvent");
OidcSessionInformation information = this.sessionRegistry.removeSessionInformation(changed.getOldSessionId());
if (information == null) {
this.logger.debug("Failed to register new session id since old session id was not found in registry");
return;
}
this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId()));
}
}
/**
* The registry where OIDC Provider sessions are linked to the Client session.
* Defaults to in-memory storage.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
}
private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy {
private final Log logger = LogFactory.getLog(getClass());
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
/**
* {@inheritDoc}
*/
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
return;
}
String sessionId = session.getId();
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken()) : Collections.emptyMap();
OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user);
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
}
this.sessionRegistry.saveSessionInformation(registration);
}
/**
* The registration for linking OIDC Provider Session information to the Client's
* session. Defaults to in-memory storage.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
/**
* An {@link org.springframework.security.core.Authentication} implementation that
* represents the result of authenticating an OIDC Logout token for the purposes of
* performing Back-Channel Logout.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
private final OidcLogoutToken logoutToken;
/**
* Construct an {@link OidcBackChannelLogoutAuthentication}
* @param logoutToken a deserialized, verified OIDC Logout Token
*/
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
super(Collections.emptyList());
this.logoutToken = logoutToken;
setAuthenticated(true);
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getPrincipal() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getCredentials() {
return this.logoutToken;
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
* deserializing it, verifying its signature, and validating its claims.
*
* <p>
* Intended to be included in a
* {@link org.springframework.security.authentication.ProviderManager}
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see org.springframework.security.authentication.ProviderManager
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
final class OidcBackChannelLogoutAuthenticationProvider implements AuthenticationProvider {
private JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
/**
* Construct an {@link OidcBackChannelLogoutAuthenticationProvider}
*/
OidcBackChannelLogoutAuthenticationProvider() {
OidcIdTokenDecoderFactory logoutTokenDecoderFactory = new OidcIdTokenDecoderFactory();
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
/**
* {@inheritDoc}
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
return null;
}
String logoutToken = token.getLogoutToken();
ClientRegistration registration = token.getClientRegistration();
Jwt jwt = decode(registration, logoutToken);
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
.claims((claims) -> claims.putAll(jwt.getClaims())).build();
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
}
/**
* {@inheritDoc}
*/
@Override
public boolean supports(Class<?> authentication) {
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
}
private Jwt decode(ClientRegistration registration, String token) {
JwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
try {
return logoutTokenDecoder.decode(token);
}
catch (BadJwtException failed) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
throw new OAuth2AuthenticationException(error, failed);
}
catch (Exception failed) {
throw new AuthenticationServiceException(failed.getMessage(), failed);
}
}
/**
* Use this {@link JwtDecoderFactory} to generate {@link JwtDecoder}s that correspond
* to the {@link ClientRegistration} associated with the OIDC logout token.
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
*/
void setLogoutTokenDecoderFactory(JwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A filter for the Client-side OIDC Back-Channel Logout endpoint
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(getClass());
private final AuthenticationConverter authenticationConverter;
private final AuthenticationManager authenticationManager;
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
/**
* Construct an {@link OidcBackChannelLogoutFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
* Logout Token authentication
* @param authenticationManager the {@link AuthenticationManager} for authenticating
* Logout Tokens
*/
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
AuthenticationManager authenticationManager) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationConverter = authenticationConverter;
this.authenticationManager = authenticationManager;
}
/**
* {@inheritDoc}
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Authentication token;
try {
token = this.authenticationConverter.convert(request);
}
catch (AuthenticationServiceException ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
throw ex;
}
catch (AuthenticationException ex) {
handleAuthenticationFailure(response, ex);
return;
}
if (token == null) {
chain.doFilter(request, response);
return;
}
Authentication authentication;
try {
authentication = this.authenticationManager.authenticate(token);
}
catch (AuthenticationServiceException ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
throw ex;
}
catch (AuthenticationException ex) {
handleAuthenticationFailure(response, ex);
return;
}
this.logoutHandler.logout(request, response, authentication);
}
private void handleAuthenticationFailure(HttpServletResponse response, Exception ex) throws IOException {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
this.errorHttpMessageConverter.write(oauth2Error(ex), null, new ServletServerHttpResponse(response));
}
private OAuth2Error oauth2Error(Exception ex) {
if (ex instanceof OAuth2AuthenticationException oauth2) {
return oauth2.getError();
}
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
"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;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@link LogoutHandler} that locates the sessions associated with a given OIDC
* Back-Channel Logout Token and invalidates each one.
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
final class OidcBackChannelLogoutHandler implements LogoutHandler {
private final Log logger = LogFactory.getLog(getClass());
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
private RestOperations restOperations = new RestTemplate();
private String logoutEndpointName = "/logout";
private String sessionCookieName = "JSESSIONID";
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
if (this.logger.isDebugEnabled()) {
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
}
return;
}
Iterable<OidcSessionInformation> sessions = this.sessionRegistry.removeSessionInformation(token.getPrincipal());
Collection<String> errors = new ArrayList<>();
int totalCount = 0;
int invalidatedCount = 0;
for (OidcSessionInformation session : sessions) {
totalCount++;
try {
eachLogout(request, session);
invalidatedCount++;
}
catch (RestClientException ex) {
this.logger.debug("Failed to invalidate session", ex);
errors.add(ex.getMessage());
this.sessionRegistry.saveSessionInformation(session);
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount, totalCount));
}
if (!errors.isEmpty()) {
handleLogoutFailure(response, oauth2Error(errors));
}
}
private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
headers.add(credential.getKey(), credential.getValue());
}
String url = request.getRequestURL().toString();
String logout = UriComponentsBuilder.fromHttpUrl(url).replacePath(this.logoutEndpointName).build()
.toUriString();
HttpEntity<?> entity = new HttpEntity<>(null, headers);
this.restOperations.postForEntity(logout, entity, Object.class);
}
private OAuth2Error oauth2Error(Collection<String> errors) {
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
try {
this.errorHttpMessageConverter.write(error, null, new ServletServerHttpResponse(response));
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
* this class uses
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
* sessions.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* Use this {@link RestOperations} to perform the per-session back-channel logout
* @param restOperations the {@link RestOperations} to use
*/
void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
/**
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
* since that is the default URI for
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
* @param logoutUri the URI to use
*/
void setLogoutUri(String logoutUri) {
Assert.hasText(logoutUri, "logoutUri cannot be empty");
this.logoutEndpointName = logoutUri;
}
/**
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
*
* <p>
* Note that if you are using Spring Session, this likely needs to change to SESSION.
* @param sessionCookieName the cookie name to use
*/
void setSessionCookieName(String sessionCookieName) {
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
this.sessionCookieName = sessionCookieName;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
* with the OIDC Back-Channel Logout Spec.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
* Token</a>
* @see <a target="blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
* Back-Channel Logout spec</a>
*/
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
private final String audience;
private final String issuer;
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Collection<OAuth2Error> errors = new ArrayList<>();
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
Map<String, Object> events = logoutClaims.getEvents();
if (events == null) {
errors.add(invalidLogoutToken("events claim must not be null"));
}
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
}
String issuer = logoutClaims.getIssuer().toExternalForm();
if (issuer == null) {
errors.add(invalidLogoutToken("iss claim must not be null"));
}
else if (!this.issuer.equals(issuer)) {
errors.add(invalidLogoutToken(
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
}
List<String> audience = logoutClaims.getAudience();
if (audience == null) {
errors.add(invalidLogoutToken("aud claim must not be null"));
}
else if (!audience.contains(this.audience)) {
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
}
Instant issuedAt = logoutClaims.getIssuedAt();
if (issuedAt == null) {
errors.add(invalidLogoutToken("iat claim must not be null"));
}
String jwtId = logoutClaims.getId();
if (jwtId == null) {
errors.add(invalidLogoutToken("jti claim must not be null"));
}
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
}
if (logoutClaims.getClaim("nonce") != null) {
errors.add(invalidLogoutToken("nonce claim must not be present"));
}
return OAuth2TokenValidatorResult.failure(errors);
}
private static OAuth2Error invalidLogoutToken(String description) {
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
* request
*
* @author Josh Cummings
* @since 6.2
*/
final class OidcLogoutAuthenticationConverter implements AuthenticationConverter {
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
private final Log logger = LogFactory.getLog(getClass());
private final ClientRegistrationRepository clientRegistrationRepository;
private RequestMatcher requestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_URI, "POST");
OidcLogoutAuthenticationConverter(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Authentication convert(HttpServletRequest request) {
RequestMatcher.MatchResult result = this.requestMatcher.matcher(request);
if (!result.isMatch()) {
return null;
}
String registrationId = result.getVariables().get("registrationId");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
this.logger.debug("Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
String logoutToken = request.getParameter("logout_token");
if (logoutToken == null) {
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
}
/**
* The logout endpoint. Defaults to
* {@code /logout/connect/back-channel/{registrationId}}.
* @param requestMatcher the {@link RequestMatcher} to use
*/
void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* An {@link org.springframework.security.core.Authentication} instance that represents a
* request to authenticate an OIDC Logout Token.
*
* @author Josh Cummings
* @since 6.2
*/
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
private final String logoutToken;
private final ClientRegistration clientRegistration;
/**
* Construct an {@link OidcLogoutAuthenticationToken}
* @param logoutToken a signed, serialized OIDC Logout token
* @param clientRegistration the {@link ClientRegistration client} associated with
* this token; this is usually derived from material in the logout HTTP request
*/
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
super(AuthorityUtils.NO_AUTHORITIES);
this.logoutToken = logoutToken;
this.clientRegistration = clientRegistration;
}
/**
* {@inheritDoc}
*/
@Override
public String getCredentials() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public String getPrincipal() {
return this.logoutToken;
}
/**
* Get the signed, serialized OIDC Logout token
* @return the logout token
*/
String getLogoutToken() {
return this.logoutToken;
}
/**
* Get the {@link ClientRegistration} associated with this logout token
* @return the {@link ClientRegistration}
*/
ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.util.function.Consumer;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.util.Assert;
/**
* An {@link AbstractHttpConfigurer} for OIDC Logout flows
*
* <p>
* OIDC Logout provides an application with the capability to have users log out by using
* their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider.
*
*
* <h2>Security Filters</h2>
*
* The following {@code Filter} is populated:
*
* <ul>
* <li>{@link OidcBackChannelLogoutFilter}</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link ClientRegistrationRepository}</li>
* </ul>
*
* @author Josh Cummings
* @since 6.2
* @see HttpSecurity#oidcLogout()
* @see OidcBackChannelLogoutFilter
* @see ClientRegistrationRepository
*/
public final class OidcLogoutConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<OidcLogoutConfigurer<B>, B> {
private BackChannelLogoutConfigurer backChannel;
/**
* Sets the repository of client registrations.
* @param clientRegistrationRepository the repository of client registrations
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> clientRegistrationRepository(
ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
return this;
}
/**
* Sets the registry for managing the OIDC client-provider session link
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
return this;
}
/**
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
* @return the {@link OidcLogoutConfigurer} for further configuration
*/
public OidcLogoutConfigurer<B> backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
if (this.backChannel == null) {
this.backChannel = new BackChannelLogoutConfigurer();
}
backChannelLogoutConfigurer.customize(this.backChannel);
return this;
}
@Deprecated(forRemoval = true, since = "6.2")
public B and() {
return getBuilder();
}
@Override
public void configure(B builder) throws Exception {
if (this.backChannel != null) {
this.backChannel.configure(builder);
}
}
/**
* A configurer for configuring OIDC Back-Channel Logout
*/
public final class BackChannelLogoutConfigurer {
private AuthenticationConverter authenticationConverter;
private final AuthenticationManager authenticationManager = new ProviderManager(
new OidcBackChannelLogoutAuthenticationProvider());
private LogoutHandler logoutHandler;
private AuthenticationConverter authenticationConverter(B http) {
if (this.authenticationConverter == null) {
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils
.getClientRegistrationRepository(http);
this.authenticationConverter = new OidcLogoutAuthenticationConverter(clientRegistrationRepository);
}
return this.authenticationConverter;
}
private AuthenticationManager authenticationManager() {
return this.authenticationManager;
}
private LogoutHandler logoutHandler(B http) {
if (this.logoutHandler == null) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
logoutHandler.setSessionRegistry(OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http));
this.logoutHandler = logoutHandler;
}
return this.logoutHandler;
}
void configure(B http) {
OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(authenticationConverter(http),
authenticationManager());
filter.setLogoutHandler(logoutHandler(http));
http.addFilterBefore(filter, CsrfFilter.class);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcBackChannelLogoutTokenValidator(clientRegistration));
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
/**
* An {@link org.springframework.security.core.Authentication} implementation that
* represents the result of authenticating an OIDC Logout token for the purposes of
* performing Back-Channel Logout.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
private final OidcLogoutToken logoutToken;
/**
* Construct an {@link OidcBackChannelLogoutAuthentication}
* @param logoutToken a deserialized, verified OIDC Logout Token
*/
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
super(Collections.emptyList());
this.logoutToken = logoutToken;
setAuthenticated(true);
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getPrincipal() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public OidcLogoutToken getCredentials() {
return this.logoutToken;
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.oidc.authentication.ReactiveOidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} that authenticates an OIDC Logout Token; namely
* deserializing it, verifying its signature, and validating its claims.
*
* <p>
* Intended to be included in a
* {@link org.springframework.security.authentication.ProviderManager}
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutAuthenticationToken
* @see org.springframework.security.authentication.ProviderManager
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel
* Logout</a>
*/
final class OidcBackChannelLogoutReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory;
/**
* Construct an {@link OidcBackChannelLogoutReactiveAuthenticationManager}
*/
OidcBackChannelLogoutReactiveAuthenticationManager() {
ReactiveOidcIdTokenDecoderFactory logoutTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
logoutTokenDecoderFactory.setJwtValidatorFactory(new DefaultOidcLogoutTokenValidatorFactory());
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
/**
* {@inheritDoc}
*/
@Override
public Mono<Authentication> authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof OidcLogoutAuthenticationToken token)) {
return Mono.empty();
}
String logoutToken = token.getLogoutToken();
ClientRegistration registration = token.getClientRegistration();
return decode(registration, logoutToken)
.map((jwt) -> OidcLogoutToken
.withTokenValue(logoutToken)
.claims((claims) -> claims.putAll(jwt.getClaims())).build()
)
.map(OidcBackChannelLogoutAuthentication::new);
}
private Mono<Jwt> decode(ClientRegistration registration, String token) {
ReactiveJwtDecoder logoutTokenDecoder = this.logoutTokenDecoderFactory.createDecoder(registration);
try {
return logoutTokenDecoder.decode(token);
}
catch (BadJwtException failed) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, failed.getMessage(),
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
return Mono.error(new OAuth2AuthenticationException(error, failed));
}
catch (Exception failed) {
return Mono.error(new AuthenticationServiceException(failed.getMessage(), failed));
}
}
/**
* Use this {@link ReactiveJwtDecoderFactory} to generate {@link JwtDecoder}s that
* correspond to the {@link ClientRegistration} associated with the OIDC logout token.
* @param logoutTokenDecoderFactory the {@link JwtDecoderFactory} to use
*/
void setLogoutTokenDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> logoutTokenDecoderFactory) {
Assert.notNull(logoutTokenDecoderFactory, "logoutTokenDecoderFactory cannot be null");
this.logoutTokenDecoderFactory = logoutTokenDecoderFactory;
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimAccessor;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
* with the OIDC Back-Channel Logout Spec.
*
* @author Josh Cummings
* @since 6.2
* @see OidcLogoutToken
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken">Logout
* Token</a>
* @see <a target="blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation">the OIDC
* Back-Channel Logout spec</a>
*/
final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<Jwt> {
private static final String LOGOUT_VALIDATION_URL = "https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation";
private static final String BACK_CHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
private final String audience;
private final String issuer;
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
this.audience = clientRegistration.getClientId();
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Collection<OAuth2Error> errors = new ArrayList<>();
LogoutTokenClaimAccessor logoutClaims = jwt::getClaims;
Map<String, Object> events = logoutClaims.getEvents();
if (events == null) {
errors.add(invalidLogoutToken("events claim must not be null"));
}
else if (events.get(BACK_CHANNEL_LOGOUT_EVENT) == null) {
errors.add(invalidLogoutToken("events claim map must contain \"" + BACK_CHANNEL_LOGOUT_EVENT + "\" key"));
}
String issuer = logoutClaims.getIssuer().toExternalForm();
if (issuer == null) {
errors.add(invalidLogoutToken("iss claim must not be null"));
}
else if (!this.issuer.equals(issuer)) {
errors.add(invalidLogoutToken(
"iss claim value must match `ClientRegistration#getProviderDetails#getIssuerUri`"));
}
List<String> audience = logoutClaims.getAudience();
if (audience == null) {
errors.add(invalidLogoutToken("aud claim must not be null"));
}
else if (!audience.contains(this.audience)) {
errors.add(invalidLogoutToken("aud claim value must include `ClientRegistration#getClientId`"));
}
Instant issuedAt = logoutClaims.getIssuedAt();
if (issuedAt == null) {
errors.add(invalidLogoutToken("iat claim must not be null"));
}
String jwtId = logoutClaims.getId();
if (jwtId == null) {
errors.add(invalidLogoutToken("jti claim must not be null"));
}
if (logoutClaims.getSubject() == null && logoutClaims.getSessionId() == null) {
errors.add(invalidLogoutToken("sub and sid claims must not both be null"));
}
if (logoutClaims.getClaim("nonce") != null) {
errors.add(invalidLogoutToken("nonce claim must not be present"));
}
return OAuth2TokenValidatorResult.failure(errors);
}
private static OAuth2Error invalidLogoutToken(String description) {
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, description, LOGOUT_VALIDATION_URL);
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* A filter for the Client-side OIDC Back-Channel Logout endpoint
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
class OidcBackChannelLogoutWebFilter implements WebFilter {
private final Log logger = LogFactory.getLog(getClass());
private final ServerAuthenticationConverter authenticationConverter;
private final ReactiveAuthenticationManager authenticationManager;
private ServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
/**
* Construct an {@link OidcBackChannelLogoutWebFilter}
* @param authenticationConverter the {@link AuthenticationConverter} for deriving
* Logout Token authentication
* @param authenticationManager the {@link AuthenticationManager} for authenticating
* Logout Tokens
*/
OidcBackChannelLogoutWebFilter(ServerAuthenticationConverter authenticationConverter,
ReactiveAuthenticationManager authenticationManager) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationConverter = authenticationConverter;
this.authenticationManager = authenticationManager;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.authenticationConverter.convert(exchange).onErrorResume(AuthenticationException.class, (ex) -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
}).switchIfEmpty(chain.filter(exchange).then(Mono.empty())).flatMap(this.authenticationManager::authenticate)
.onErrorResume(AuthenticationException.class, (ex) -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
if (ex instanceof AuthenticationServiceException) {
return Mono.error(ex);
}
return handleAuthenticationFailure(exchange.getResponse(), ex).then(Mono.empty());
}).flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return this.logoutHandler.logout(webFilterExchange, authentication);
});
}
private Mono<Void> handleAuthenticationFailure(ServerHttpResponse response, Exception ex) {
this.logger.debug("Failed to process OIDC Back-Channel Logout", ex);
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
OAuth2Error error = oauth2Error(ex);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri())
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
private OAuth2Error oauth2Error(Exception ex) {
if (ex instanceof OAuth2AuthenticationException oauth2) {
return oauth2.getError();
}
return new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, ex.getMessage(),
"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;
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@link ServerLogoutHandler} that locates the sessions associated with a given OIDC
* Back-Channel Logout Token and invalidates each one.
*
* @author Josh Cummings
* @since 6.2
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
* Spec</a>
*/
final class OidcBackChannelServerLogoutHandler implements ServerLogoutHandler {
private final Log logger = LogFactory.getLog(getClass());
private ReactiveOidcSessionRegistry sessionRegistry = new InMemoryReactiveOidcSessionRegistry();
private WebClient web = WebClient.create();
private String logoutEndpointName = "/logout";
private String sessionCookieName = "SESSION";
@Override
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
return Mono.defer(() -> {
if (this.logger.isDebugEnabled()) {
String message = "Did not perform OIDC Back-Channel Logout since authentication [%s] was of the wrong type";
this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
}
return Mono.empty();
});
}
AtomicInteger totalCount = new AtomicInteger(0);
AtomicInteger invalidatedCount = new AtomicInteger(0);
return this.sessionRegistry.removeSessionInformation(token.getPrincipal())
.concatMap((session) -> {
totalCount.incrementAndGet();
return eachLogout(exchange, session)
.flatMap((response) -> {
invalidatedCount.incrementAndGet();
return Mono.empty();
})
.onErrorResume((ex) -> {
this.logger.debug("Failed to invalidate session", ex);
return this.sessionRegistry.saveSessionInformation(session)
.then(Mono.just(ex.getMessage()));
});
}).collectList().flatMap((list) -> {
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Invalidated %d out of %d sessions", invalidatedCount.intValue(), totalCount.intValue()));
}
if (!list.isEmpty()) {
return handleLogoutFailure(exchange.getExchange().getResponse(), oauth2Error(list));
}
else {
return Mono.empty();
}
});
}
private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
headers.add(credential.getKey(), credential.getValue());
}
String url = exchange.getExchange().getRequest().getURI().toString();
String logout = UriComponentsBuilder.fromHttpUrl(url).replacePath(this.logoutEndpointName).build()
.toUriString();
return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
}
private OAuth2Error oauth2Error(Collection<?> errors) {
return new OAuth2Error("partial_logout", "not all sessions were terminated: " + errors,
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
}
private Mono<Void> handleLogoutFailure(ServerHttpResponse response, OAuth2Error error) {
response.setRawStatusCode(HttpServletResponse.SC_BAD_REQUEST);
byte[] bytes = String.format("""
{
"error_code": "%s",
"error_description": "%s",
"error_uri: "%s"
}
""", error.getErrorCode(), error.getDescription(), error.getUri())
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Flux.just(buffer));
}
/**
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
* this class uses
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
* sessions.
* @param sessionRegistry the {@link OidcSessionRegistry} to use
*/
void setSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* Use this {@link WebClient} to perform the per-session back-channel logout
* @param web the {@link WebClient} to use
*/
void setWebClient(WebClient web) {
Assert.notNull(web, "web cannot be null");
this.web = web;
}
/**
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
* since that is the default URI for
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
* @param logoutUri the URI to use
*/
void setLogoutUri(String logoutUri) {
Assert.hasText(logoutUri, "logoutUri cannot be empty");
this.logoutEndpointName = logoutUri;
}
/**
* Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
*
* <p>
* Note that if you are using Spring Session, this likely needs to change to SESSION.
* @param sessionCookieName the cookie name to use
*/
void setSessionCookieName(String sessionCookieName) {
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
this.sessionCookieName = sessionCookieName;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* An {@link org.springframework.security.core.Authentication} instance that represents a
* request to authenticate an OIDC Logout Token.
*
* @author Josh Cummings
* @since 6.2
*/
class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
private final String logoutToken;
private final ClientRegistration clientRegistration;
/**
* Construct an {@link OidcLogoutAuthenticationToken}
* @param logoutToken a signed, serialized OIDC Logout token
* @param clientRegistration the {@link ClientRegistration client} associated with
* this token; this is usually derived from material in the logout HTTP request
*/
OidcLogoutAuthenticationToken(String logoutToken, ClientRegistration clientRegistration) {
super(AuthorityUtils.NO_AUTHORITIES);
this.logoutToken = logoutToken;
this.clientRegistration = clientRegistration;
}
/**
* {@inheritDoc}
*/
@Override
public String getCredentials() {
return this.logoutToken;
}
/**
* {@inheritDoc}
*/
@Override
public String getPrincipal() {
return this.logoutToken;
}
/**
* Get the signed, serialized OIDC Logout token
* @return the logout token
*/
String getLogoutToken() {
return this.logoutToken;
}
/**
* Get the {@link ClientRegistration} associated with this logout token
* @return the {@link ClientRegistration}
*/
ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* An {@link AuthenticationConverter} that extracts the OIDC Logout Token authentication
* request
*
* @author Josh Cummings
* @since 6.2
*/
final class OidcLogoutServerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String DEFAULT_LOGOUT_URI = "/logout/connect/back-channel/{registrationId}";
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private ServerWebExchangeMatcher exchangeMatcher = new PathPatternParserServerWebExchangeMatcher(DEFAULT_LOGOUT_URI,
HttpMethod.POST);
OidcLogoutServerAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return this.exchangeMatcher.matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.flatMap((match) -> {
String registrationId = (String) match.getVariables().get("registrationId");
return this.clientRegistrationRepository.findByRegistrationId(registrationId)
.switchIfEmpty(Mono.error(() -> {
this.logger.debug(
"Did not process OIDC Back-Channel Logout since no ClientRegistration was found");
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}));
}).flatMap((clientRegistration) -> exchange.getFormData().map((data) -> {
String logoutToken = data.getFirst("logout_token");
return new OidcLogoutAuthenticationToken(logoutToken, clientRegistration);
}).switchIfEmpty(Mono.error(() -> {
this.logger.debug("Failed to process OIDC Back-Channel Logout since no logout token was found");
return new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
})));
}
/**
* The logout endpoint. Defaults to
* {@code /logout/connect/back-channel/{registrationId}}.
* @param exchangeMatcher the {@link ServerWebExchangeMatcher} to use
*/
void setExchangeMatcher(ServerWebExchangeMatcher exchangeMatcher) {
Assert.notNull(exchangeMatcher, "exchangeMatcher cannot be null");
this.exchangeMatcher = exchangeMatcher;
}
}

View File

@@ -21,6 +21,7 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -28,10 +29,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@@ -67,6 +71,9 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -113,6 +120,7 @@ import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
@@ -147,6 +155,7 @@ import org.springframework.security.web.server.context.SecurityContextServerWebE
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler;
@@ -193,8 +202,10 @@ import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import org.springframework.web.util.pattern.PathPatternParser;
/**
@@ -295,6 +306,8 @@ public class ServerHttpSecurity {
private OAuth2ClientSpec client;
private OidcLogoutSpec oidcLogout;
private LogoutSpec logout = new LogoutSpec();
private LoginPageSpec loginPage = new LoginPageSpec();
@@ -1093,6 +1106,33 @@ public class ServerHttpSecurity {
return this;
}
/**
* Configures OIDC Connect 1.0 Logout support.
*
* <pre class="code">
* &#064;Bean
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
* http
* // ...
* .oidcLogout((logout) -&gt; logout
* .backChannel(Customizer.withDefaults())
* );
* return http.build();
* }
* </pre>
* @param oidcLogoutCustomizer the {@link Customizer} to provide more options for the
* {@link OidcLogoutSpec}
* @return the {@link ServerHttpSecurity} to customize
* @since 6.2
*/
public ServerHttpSecurity oidcLogout(Customizer<OidcLogoutSpec> oidcLogoutCustomizer) {
if (this.oidcLogout == null) {
this.oidcLogout = new OidcLogoutSpec();
}
oidcLogoutCustomizer.customize(this.oidcLogout);
return this;
}
/**
* Configures HTTP Response Headers. The default headers are:
*
@@ -1537,6 +1577,9 @@ public class ServerHttpSecurity {
if (this.resourceServer != null) {
this.resourceServer.configure(this);
}
if (this.oidcLogout != null) {
this.oidcLogout.configure(this);
}
if (this.client != null) {
this.client.configure(this);
}
@@ -3689,6 +3732,8 @@ public class ServerHttpSecurity {
private ServerWebExchangeMatcher authenticationMatcher;
private ReactiveOidcSessionRegistry oidcSessionRegistry;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
private ServerAuthenticationFailureHandler authenticationFailureHandler;
@@ -3720,6 +3765,20 @@ public class ServerHttpSecurity {
return this;
}
/**
* Configures the {@link ReactiveOidcSessionRegistry} to use when logins use OIDC.
* Default is to look the value up as a Bean, or else use an
* {@link InMemoryReactiveOidcSessionRegistry}.
* @param oidcSessionRegistry the registry to use
* @return the {@link OidcLogoutSpec} to customize
* @since 6.2
*/
public OAuth2LoginSpec oidcSessionRegistry(ReactiveOidcSessionRegistry oidcSessionRegistry) {
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
this.oidcSessionRegistry = oidcSessionRegistry;
return this;
}
/**
* The {@link ServerAuthenticationSuccessHandler} used after authentication
* success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
@@ -3913,8 +3972,9 @@ public class ServerHttpSecurity {
oauthRedirectFilter.setRequestCache(http.requestCache.requestCache);
ReactiveAuthenticationManager manager = getAuthenticationManager();
AuthenticationWebFilter authenticationFilter = new OAuth2LoginAuthenticationWebFilter(manager,
authorizedClientRepository);
ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
AuthenticationWebFilter authenticationFilter = new OidcSessionRegistryAuthenticationWebFilter(manager,
authorizedClientRepository, sessionRegistry);
authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
authenticationFilter
.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
@@ -3923,6 +3983,8 @@ public class ServerHttpSecurity {
authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
setDefaultEntryPoints(http);
http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
}
@@ -3967,6 +4029,16 @@ public class ServerHttpSecurity {
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
if (this.oidcSessionRegistry == null) {
this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
}
if (this.oidcSessionRegistry == null) {
this.oidcSessionRegistry = new InMemoryReactiveOidcSessionRegistry();
}
return this.oidcSessionRegistry;
}
private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
if (this.authenticationSuccessHandler == null) {
RedirectServerAuthenticationSuccessHandler handler = new RedirectServerAuthenticationSuccessHandler();
@@ -4083,6 +4155,154 @@ public class ServerHttpSecurity {
return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
}
private static final class OidcSessionRegistryWebFilter implements WebFilter {
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
OidcSessionRegistryWebFilter(ReactiveOidcSessionRegistry oidcSessionRegistry) {
this.oidcSessionRegistry = oidcSessionRegistry;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(new OidcSessionRegistryServerWebExchange(exchange));
}
private final class OidcSessionRegistryServerWebExchange extends ServerWebExchangeDecorator {
private final Mono<WebSession> sessionMono;
protected OidcSessionRegistryServerWebExchange(ServerWebExchange delegate) {
super(delegate);
this.sessionMono = delegate.getSession().map(OidcSessionRegistryWebSession::new);
}
@Override
public Mono<WebSession> getSession() {
return this.sessionMono;
}
private final class OidcSessionRegistryWebSession implements WebSession {
private final WebSession session;
OidcSessionRegistryWebSession(WebSession session) {
this.session = session;
}
@Override
public String getId() {
return this.session.getId();
}
@Override
public Map<String, Object> getAttributes() {
return this.session.getAttributes();
}
@Override
public void start() {
this.session.start();
}
@Override
public boolean isStarted() {
return this.session.isStarted();
}
@Override
public Mono<Void> changeSessionId() {
String currentId = this.session.getId();
return this.session.changeSessionId()
.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.removeSessionInformation(currentId).flatMap((information) -> {
information = information.withSessionId(this.session.getId());
return OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.saveSessionInformation(information);
})));
}
@Override
public Mono<Void> invalidate() {
String currentId = this.session.getId();
return this.session.invalidate()
.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
.removeSessionInformation(currentId).then(Mono.empty())));
}
@Override
public Mono<Void> save() {
return this.session.save();
}
@Override
public boolean isExpired() {
return this.session.isExpired();
}
@Override
public Instant getCreationTime() {
return this.session.getCreationTime();
}
@Override
public Instant getLastAccessTime() {
return this.session.getLastAccessTime();
}
@Override
public void setMaxIdleTime(Duration maxIdleTime) {
this.session.setMaxIdleTime(maxIdleTime);
}
@Override
public Duration getMaxIdleTime() {
return this.session.getMaxIdleTime();
}
}
}
}
private static final class OidcSessionRegistryAuthenticationWebFilter
extends OAuth2LoginAuthenticationWebFilter {
private final Log logger = LogFactory.getLog(getClass());
private final ReactiveOidcSessionRegistry oidcSessionRegistry;
OidcSessionRegistryAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
ReactiveOidcSessionRegistry oidcSessionRegistry) {
super(authenticationManager, authorizedClientRepository);
this.oidcSessionRegistry = oidcSessionRegistry;
}
@Override
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
return super.onAuthenticationSuccess(authentication, webFilterExchange);
}
return webFilterExchange.getExchange().getSession()
.doOnNext((session) -> {
if (this.logger.isTraceEnabled()) {
this.logger.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
}
})
.flatMap((session) -> {
Mono<CsrfToken> csrfToken = webFilterExchange.getExchange().getAttribute(CsrfToken.class.getName());
return (csrfToken != null) ?
csrfToken.map((token) -> new OidcSessionInformation(session.getId(), Map.of(token.getHeaderName(), token.getToken()), user)) :
Mono.just(new OidcSessionInformation(session.getId(), Map.of(), user));
})
.flatMap(this.oidcSessionRegistry::saveSessionInformation)
.then(super.onAuthenticationSuccess(authentication, webFilterExchange));
}
}
}
public final class OAuth2ClientSpec {
@@ -4755,6 +4975,129 @@ public class ServerHttpSecurity {
}
/**
* Configures OIDC 1.0 Logout support
*
* @author Josh Cummings
* @since 6.2
*/
public final class OidcLogoutSpec {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
private ReactiveOidcSessionRegistry sessionRegistry;
private BackChannelLogoutConfigurer backChannel;
/**
* Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
* the value up as a Bean.
* @param clientRegistrationRepository the repository to use
* @return the {@link OidcLogoutSpec} to customize
*/
public OidcLogoutSpec clientRegistrationRepository(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
return this;
}
/**
* Configures the {@link ReactiveOidcSessionRegistry}. Default is to use the value
* from {@link OAuth2LoginSpec#oidcSessionRegistry}, then look the value up as a
* Bean, or else use an {@link InMemoryReactiveOidcSessionRegistry}.
* @param sessionRegistry the registry to use
* @return the {@link OidcLogoutSpec} to customize
*/
public OidcLogoutSpec oidcSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
return this;
}
/**
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
* @return the {@link OidcLogoutSpec} for further configuration
*/
public OidcLogoutSpec backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
if (this.backChannel == null) {
this.backChannel = new OidcLogoutSpec.BackChannelLogoutConfigurer();
}
backChannelLogoutConfigurer.customize(this.backChannel);
return this;
}
@Deprecated(forRemoval = true, since = "6.2")
public ServerHttpSecurity and() {
return ServerHttpSecurity.this;
}
void configure(ServerHttpSecurity http) {
if (this.backChannel != null) {
this.backChannel.configure(http);
}
}
private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
if (this.clientRegistrationRepository == null) {
this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class);
}
return this.clientRegistrationRepository;
}
private ReactiveOidcSessionRegistry getSessionRegistry() {
if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) {
return new InMemoryReactiveOidcSessionRegistry();
}
if (this.sessionRegistry == null) {
return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry;
}
return this.sessionRegistry;
}
/**
* A configurer for configuring OIDC Back-Channel Logout
*/
public final class BackChannelLogoutConfigurer {
private ServerAuthenticationConverter authenticationConverter;
private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();
private ServerLogoutHandler logoutHandler;
private ServerAuthenticationConverter authenticationConverter() {
if (this.authenticationConverter == null) {
this.authenticationConverter = new OidcLogoutServerAuthenticationConverter(
OidcLogoutSpec.this.getClientRegistrationRepository());
}
return this.authenticationConverter;
}
private ReactiveAuthenticationManager authenticationManager() {
return this.authenticationManager;
}
private ServerLogoutHandler logoutHandler() {
if (this.logoutHandler == null) {
OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
logoutHandler.setSessionRegistry(OidcLogoutSpec.this.getSessionRegistry());
this.logoutHandler = logoutHandler;
}
return this.logoutHandler;
}
void configure(ServerHttpSecurity http) {
OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
authenticationManager());
filter.setLogoutHandler(logoutHandler());
http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
}
}
}
/**
* Configures anonymous authentication
*

View File

@@ -868,6 +868,38 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
}
/**
* Configures OIDC 1.0 logout support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* return http.build()
* }
* }
* ```
*
* @param oidcLogoutConfiguration custom configuration to configure the
* OIDC 1.0 logout support
* @see [OidcLogoutDsl]
*/
fun oidcLogout(oidcLogoutConfiguration: OidcLogoutDsl.() -> Unit) {
val oidcLogoutCustomizer = OidcLogoutDsl().apply(oidcLogoutConfiguration).get()
this.http.oidcLogout(oidcLogoutCustomizer)
}
/**
* Configures Remember Me authentication.
*

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web
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.oauth2.login.OidcBackChannelLogoutDsl
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
/**
* A Kotlin DSL to configure [HttpSecurity] OAuth 1.0 Logout using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@SecurityMarker
class OidcLogoutDsl {
var clientRegistrationRepository: ClientRegistrationRepository? = null
var oidcSessionRegistry: OidcSessionRegistry? = null
private var backChannel: ((OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit)? = null
/**
* Configures the OIDC 1.0 Back-Channel endpoint.
*
* Example:
*
* ```
* @Configuration
* @EnableWebSecurity
* class SecurityConfig {
*
* @Bean
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* return http.build()
* }
* }
* ```
*
* @param backChannelConfig custom configurations to configure the back-channel endpoint
* @see [OidcBackChannelLogoutDsl]
*/
fun backChannel(backChannelConfig: OidcBackChannelLogoutDsl.() -> Unit) {
this.backChannel = OidcBackChannelLogoutDsl().apply(backChannelConfig).get()
}
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>) -> Unit {
return { oidcLogout ->
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
backChannel?.also { oidcLogout.backChannel(backChannel) }
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.oauth2.login
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer
/**
* A Kotlin DSL to configure the OIDC 1.0 Back-Channel configuration using
* idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@OAuth2LoginSecurityMarker
class OidcBackChannelLogoutDsl {
internal fun get(): (OidcLogoutConfigurer<HttpSecurity>.BackChannelLogoutConfigurer) -> Unit {
return { backChannel -> }
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -650,6 +650,38 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer)
}
/**
* Configures logout support using an OpenID Connect 1.0 Provider.
* A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or
* configured via [ServerOidcLogoutDsl.clientRegistrationRepository].
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* class SecurityConfig {
*
* @Bean
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* }
* }
* ```
*
* @param oidcLogoutConfiguration custom configuration to configure the OIDC 1.0 Logout
* @see [ServerOidcLogoutDsl]
*/
fun oidcLogout(oidcLogoutConfiguration: ServerOidcLogoutDsl.() -> Unit) {
val oidcLogoutCustomizer = ServerOidcLogoutDsl().apply(oidcLogoutConfiguration).get()
this.http.oidcLogout(oidcLogoutCustomizer)
}
/**
* Apply all configurations to the provided [ServerHttpSecurity]
*/

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
/**
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 Back-Channel Logout support using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@ServerSecurityMarker
class ServerOidcBackChannelLogoutDsl {
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit {
return { backChannel -> }
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.web.server
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
/**
* A Kotlin DSL to configure [ServerHttpSecurity] OIDC 1.0 login using idiomatic Kotlin code.
*
* @author Josh Cummings
* @since 6.2
*/
@ServerSecurityMarker
class ServerOidcLogoutDsl {
var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null
var oidcSessionRegistry: ReactiveOidcSessionRegistry? = null
private var backChannel: ((ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit)? = null
/**
* Enables OIDC 1.0 Back-Channel Logout support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* class SecurityConfig {
*
* @Bean
* fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* oauth2Login { }
* oidcLogout {
* backChannel { }
* }
* }
* }
* }
* ```
*
* @param backChannelConfig custom configurations to configure OIDC 1.0 Back-Channel Logout support
* @see [ServerOidcBackChannelLogoutDsl]
*/
fun backChannel(backChannelConfig: ServerOidcBackChannelLogoutDsl.() -> Unit) {
this.backChannel = ServerOidcBackChannelLogoutDsl().apply(backChannelConfig).get()
}
internal fun get(): (ServerHttpSecurity.OidcLogoutSpec) -> Unit {
return { oidcLogout ->
clientRegistrationRepository?.also { oidcLogout.clientRegistrationRepository(clientRegistrationRepository) }
oidcSessionRegistry?.also { oidcLogout.oidcSessionRegistry(oidcSessionRegistry) }
backChannel?.also { oidcLogout.backChannel(backChannel) }
}
}
}