From 380c55a8a3e83eb325ef0382e5867c9c261327c3 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 3 Sep 2024 17:58:16 -0600 Subject: [PATCH] Add Kotlin Oidc SessionLogout Support Issue gh-14904 --- .../config/annotation/web/OidcLogoutDsl.kt | 2 + .../oauth2/login/OidcBackChannelLogoutDsl.kt | 37 ++++++++++++++++- .../web/oauth2/login/OidcSessionLogoutDsl.kt | 40 +++++++++++++++++++ .../server/ServerOidcBackChannelLogoutDsl.kt | 11 ++++- .../config/web/server/ServerOidcLogoutDsl.kt | 4 +- .../web/server/ServerOidcSessionLogoutDsl.kt | 36 +++++++++++++++++ .../annotation/web/OidcLogoutDslTests.kt | 17 +++++++- .../web/server/ServerOidcLogoutDslTests.kt | 12 ++++++ 8 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcSessionLogoutDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcSessionLogoutDsl.kt diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt index f9fdd7dc4dd..27532b4c015 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDsl.kt @@ -1,3 +1,4 @@ + /* * Copyright 2002-2023 the original author or authors. * @@ -72,4 +73,5 @@ class OidcLogoutDsl { backChannel?.also { oidcLogout.backChannel(backChannel) } } } + } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt index efac77a5667..a0d83447171 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcBackChannelLogoutDsl.kt @@ -28,7 +28,42 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli */ @OAuth2LoginSecurityMarker class OidcBackChannelLogoutDsl { + + private var sessionLogout: ((OidcLogoutConfigurer.BackChannelLogoutConfigurer.SessionLogoutConfigurer) -> 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 sessionLogout(sessionLogoutConfig: OidcSessionLogoutDsl.() -> Unit) { + this.sessionLogout = OidcSessionLogoutDsl().apply(sessionLogoutConfig).get() + } + internal fun get(): (OidcLogoutConfigurer.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } + return { backChannel -> + sessionLogout?.also { backChannel.sessionLogout(sessionLogout) } + } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcSessionLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcSessionLogoutDsl.kt new file mode 100644 index 00000000000..f547607a4c0 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/login/OidcSessionLogoutDsl.kt @@ -0,0 +1,40 @@ +/* + * 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 OidcSessionLogoutDsl { + var uri: String = "{baseUrl}/logout/connect/back-channel/{registrationId}" + var cookieName: String = "JSESSIONID" + + internal fun get(): (OidcLogoutConfigurer.BackChannelLogoutConfigurer.SessionLogoutConfigurer) -> Unit { + return { sessionLogout -> + uri?.also { sessionLogout.uri(uri) } + cookieName?.also { sessionLogout.cookieName(cookieName) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt index 5a245e5092e..0d4098c7394 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcBackChannelLogoutDsl.kt @@ -24,7 +24,16 @@ package org.springframework.security.config.web.server */ @ServerSecurityMarker class ServerOidcBackChannelLogoutDsl { + private var sessionLogout: ((ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer.SessionLogoutConfigurer) -> Unit)? = null + + fun sessionLogout(sessionLogoutConfig: ServerOidcSessionLogoutDsl.() -> Unit) { + this.sessionLogout = ServerOidcSessionLogoutDsl().apply(sessionLogoutConfig).get() + } + + internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer) -> Unit { - return { backChannel -> } + return { backChannel -> + sessionLogout?.also { backChannel.sessionLogout(sessionLogout) } + } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt index 503a5b0c843..7c27e3e0812 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDsl.kt @@ -47,7 +47,9 @@ class ServerOidcLogoutDsl { * return http { * oauth2Login { } * oidcLogout { - * backChannel { } + * backChannel { + * sessionLogout { } + * } * } * } * } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcSessionLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcSessionLogoutDsl.kt new file mode 100644 index 00000000000..f76de67adba --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOidcSessionLogoutDsl.kt @@ -0,0 +1,36 @@ +/* + * 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 ServerOidcSessionLogoutDsl { + var uri: String = "{baseUrl}/logout/connect/back-channel/{registrationId}" + var cookieName: String = "JSESSIONID" + + internal fun get(): (ServerHttpSecurity.OidcLogoutSpec.BackChannelLogoutConfigurer.SessionLogoutConfigurer) -> Unit { + return { sessionLogout -> + uri?.also { sessionLogout.uri(uri) } + cookieName?.also { sessionLogout.cookieName(cookieName) } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt index 468be4251bb..a9c4e11c482 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OidcLogoutDslTests.kt @@ -30,6 +30,8 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository import org.springframework.security.oauth2.client.registration.TestClientRegistrations import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.logout.LogoutHandler +import org.springframework.test.util.ReflectionTestUtils import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.post @@ -53,6 +55,15 @@ class OidcLogoutDslTests { this.mockMvc.post("/logout/connect/back-channel/" + clientRegistration.registrationId) { param("logout_token", "token") }.andExpect { status { isBadRequest() } } + val chain: SecurityFilterChain = this.spring.context.getBean(SecurityFilterChain::class.java) + for (filter in chain.filters) { + if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutFilter")) { + val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler + val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler + var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String + assert(cookieName.equals("SESSION")) + } + } } @Configuration @@ -64,7 +75,11 @@ class OidcLogoutDslTests { http { oauth2Login { } oidcLogout { - backChannel { } + backChannel { + sessionLogout { + cookieName = "SESSION" + } + } } authorizeHttpRequests { authorize(anyRequest, authenticated) diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt index a4b62b2dc0f..b5dc2780d8c 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOidcLogoutDslTests.kt @@ -29,10 +29,13 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository import org.springframework.security.oauth2.client.registration.TestClientRegistrations +import org.springframework.security.web.authentication.logout.LogoutHandler import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.util.ReflectionTestUtils import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.server.WebFilter /** * Tests for [ServerOidcLogoutDsl] @@ -63,6 +66,15 @@ class ServerOidcLogoutDslTests { .body(BodyInserters.fromFormData("logout_token", "token")) .exchange() .expectStatus().isBadRequest + val chain: SecurityWebFilterChain = this.spring.context.getBean(SecurityWebFilterChain::class.java) + chain.webFilters.doOnNext({ filter: WebFilter -> + if (filter.javaClass.simpleName.equals("OidcBackChannelLogoutWebFilter")) { + val logoutHandler = ReflectionTestUtils.getField(filter, "logoutHandler") as LogoutHandler + val backChannelLogoutHandler = ReflectionTestUtils.getField(logoutHandler, "left") as LogoutHandler + var cookieName = ReflectionTestUtils.getField(backChannelLogoutHandler, "sessionCookieName") as String + assert(cookieName.equals("SESSION")) + } + }) } @Configuration