Add OIDC Back-Channel Logout Support
Closes gh-12570
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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">
|
||||
* @Bean
|
||||
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
* http
|
||||
* // ...
|
||||
* .oidcLogout((logout) -> 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
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> }
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
*/
|
||||
|
||||
@@ -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 -> }
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user