2
0
Эх сурвалжийг харах

Add Saml2Logout DSL Support

Closes gh-14935
Josh Cummings 1 жил өмнө
parent
commit
2bcbef1695

+ 64 - 1
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. <br>
+     * <br>
+     *
+     * Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
+     * documented in the
+     * <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
+     * Core, Profiles and Bindings</a> specifications. <br>
+     * <br>
+     *
+     * 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]. <br>
+     * <br>
+     *
+     * [RelyingPartyRegistration] (s) are composed within a
+     * [RelyingPartyRegistrationRepository], which is <b>required</b> and must be
+     * registered with the [ApplicationContext] or configured via
+     * [HttpSecurityDsl.saml2Login].<br>
+     * <br>
+     *
+     * The default configuration provides an auto-generated logout endpoint at
+     * `/logout` and redirects to `/login?logout` when
+     * logout completes. <br>
+     * <br>
+     *
+     * <p>
+     * <h2>Example Configuration</h2>
+     *
+     * 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()
+     *     }
+     * }
+     * ```
+     *
+     * <p>
+     * @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.
 	 *

+ 69 - 0
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<HttpSecurity>.LogoutRequestConfigurer) -> Unit)? = null
+    private var logoutResponse: ((Saml2LogoutConfigurer<HttpSecurity>.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<HttpSecurity>) -> Unit {
+        return { saml2Logout ->
+            relyingPartyRegistrationRepository?.also { saml2Logout.relyingPartyRegistrationRepository(relyingPartyRegistrationRepository) }
+            logoutUrl?.also { saml2Logout.logoutUrl(logoutUrl) }
+            logoutRequest?.also { saml2Logout.logoutRequest(logoutRequest) }
+            logoutResponse?.also { saml2Logout.logoutResponse(logoutResponse) }
+        }
+    }
+}

+ 53 - 0
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<HttpSecurity>.LogoutRequestConfigurer) -> Unit {
+        return { logoutRequest ->
+            logoutUrl.also { logoutRequest.logoutUrl(logoutUrl) }
+            logoutRequestValidator?.also { logoutRequest.logoutRequestValidator(logoutRequestValidator) }
+            logoutRequestResolver?.also { logoutRequest.logoutRequestResolver(logoutRequestResolver) }
+            logoutRequestRepository.also { logoutRequest.logoutRequestRepository(logoutRequestRepository) }
+        }
+    }
+}

+ 47 - 0
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<HttpSecurity>.LogoutResponseConfigurer) -> Unit {
+        return { logoutResponse ->
+            logoutUrl.also { logoutResponse.logoutUrl(logoutUrl) }
+            logoutResponseValidator?.also { logoutResponse.logoutResponseValidator(logoutResponseValidator) }
+            logoutResponseResolver?.also { logoutResponse.logoutResponseResolver(logoutResponseResolver) }
+        }
+    }
+}

+ 26 - 0
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

+ 126 - 0
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())
+
+    }
+
+}