Skip to content

Add Reactive One-Time Token Login Kotlin DSL Support #15888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 15, 2024
Merged
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 @@ -3003,8 +3003,8 @@ public HttpSecurity oauth2ResourceServer(
* }
*
* @Bean
* public GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler() {
* return new MyMagicLinkGeneratedOneTimeTokenHandler();
* public OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
* return new MyMagicLinkOneTimeTokenGenerationSuccessHandler();
* }
*
* }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,19 @@ private SecurityContextRepository getSecurityContextRepository(H http) {

private void configureOttGenerateFilter(H http) {
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
getGeneratedOneTimeTokenHandler(http));
getOneTimeTokenGenerationSuccessHandler(http));
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
http.addFilter(postProcess(generateFilter));
http.addFilter(DefaultResourcesFilter.css());
}

private OneTimeTokenGenerationSuccessHandler getGeneratedOneTimeTokenHandler(H http) {
private OneTimeTokenGenerationSuccessHandler getOneTimeTokenGenerationSuccessHandler(H http) {
if (this.oneTimeTokenGenerationSuccessHandler == null) {
this.oneTimeTokenGenerationSuccessHandler = getBeanOrNull(http, OneTimeTokenGenerationSuccessHandler.class);
}
if (this.oneTimeTokenGenerationSuccessHandler == null) {
throw new IllegalStateException("""
A GeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
""");
}
Expand Down Expand Up @@ -200,7 +200,7 @@ public OneTimeTokenLoginConfigurer<H> tokenGeneratingUrl(String tokenGeneratingU
*/
public OneTimeTokenLoginConfigurer<H> tokenGenerationSuccessHandler(
OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) {
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "generatedOneTimeTokenHandler cannot be null");
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null");
this.oneTimeTokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1578,8 +1578,8 @@ public ServerHttpSecurity authenticationManager(ReactiveAuthenticationManager ma
* }
*
* &#064;Bean
* public ServerGeneratedOneTimeTokenHandler generatedOneTimeTokenHandler() {
* return new MyMagicLinkServerGeneratedOneTimeTokenHandler();
* public ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
* return new MyMagicLinkServerOneTimeTokenGenerationSuccessHandler();
* }
*
* }
Expand Down Expand Up @@ -6151,12 +6151,12 @@ public OneTimeTokenLoginSpec defaultSubmitPageUrl(String submitPageUrl) {

/**
* Specifies strategy to be used to handle generated one-time tokens.
* @param generatedOneTimeTokenHandler
* @param oneTimeTokenGenerationSuccessHandler
*/
public OneTimeTokenLoginSpec tokenGenerationSuccessHandler(
ServerOneTimeTokenGenerationSuccessHandler generatedOneTimeTokenHandler) {
Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null");
this.tokenGenerationSuccessHandler = generatedOneTimeTokenHandler;
ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) {
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null");
this.tokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler;
return this;
}

Expand Down Expand Up @@ -6193,7 +6193,7 @@ private ServerOneTimeTokenGenerationSuccessHandler getTokenGenerationSuccessHand
}
if (this.tokenGenerationSuccessHandler == null) {
throw new IllegalStateException("""
A ServerGeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oneTimeTokenLogin {
* generatedOneTimeTokenHandler = MyMagicLinkGeneratedOneTimeTokenHandler()
* oneTimeTokenGenerationSuccessHandler = MyMagicLinkOneTimeTokenGenerationSuccessHandler()
* }
* }
* return http.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,36 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
this.http.sessionManagement(sessionManagementCustomizer)
}

/**
* Configures One-Time Token Login support.
*
* Example:
*
* ```
* @Configuration
* @EnableWebFluxSecurity
* open class SecurityConfig {
*
* @Bean
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
* return http {
* oneTimeTokenLogin {
* tokenGenerationSuccessHandler = MyMagicLinkServerOneTimeTokenGenerationSuccessHandler()
* }
* }
* }
* }
* ```
*
* @param oneTimeTokenLoginConfiguration custom configuration to configure the One-Time Token Login
* @since 6.4
* @see [ServerOneTimeTokenLoginDsl]
*/
fun oneTimeTokenLogin(oneTimeTokenLoginConfiguration: ServerOneTimeTokenLoginDsl.()-> Unit){
val oneTimeTokenLoginCustomizer = ServerOneTimeTokenLoginDsl().apply(oneTimeTokenLoginConfiguration).get()
this.http.oneTimeTokenLogin(oneTimeTokenLoginCustomizer)
}

/**
* Apply all configurations to the provided [ServerHttpSecurity]
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.web.server

import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.server.context.ServerSecurityContextRepository

/**
* A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic Kotlin code.
*
* @author Max Batischev
* @since 6.4
* @property tokenService configures the [ReactiveOneTimeTokenService] used to generate and consume
* @property authenticationManager configures the [ReactiveAuthenticationManager] used to generate and consume
* @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] 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 tokenGeneratingUrl the URL that a One-Time Token generate request will be processed
* @property tokenGenerationSuccessHandler the strategy to be used to handle generated one-time tokens
* @property securityContextRepository the [ServerSecurityContextRepository] used to save the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the [ReactorContextWebFilter] must be configured to be able to load the value (they are not implicitly linked).
*/
@ServerSecurityMarker
class ServerOneTimeTokenLoginDsl {
var authenticationManager: ReactiveAuthenticationManager? = null
var tokenService: ReactiveOneTimeTokenService? = null
var authenticationConverter: ServerAuthenticationConverter? = null
var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
var securityContextRepository: ServerSecurityContextRepository? = null
var defaultSubmitPageUrl: String? = null
var loginProcessingUrl: String? = null
var tokenGeneratingUrl: String? = null
var showDefaultSubmitPage: Boolean? = true

internal fun get(): (ServerHttpSecurity.OneTimeTokenLoginSpec) -> Unit {
return { oneTimeTokenLogin ->
authenticationManager?.also { oneTimeTokenLogin.authenticationManager(authenticationManager) }
tokenService?.also { oneTimeTokenLogin.tokenService(tokenService) }
authenticationConverter?.also { oneTimeTokenLogin.authenticationConverter(authenticationConverter) }
authenticationFailureHandler?.also {
oneTimeTokenLogin.authenticationFailureHandler(
authenticationFailureHandler
)
}
authenticationSuccessHandler?.also {
oneTimeTokenLogin.authenticationSuccessHandler(
authenticationSuccessHandler
)
}
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }
tokenGeneratingUrl?.also { oneTimeTokenLogin.tokenGeneratingUrl(tokenGeneratingUrl) }
tokenGenerationSuccessHandler?.also {
oneTimeTokenLogin.tokenGenerationSuccessHandler(
tokenGenerationSuccessHandler
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ void oneTimeTokenWhenNoTokenGenerationSuccessHandlerThenException() {
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessage("""
A GeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,13 @@ void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() {
}

@Test
void oneTimeTokenWhenNoGeneratedOneTimeTokenHandlerThenException() {
void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() {
assertThatException()
.isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire())
.havingRootCause()
.isInstanceOf(IllegalStateException.class)
.withMessage("""
A ServerGeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class OneTimeTokenLoginDslTests {
.redirectedUrl("/login/ott")
)

val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue
val token = TestOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue

this.mockMvc.perform(
MockMvcRequestBuilders.post("/login/ott").param("token", token)
Expand All @@ -91,7 +91,7 @@ class OneTimeTokenLoginDslTests {
)
.andExpectAll(MockMvcResultMatchers.status().isFound(), MockMvcResultMatchers.redirectedUrl("/redirected"))

val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue
val token = TestOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue

this.mockMvc.perform(
MockMvcRequestBuilders.post("/loginprocessingurl").param("token", token)
Expand All @@ -117,7 +117,7 @@ class OneTimeTokenLoginDslTests {
authorize(anyRequest, authenticated)
}
oneTimeTokenLogin {
oneTimeTokenGenerationSuccessHandler = TestGeneratedOneTimeTokenHandler()
oneTimeTokenGenerationSuccessHandler = TestOneTimeTokenGenerationSuccessHandler()
}
}
// @formatter:on
Expand All @@ -138,7 +138,7 @@ class OneTimeTokenLoginDslTests {
}
oneTimeTokenLogin {
tokenGeneratingUrl = "/generateurl"
oneTimeTokenGenerationSuccessHandler = TestGeneratedOneTimeTokenHandler("/redirected")
oneTimeTokenGenerationSuccessHandler = TestOneTimeTokenGenerationSuccessHandler("/redirected")
loginProcessingUrl = "/loginprocessingurl"
authenticationSuccessHandler = SimpleUrlAuthenticationSuccessHandler("/authenticated")
}
Expand All @@ -156,7 +156,7 @@ class OneTimeTokenLoginDslTests {
InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin())
}

private class TestGeneratedOneTimeTokenHandler :
private class TestOneTimeTokenGenerationSuccessHandler :
OneTimeTokenGenerationSuccessHandler {
private val delegate: OneTimeTokenGenerationSuccessHandler

Expand Down
Loading