Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -194,4 +194,13 @@ public void setSessionCookieName(String sessionCookieName) {
this.sessionCookieName = sessionCookieName;
}

/**
* Use this {@link RestOperations} to perform the per-session back-channel logout
* @param restOperations the {@link RestOperations} to use
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -255,6 +256,23 @@ void logoutWhenDifferentCookieNameThenUses() throws Exception {
this.mvc.perform(get("/token/logout").session(session)).andExpect(status().isUnauthorized());
}

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

@Test
void logoutWhenRemoteLogoutFailsThenReportsPartialLogout() throws Exception {
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, WithBrokenLogoutConfig.class).autowire();
Expand Down Expand Up @@ -485,6 +503,70 @@ void shutdown() throws IOException {

}

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

private final MockWebServer server = new MockWebServer();

@Bean
@Order(1)
SecurityFilterChain filters(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults())
.oidcLogout((oidc) -> oidc
.backChannel(Customizer.withDefaults())
);
// @formatter:on

return http.build();
}

@Bean
OidcSessionRegistry sessionRegistry() {
return new InMemoryOidcSessionRegistry();
}

@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);

RestTemplate restTemplate = new RestTemplate();

restTemplate.getInterceptors().add((request, body, execution) -> {
request.getHeaders().add("X-EXTREMLY-IMPORTANT-CUSTOM-HEADER","IMPORTANT");
return execution.execute(request, body);
});

logoutHandler.setRestOperations(restTemplate);
return logoutHandler;
}

@Bean
MockWebServer web(ObjectProvider<MockMvc> mvc) {
MockMvcDispatcher dispatcher = new MockMvcDispatcher(mvc);
dispatcher.setAssertion((rr) -> {
if(!rr.getRequestUrl().encodedPath().equals("/logout/connect/back-channel/registration-id")){
return;
}

String headerValue = rr.getHeaders().get("X-EXTREMLY-IMPORTANT-CUSTOM-HEADER");
assertThat(headerValue).isEqualTo("IMPORTANT");
});
this.server.setDispatcher(dispatcher);
return this.server;
}

@PreDestroy
void shutdown() throws IOException {
this.server.shutdown();
}

}

@Configuration
@EnableWebSecurity
@Import(RegistrationConfig.class)
Expand Down
37 changes: 37 additions & 0 deletions docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,43 @@ open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackCh
----
======


=== Customizing the Rest Operations

By default, a default `RestTemplate` is used to call the internal logout endpoint. You can set your own
if you e.g. need certain headers to be sent to your internal logout endpoint.

You can configure the `RestOperations` in the DSL like so:

[tabs]
======
Java::
+
[source=java,role="primary"]
----
@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(sessionRegistry);
RestTemplate customRestTemplate = new RestTemplate();
logoutHandler.setRestOperations(customRestTemplate);
return logoutHandler;
}
----

Kotlin::
+
[source=kotlin,role="secondary"]
----
@Bean
open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
val restTemplate = RestTemplate()
logoutHandler.setRestOperations(restTemplate)
return logoutHandler
}
----
======

[[oidc-backchannel-logout-session-registry]]
=== Customizing the OIDC Provider Session Registry

Expand Down