diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt
index 60342d2af8..f13ce656b1 100644
--- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -707,6 +707,69 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
this.http.saml2Login(saml2LoginCustomizer)
}
+ /**
+ * Configures logout support for a SAML 2.0 Service Provider.
+ *
+ *
+ * Implements the Single Logout Profile, using POST and REDIRECT bindings, as
+ * documented in the
+ * SAML V2.0
+ * Core, Profiles and Bindings specifications.
+ *
+ *
+ * As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting
+ * Party to send a logout request to. The representation of the relying party and the
+ * asserting party is contained within [RelyingPartyRegistration].
+ *
+ *
+ * [RelyingPartyRegistration] (s) are composed within a
+ * [RelyingPartyRegistrationRepository], which is required and must be
+ * registered with the [ApplicationContext] or configured via
+ * [HttpSecurityDsl.saml2Login].
+ *
+ *
+ * The default configuration provides an auto-generated logout endpoint at
+ * `/logout` and redirects to `/login?logout` when
+ * logout completes.
+ *
+ *
+ *
+ *
Example Configuration
+ *
+ * The following example shows the minimal configuration required, using a
+ * hypothetical asserting party.
+ *
+ * Example:
+ *
+ * ```
+ * @Configuration
+ * @EnableWebSecurity
+ * class SecurityConfig {
+ *
+ * @Bean
+ * fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ * http {
+ * saml2Login {
+ * relyingPartyRegistration = getSaml2RelyingPartyRegistration()
+ * }
+ * saml2Logout { }
+ * }
+ * return http.build()
+ * }
+ * }
+ * ```
+ *
+ *
+ * @param saml2LogoutConfiguration custom configuration to configure the
+ * SAML 2.0 service provider
+ * @since 6.3
+ * @see [Saml2LogoutDsl]
+ */
+ fun saml2Logout(saml2LogoutConfiguration: Saml2LogoutDsl.() -> Unit) {
+ val saml2LogoutCustomizer = Saml2LogoutDsl().apply(saml2LogoutConfiguration).get()
+ this.http.saml2Logout(saml2LogoutCustomizer)
+ }
+
/**
* Configures a SAML 2.0 relying party metadata endpoint.
*
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDsl.kt
new file mode 100644
index 0000000000..3a5b090b0e
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDsl.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2021 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.saml2.Saml2LogoutConfigurer
+import org.springframework.security.config.annotation.web.saml2.LogoutRequestDsl
+import org.springframework.security.config.annotation.web.saml2.LogoutResponseDsl
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
+
+/**
+ * A Kotlin DSL to configure [HttpSecurity] SAML2 logout using idiomatic Kotlin code.
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @property relyingPartyRegistrationRepository the [RelyingPartyRegistrationRepository] of relying parties,
+ * each party representing a service provider, SP and this host, and identity provider, IDP pair that
+ * communicate with each other.
+ * @property logoutUrl the logout page to begin the SLO redirect flow
+ */
+@SecurityMarker
+class Saml2LogoutDsl {
+ var relyingPartyRegistrationRepository: RelyingPartyRegistrationRepository? = null
+ var logoutUrl: String? = null
+
+ private var logoutRequest: ((Saml2LogoutConfigurer.LogoutRequestConfigurer) -> Unit)? = null
+ private var logoutResponse: ((Saml2LogoutConfigurer.LogoutResponseConfigurer) -> Unit)? = null
+
+ /**
+ * Configures SAML 2.0 Logout Request components
+ * @param logoutRequestConfig the {@link Customizer} to provide more
+ * options for the {@link LogoutRequestConfigurer}
+ */
+ fun logoutRequest(logoutRequestConfig: LogoutRequestDsl.() -> Unit) {
+ this.logoutRequest = LogoutRequestDsl().apply(logoutRequestConfig).get()
+ }
+
+ /**
+ * Configures SAML 2.0 Logout Response components
+ * @param logoutResponseConfig the {@link Customizer} to provide more
+ * options for the {@link LogoutResponseConfigurer}
+ */
+ fun logoutResponse(logoutResponseConfig: LogoutResponseDsl.() -> Unit) {
+ this.logoutResponse = LogoutResponseDsl().apply(logoutResponseConfig).get()
+ }
+
+ internal fun get(): (Saml2LogoutConfigurer) -> Unit {
+ return { saml2Logout ->
+ relyingPartyRegistrationRepository?.also { saml2Logout.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) }
+ logoutUrl?.also { saml2Logout.logoutUrl(logoutUrl) }
+ logoutRequest?.also { saml2Logout.logoutRequest(logoutRequest) }
+ logoutResponse?.also { saml2Logout.logoutResponse(logoutResponse) }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutRequestDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutRequestDsl.kt
new file mode 100644
index 0000000000..0a07c15fb5
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutRequestDsl.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2024 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.saml2
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver
+
+/**
+ * A Kotlin DSL to configure SAML 2.0 Logout Request components using idiomatic Kotlin code.
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Request.
+ * The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
+ * @property logoutRequestValidator the [Saml2LogoutRequestValidator] to use for validating incoming {@code LogoutRequest}s.
+ * @property logoutRequestResolver the [Saml2LogoutRequestResolver] to use for generating outgoing {@code LogoutRequest}s.
+ * @property logoutRequestRepository the [Saml2LogoutRequestRepository] to use for storing outgoing {@code LogoutRequest}s for
+ * linking to the corresponding {@code LogoutResponse} from the asserting party
+ */
+@Saml2SecurityMarker
+class LogoutRequestDsl {
+ var logoutUrl = "/logout/saml2/slo"
+ var logoutRequestValidator: Saml2LogoutRequestValidator? = null
+ var logoutRequestResolver: Saml2LogoutRequestResolver? = null
+ var logoutRequestRepository: Saml2LogoutRequestRepository = HttpSessionLogoutRequestRepository()
+
+ internal fun get(): (Saml2LogoutConfigurer.LogoutRequestConfigurer) -> Unit {
+ return { logoutRequest ->
+ logoutUrl.also { logoutRequest.logoutUrl(logoutUrl) }
+ logoutRequestValidator?.also { logoutRequest.logoutRequestValidator(logoutRequestValidator) }
+ logoutRequestResolver?.also { logoutRequest.logoutRequestResolver(logoutRequestResolver) }
+ logoutRequestRepository.also { logoutRequest.logoutRequestRepository(logoutRequestRepository) }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutResponseDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutResponseDsl.kt
new file mode 100644
index 0000000000..dfc360c88d
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/LogoutResponseDsl.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2024 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.saml2
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver
+
+/**
+ * A Kotlin DSL to configure SAML 2.0 Logout Response components using idiomatic Kotlin code.
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ * @property logoutUrl The URL by which the asserting party can send a SAML 2.0 Logout Response.
+ * The Asserting Party should use whatever HTTP method specified in {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
+ * @property logoutResponseValidator the [Saml2LogoutResponseValidator] to use for validating incoming {@code LogoutResponse}s.
+ * @property logoutResponseResolver the [Saml2LogoutResponseResolver] to use for generating outgoing {@code LogoutResponse}s.
+ */
+@Saml2SecurityMarker
+class LogoutResponseDsl {
+ var logoutUrl = "/logout/saml2/slo"
+ var logoutResponseValidator: Saml2LogoutResponseValidator? = null
+ var logoutResponseResolver: Saml2LogoutResponseResolver? = null
+
+ internal fun get(): (Saml2LogoutConfigurer.LogoutResponseConfigurer) -> Unit {
+ return { logoutResponse ->
+ logoutUrl.also { logoutResponse.logoutUrl(logoutUrl) }
+ logoutResponseValidator?.also { logoutResponse.logoutResponseValidator(logoutResponseValidator) }
+ logoutResponseResolver?.also { logoutResponse.logoutResponseResolver(logoutResponseResolver) }
+ }
+ }
+}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/Saml2SecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/Saml2SecurityMarker.kt
new file mode 100644
index 0000000000..bcf8df5b81
--- /dev/null
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/saml2/Saml2SecurityMarker.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2002-2024 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.saml2
+
+/**
+ * Marker annotation indicating that the annotated class is part of the SAML 2.0 logout security DSL.
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ */
+@DslMarker
+annotation class Saml2SecurityMarker
diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDslTests.kt
new file mode 100644
index 0000000000..9e3e8fceca
--- /dev/null
+++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/Saml2LogoutDslTests.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2024 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.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.BeanCreationException
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.TestAuthentication
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication
+import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
+import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.MvcResult
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers
+import java.util.*
+
+/**
+ * Tests for [Saml2LogoutDsl]
+ *
+ * @author Josh Cummings
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class Saml2LogoutDslTests {
+ @JvmField
+ val spring = SpringTestContext(this)
+
+ @Autowired
+ lateinit var mockMvc: MockMvc
+
+ @Test
+ fun `saml2Logout when no relying party registration repository then exception`() {
+ Assertions.assertThatThrownBy { this.spring.register(Saml2LogoutNoRelyingPartyRegistrationRepoConfig::class.java).autowire() }
+ .isInstanceOf(BeanCreationException::class.java)
+ .hasMessageContaining("relyingPartyRegistrationRepository cannot be null")
+
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun `saml2Logout when defaults and not saml login then default logout`() {
+ this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
+ val user = TestAuthentication.authenticatedUser()
+ val result: MvcResult = this.mockMvc.perform(
+ MockMvcRequestBuilders.post("/logout").with(SecurityMockMvcRequestPostProcessors.authentication(user))
+ .with(SecurityMockMvcRequestPostProcessors.csrf()))
+ .andExpect(MockMvcResultMatchers.status().isFound())
+ .andReturn()
+ val location = result.response.getHeader("Location")
+ Assertions.assertThat(location).isEqualTo("/login?logout")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() {
+ this.spring.register(Saml2LogoutDefaultsConfig::class.java).autowire()
+ val principal = DefaultSaml2AuthenticatedPrincipal("user", emptyMap())
+ principal.relyingPartyRegistrationId = "registration-id"
+ val user = Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER"))
+ val result: MvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/logout")
+ .with(SecurityMockMvcRequestPostProcessors.authentication(user))
+ .with(SecurityMockMvcRequestPostProcessors.csrf()))
+ .andExpect(MockMvcResultMatchers.status().isFound())
+ .andReturn()
+ val location = result.response.getHeader("Location")
+ Assertions.assertThat(location).startsWith("https://ap.example.org/logout/saml2/request")
+ }
+
+ @Configuration
+ @EnableWebSecurity
+ open class Saml2LogoutNoRelyingPartyRegistrationRepoConfig {
+ @Bean
+ open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http {
+ saml2Logout { }
+ }
+ return http.build()
+ }
+ }
+
+ @Configuration
+ @EnableWebSecurity
+ open class Saml2LogoutDefaultsConfig {
+
+ @Bean
+ open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http {
+ saml2Logout { }
+ }
+ return http.build()
+ }
+
+ @Bean
+ open fun registrations(): RelyingPartyRegistrationRepository =
+ InMemoryRelyingPartyRegistrationRepository(TestRelyingPartyRegistrations.full().build())
+
+ }
+
+}