4 changed files with 498 additions and 48 deletions
@ -0,0 +1,83 @@ |
|||||||
|
/* |
||||||
|
* 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.springframework.security.authentication.AuthenticationProvider |
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenService |
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity |
||||||
|
import org.springframework.security.config.annotation.web.configurers.ott.OneTimeTokenLoginConfigurer |
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter |
||||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler |
||||||
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.GeneratedOneTimeTokenHandler |
||||||
|
|
||||||
|
/** |
||||||
|
* A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. |
||||||
|
* |
||||||
|
* @author Max Batischev |
||||||
|
* @since 6.4 |
||||||
|
* @property oneTimeTokenService configures the [OneTimeTokenService] used to generate and consume |
||||||
|
* @property authenticationConverter Use this [AuthenticationConverter] when converting incoming requests to an authentication |
||||||
|
* @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication |
||||||
|
* @property authenticationSuccessHandler the [AuthenticationSuccessHandler] to be used |
||||||
|
* @property defaultSubmitPageUrl sets the URL that the default submit page will be generated |
||||||
|
* @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown |
||||||
|
* @property loginProcessingUrl the URL to process the login request |
||||||
|
* @property generateTokenUrl the URL that a One-Time Token generate request will be processed |
||||||
|
* @property generatedOneTimeTokenHandler the strategy to be used to handle generated one-time tokens |
||||||
|
* @property authenticationProvider the [AuthenticationProvider] to use when authenticating the user |
||||||
|
*/ |
||||||
|
@SecurityMarker |
||||||
|
class OneTimeTokenLoginDsl { |
||||||
|
var oneTimeTokenService: OneTimeTokenService? = null |
||||||
|
var authenticationConverter: AuthenticationConverter? = null |
||||||
|
var authenticationFailureHandler: AuthenticationFailureHandler? = null |
||||||
|
var authenticationSuccessHandler: AuthenticationSuccessHandler? = null |
||||||
|
var defaultSubmitPageUrl: String? = null |
||||||
|
var loginProcessingUrl: String? = null |
||||||
|
var generateTokenUrl: String? = null |
||||||
|
var showDefaultSubmitPage: Boolean? = true |
||||||
|
var generatedOneTimeTokenHandler: GeneratedOneTimeTokenHandler? = null |
||||||
|
var authenticationProvider: AuthenticationProvider? = null |
||||||
|
|
||||||
|
internal fun get(): (OneTimeTokenLoginConfigurer<HttpSecurity>) -> Unit { |
||||||
|
return { oneTimeTokenLoginConfigurer -> |
||||||
|
oneTimeTokenService?.also { oneTimeTokenLoginConfigurer.oneTimeTokenService(oneTimeTokenService) } |
||||||
|
authenticationConverter?.also { oneTimeTokenLoginConfigurer.authenticationConverter(authenticationConverter) } |
||||||
|
authenticationFailureHandler?.also { |
||||||
|
oneTimeTokenLoginConfigurer.authenticationFailureHandler( |
||||||
|
authenticationFailureHandler |
||||||
|
) |
||||||
|
} |
||||||
|
authenticationSuccessHandler?.also { |
||||||
|
oneTimeTokenLoginConfigurer.authenticationSuccessHandler( |
||||||
|
authenticationSuccessHandler |
||||||
|
) |
||||||
|
} |
||||||
|
defaultSubmitPageUrl?.also { oneTimeTokenLoginConfigurer.defaultSubmitPageUrl(defaultSubmitPageUrl) } |
||||||
|
showDefaultSubmitPage?.also { oneTimeTokenLoginConfigurer.showDefaultSubmitPage(showDefaultSubmitPage!!) } |
||||||
|
loginProcessingUrl?.also { oneTimeTokenLoginConfigurer.loginProcessingUrl(loginProcessingUrl) } |
||||||
|
generateTokenUrl?.also { oneTimeTokenLoginConfigurer.generateTokenUrl(generateTokenUrl) } |
||||||
|
generatedOneTimeTokenHandler?.also { |
||||||
|
oneTimeTokenLoginConfigurer.generatedOneTimeTokenHandler( |
||||||
|
generatedOneTimeTokenHandler |
||||||
|
) |
||||||
|
} |
||||||
|
authenticationProvider?.also { oneTimeTokenLoginConfigurer.authenticationProvider(authenticationProvider) } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,179 @@ |
|||||||
|
/* |
||||||
|
* 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 jakarta.servlet.http.HttpServletRequest |
||||||
|
import jakarta.servlet.http.HttpServletResponse |
||||||
|
import org.junit.jupiter.api.Test |
||||||
|
import org.junit.jupiter.api.extension.ExtendWith |
||||||
|
import org.springframework.beans.factory.annotation.Autowired |
||||||
|
import org.springframework.context.annotation.Bean |
||||||
|
import org.springframework.context.annotation.Configuration |
||||||
|
import org.springframework.context.annotation.Import |
||||||
|
import org.springframework.security.authentication.ott.OneTimeToken |
||||||
|
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.userdetails.PasswordEncodedUser |
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService |
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager |
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors |
||||||
|
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers |
||||||
|
import org.springframework.security.web.SecurityFilterChain |
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler |
||||||
|
import org.springframework.security.web.authentication.ott.GeneratedOneTimeTokenHandler |
||||||
|
import org.springframework.security.web.authentication.ott.RedirectGeneratedOneTimeTokenHandler |
||||||
|
import org.springframework.test.web.servlet.MockMvc |
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders |
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for [OneTimeTokenLoginDsl] |
||||||
|
* |
||||||
|
* @author Max Batischev |
||||||
|
*/ |
||||||
|
@ExtendWith(SpringTestContextExtension::class) |
||||||
|
class OneTimeTokenLoginDslTests { |
||||||
|
@JvmField |
||||||
|
val spring = SpringTestContext(this) |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private lateinit var mockMvc: MockMvc |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `oneTimeToken when correct token then can authenticate`() { |
||||||
|
spring.register(OneTimeTokenConfig::class.java).autowire() |
||||||
|
this.mockMvc.perform( |
||||||
|
MockMvcRequestBuilders.post("/ott/generate").param("username", "user") |
||||||
|
.with(SecurityMockMvcRequestPostProcessors.csrf()) |
||||||
|
).andExpectAll( |
||||||
|
MockMvcResultMatchers |
||||||
|
.status() |
||||||
|
.isFound(), |
||||||
|
MockMvcResultMatchers |
||||||
|
.redirectedUrl("/login/ott") |
||||||
|
) |
||||||
|
|
||||||
|
val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue |
||||||
|
|
||||||
|
this.mockMvc.perform( |
||||||
|
MockMvcRequestBuilders.post("/login/ott").param("token", token) |
||||||
|
.with(SecurityMockMvcRequestPostProcessors.csrf()) |
||||||
|
) |
||||||
|
.andExpectAll( |
||||||
|
MockMvcResultMatchers.status().isFound(), |
||||||
|
MockMvcResultMatchers.redirectedUrl("/"), |
||||||
|
SecurityMockMvcResultMatchers.authenticated() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `oneTimeToken when different authentication urls then can authenticate`() { |
||||||
|
spring.register(OneTimeTokenDifferentUrlsConfig::class.java).autowire() |
||||||
|
this.mockMvc.perform( |
||||||
|
MockMvcRequestBuilders.post("/generateurl").param("username", "user") |
||||||
|
.with(SecurityMockMvcRequestPostProcessors.csrf()) |
||||||
|
) |
||||||
|
.andExpectAll(MockMvcResultMatchers.status().isFound(), MockMvcResultMatchers.redirectedUrl("/redirected")) |
||||||
|
|
||||||
|
val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue |
||||||
|
|
||||||
|
this.mockMvc.perform( |
||||||
|
MockMvcRequestBuilders.post("/loginprocessingurl").param("token", token) |
||||||
|
.with(SecurityMockMvcRequestPostProcessors.csrf()) |
||||||
|
) |
||||||
|
.andExpectAll( |
||||||
|
MockMvcResultMatchers.status().isFound(), |
||||||
|
MockMvcResultMatchers.redirectedUrl("/authenticated"), |
||||||
|
SecurityMockMvcResultMatchers.authenticated() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
@Configuration |
||||||
|
@EnableWebSecurity |
||||||
|
@Import(UserDetailsServiceConfig::class) |
||||||
|
open class OneTimeTokenConfig { |
||||||
|
|
||||||
|
@Bean |
||||||
|
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
oneTimeTokenLogin { |
||||||
|
generatedOneTimeTokenHandler = TestGeneratedOneTimeTokenHandler() |
||||||
|
} |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@EnableWebSecurity |
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
@Import(UserDetailsServiceConfig::class) |
||||||
|
open class OneTimeTokenDifferentUrlsConfig { |
||||||
|
@Bean |
||||||
|
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { |
||||||
|
// @formatter:off |
||||||
|
http { |
||||||
|
authorizeHttpRequests { |
||||||
|
authorize(anyRequest, authenticated) |
||||||
|
} |
||||||
|
oneTimeTokenLogin { |
||||||
|
generateTokenUrl = "/generateurl" |
||||||
|
generatedOneTimeTokenHandler = TestGeneratedOneTimeTokenHandler("/redirected") |
||||||
|
loginProcessingUrl = "/loginprocessingurl" |
||||||
|
authenticationSuccessHandler = SimpleUrlAuthenticationSuccessHandler("/authenticated") |
||||||
|
} |
||||||
|
} |
||||||
|
// @formatter:on |
||||||
|
return http.build() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
open class UserDetailsServiceConfig { |
||||||
|
|
||||||
|
@Bean |
||||||
|
open fun userDetailsService(): UserDetailsService = |
||||||
|
InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin()) |
||||||
|
} |
||||||
|
|
||||||
|
private class TestGeneratedOneTimeTokenHandler : GeneratedOneTimeTokenHandler { |
||||||
|
private val delegate: GeneratedOneTimeTokenHandler |
||||||
|
|
||||||
|
constructor() { |
||||||
|
this.delegate = RedirectGeneratedOneTimeTokenHandler("/login/ott") |
||||||
|
} |
||||||
|
|
||||||
|
constructor(redirectUrl: String?) { |
||||||
|
this.delegate = RedirectGeneratedOneTimeTokenHandler(redirectUrl) |
||||||
|
} |
||||||
|
|
||||||
|
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) { |
||||||
|
lastToken = oneTimeToken |
||||||
|
delegate.handle(request, response, oneTimeToken) |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
var lastToken: OneTimeToken? = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue