Skip to content

Commit

Permalink
Add Reactive One-Time Token Login Kotlin DSL Support
Browse files Browse the repository at this point in the history
Closes gh-15887
  • Loading branch information
franticticktick committed Oct 15, 2024
1 parent 562ba01 commit ec314ee
Show file tree
Hide file tree
Showing 14 changed files with 605 additions and 64 deletions.
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 generationSuccessHandler
*/
public OneTimeTokenLoginSpec tokenGenerationSuccessHandler(
ServerOneTimeTokenGenerationSuccessHandler generatedOneTimeTokenHandler) {
Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null");
this.tokenGenerationSuccessHandler = generatedOneTimeTokenHandler;
ServerOneTimeTokenGenerationSuccessHandler generationSuccessHandler) {
Assert.notNull(generationSuccessHandler, "generationSuccessHandler cannot be null");
this.tokenGenerationSuccessHandler = generationSuccessHandler;
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

0 comments on commit ec314ee

Please sign in to comment.