Skip to content

Simplify OIDC Back-Channel Logout DSL (Closes gh-15817) #16698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,8 @@

package org.springframework.security.config.annotation.web.builders;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave the import statements as-is.

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
Expand All @@ -48,29 +38,8 @@
import org.springframework.security.config.annotation.web.RequestMatcherFactory;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.*;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer;
import org.springframework.security.config.annotation.web.configurers.JeeConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer;
import org.springframework.security.config.annotation.web.configurers.PortMapperConfigurer;
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer;
import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer;
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;
Expand Down Expand Up @@ -103,6 +72,11 @@
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* A {@link HttpSecurity} is similar to Spring Security's XML <http> element in the
* namespace configuration. It allows configuring web based security for specific http
Expand Down Expand Up @@ -2870,6 +2844,14 @@ public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oi
return HttpSecurity.this;
}

public HttpSecurity oidcBackChannelLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcBackChannelLogoutCustomizer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add JavaDoc so folks can read how to use the method. Please make sure the JavaDoc includes @since 6.5.

throws Exception {
oidcBackChannelLogoutCustomizer.customize(
getOrApply(new OidcLogoutConfigurer<>()).backChannel(Customizer.withDefaults())
);
return this;
}

/**
* Configures OAuth 2.0 Client support.
* @return the {@link OAuth2ClientConfigurer} for further customizations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@

package org.springframework.security.config.annotation.web.configurers.oauth2.client;

import java.util.function.Consumer;
import java.util.function.Function;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
Expand All @@ -40,6 +36,9 @@
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.util.Assert;

import java.util.function.Consumer;
import java.util.function.Function;

/**
* An {@link AbstractHttpConfigurer} for OIDC Logout flows
*
Expand Down Expand Up @@ -102,7 +101,10 @@ public OidcLogoutConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessi
/**
* Configure OIDC Back-Channel Logout using the provided {@link Consumer}
* @return the {@link OidcLogoutConfigurer} for further configuration
* @deprecated For removal in a future release. Use
* {@link HttpSecurity#oidcBackChannelLogout(Customizer)} instead.
*/
@Deprecated(since = "6.2", forRemoval = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change the since value to 6.5

public OidcLogoutConfigurer<B> backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
if (this.backChannel == null) {
this.backChannel = new BackChannelLogoutConfigurer();
Expand Down Expand Up @@ -157,35 +159,6 @@ private LogoutHandler logoutHandler(B http) {
return logoutHandler;
}

/**
* Use this endpoint when invoking a back-channel logout.
*
* <p>
* The resulting {@link LogoutHandler} will {@code POST} the session cookie and
* CSRF token to this endpoint to invalidate the corresponding end-user session.
*
* <p>
* Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
* {@code {basePort}}.
*
* <p>
* By default, the URI is set to
* {@code {baseScheme}://localhost{basePort}/logout}, meaning that the scheme and
* port of the original back-channel request is preserved, while the host and
* endpoint are changed.
*
* <p>
* If you are using Spring Security for the logout endpoint, the path part of this
* URI should match the value configured there.
*
* <p>
* Otherwise, this is handy in the event that your server configuration means that
* the scheme, server name, or port in the {@code Host} header are different from
* how you would address the same server internally.
* @param logoutUri the URI to request logout on the back-channel
* @return the {@link BackChannelLogoutConfigurer} for further customizations
* @since 6.2.4
*/
public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
this.logoutHandler = (http) -> {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,6 @@

package org.springframework.security.config.annotation.web.configurers.oauth2.client;

import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
Expand All @@ -44,7 +34,6 @@
import org.htmlunit.util.UrlUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -75,11 +64,7 @@
import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutHandler;
Expand All @@ -92,14 +77,22 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
Expand Down Expand Up @@ -312,6 +305,24 @@ void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws E
.param("logout_token", logoutToken)));
}

@Test
void oidcBackChannelLogoutWhenDefaultsThenRemotelyInvalidatesSessions() throws Exception {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithOidcBackChannelDslConfig.class)
.autowire();
String registrationId = this.clientRegistration.getRegistrationId();
MockHttpSession session = login();
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
.param("logout_token", logoutToken))
.andExpect(status().isOk());
this.mvc.perform(get("/token/logout").session(session))
.andExpect(status().isUnauthorized());
}

private MockHttpSession login() throws Exception {
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
Expand Down Expand Up @@ -739,6 +750,23 @@ void shutdown() throws IOException {

}

@Configuration
@EnableWebSecurity
@Import(RegistrationConfig.class)
static class WithOidcBackChannelDslConfig {

@Bean
@Order(1)
SecurityFilterChain filters(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcBackChannelLogout(Customizer.withDefaults());
return http.build();
}

}

private static class MockMvcDispatcher extends Dispatcher {

private final Map<String, MockHttpSession> session = new ConcurrentHashMap<>();
Expand Down