Skip to content

Commit ec314ee

Browse files
Add Reactive One-Time Token Login Kotlin DSL Support
Closes gh-15887
1 parent 562ba01 commit ec314ee

File tree

14 files changed

+605
-64
lines changed

14 files changed

+605
-64
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3003,8 +3003,8 @@ public HttpSecurity oauth2ResourceServer(
30033003
* }
30043004
*
30053005
* @Bean
3006-
* public GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler() {
3007-
* return new MyMagicLinkGeneratedOneTimeTokenHandler();
3006+
* public OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
3007+
* return new MyMagicLinkOneTimeTokenGenerationSuccessHandler();
30083008
* }
30093009
*
30103010
* }

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,19 @@ private SecurityContextRepository getSecurityContextRepository(H http) {
133133

134134
private void configureOttGenerateFilter(H http) {
135135
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
136-
getGeneratedOneTimeTokenHandler(http));
136+
getOneTimeTokenGenerationSuccessHandler(http));
137137
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
138138
http.addFilter(postProcess(generateFilter));
139139
http.addFilter(DefaultResourcesFilter.css());
140140
}
141141

142-
private OneTimeTokenGenerationSuccessHandler getGeneratedOneTimeTokenHandler(H http) {
142+
private OneTimeTokenGenerationSuccessHandler getOneTimeTokenGenerationSuccessHandler(H http) {
143143
if (this.oneTimeTokenGenerationSuccessHandler == null) {
144144
this.oneTimeTokenGenerationSuccessHandler = getBeanOrNull(http, OneTimeTokenGenerationSuccessHandler.class);
145145
}
146146
if (this.oneTimeTokenGenerationSuccessHandler == null) {
147147
throw new IllegalStateException("""
148-
A GeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
148+
A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
149149
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
150150
""");
151151
}
@@ -200,7 +200,7 @@ public OneTimeTokenLoginConfigurer<H> tokenGeneratingUrl(String tokenGeneratingU
200200
*/
201201
public OneTimeTokenLoginConfigurer<H> tokenGenerationSuccessHandler(
202202
OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) {
203-
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "generatedOneTimeTokenHandler cannot be null");
203+
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null");
204204
this.oneTimeTokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler;
205205
return this;
206206
}

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,8 +1578,8 @@ public ServerHttpSecurity authenticationManager(ReactiveAuthenticationManager ma
15781578
* }
15791579
*
15801580
* &#064;Bean
1581-
* public ServerGeneratedOneTimeTokenHandler generatedOneTimeTokenHandler() {
1582-
* return new MyMagicLinkServerGeneratedOneTimeTokenHandler();
1581+
* public ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
1582+
* return new MyMagicLinkServerOneTimeTokenGenerationSuccessHandler();
15831583
* }
15841584
*
15851585
* }
@@ -6151,12 +6151,12 @@ public OneTimeTokenLoginSpec defaultSubmitPageUrl(String submitPageUrl) {
61516151

61526152
/**
61536153
* Specifies strategy to be used to handle generated one-time tokens.
6154-
* @param generatedOneTimeTokenHandler
6154+
* @param generationSuccessHandler
61556155
*/
61566156
public OneTimeTokenLoginSpec tokenGenerationSuccessHandler(
6157-
ServerOneTimeTokenGenerationSuccessHandler generatedOneTimeTokenHandler) {
6158-
Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null");
6159-
this.tokenGenerationSuccessHandler = generatedOneTimeTokenHandler;
6157+
ServerOneTimeTokenGenerationSuccessHandler generationSuccessHandler) {
6158+
Assert.notNull(generationSuccessHandler, "generationSuccessHandler cannot be null");
6159+
this.tokenGenerationSuccessHandler = generationSuccessHandler;
61606160
return this;
61616161
}
61626162

@@ -6193,7 +6193,7 @@ private ServerOneTimeTokenGenerationSuccessHandler getTokenGenerationSuccessHand
61936193
}
61946194
if (this.tokenGenerationSuccessHandler == null) {
61956195
throw new IllegalStateException("""
6196-
A ServerGeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
6196+
A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
61976197
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
61986198
""");
61996199
}

config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
985985
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
986986
* http {
987987
* oneTimeTokenLogin {
988-
* generatedOneTimeTokenHandler = MyMagicLinkGeneratedOneTimeTokenHandler()
988+
* oneTimeTokenGenerationSuccessHandler = MyMagicLinkOneTimeTokenGenerationSuccessHandler()
989989
* }
990990
* }
991991
* return http.build()

config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,36 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
714714
this.http.sessionManagement(sessionManagementCustomizer)
715715
}
716716

717+
/**
718+
* Configures One-Time Token Login support.
719+
*
720+
* Example:
721+
*
722+
* ```
723+
* @Configuration
724+
* @EnableWebFluxSecurity
725+
* open class SecurityConfig {
726+
*
727+
* @Bean
728+
* open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
729+
* return http {
730+
* oneTimeTokenLogin {
731+
* tokenGenerationSuccessHandler = MyMagicLinkServerOneTimeTokenGenerationSuccessHandler()
732+
* }
733+
* }
734+
* }
735+
* }
736+
* ```
737+
*
738+
* @param oneTimeTokenLoginConfiguration custom configuration to configure the One-Time Token Login
739+
* @since 6.4
740+
* @see [ServerOneTimeTokenLoginDsl]
741+
*/
742+
fun oneTimeTokenLogin(oneTimeTokenLoginConfiguration: ServerOneTimeTokenLoginDsl.()-> Unit){
743+
val oneTimeTokenLoginCustomizer = ServerOneTimeTokenLoginDsl().apply(oneTimeTokenLoginConfiguration).get()
744+
this.http.oneTimeTokenLogin(oneTimeTokenLoginCustomizer)
745+
}
746+
717747
/**
718748
* Apply all configurations to the provided [ServerHttpSecurity]
719749
*/
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.web.server
18+
19+
import org.springframework.security.authentication.ReactiveAuthenticationManager
20+
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
21+
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
22+
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
23+
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
24+
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
25+
import org.springframework.security.web.server.context.ServerSecurityContextRepository
26+
27+
/**
28+
* A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic Kotlin code.
29+
*
30+
* @author Max Batischev
31+
* @since 6.4
32+
* @property tokenService configures the [ReactiveOneTimeTokenService] used to generate and consume
33+
* @property authenticationManager configures the [ReactiveAuthenticationManager] used to generate and consume
34+
* @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
35+
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
36+
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used
37+
* @property defaultSubmitPageUrl sets the URL that the default submit page will be generated
38+
* @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown
39+
* @property loginProcessingUrl the URL to process the login request
40+
* @property tokenGeneratingUrl the URL that a One-Time Token generate request will be processed
41+
* @property tokenGenerationSuccessHandler the strategy to be used to handle generated one-time tokens
42+
* @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).
43+
*/
44+
@ServerSecurityMarker
45+
class ServerOneTimeTokenLoginDsl {
46+
var authenticationManager: ReactiveAuthenticationManager? = null
47+
var tokenService: ReactiveOneTimeTokenService? = null
48+
var authenticationConverter: ServerAuthenticationConverter? = null
49+
var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null
50+
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
51+
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
52+
var securityContextRepository: ServerSecurityContextRepository? = null
53+
var defaultSubmitPageUrl: String? = null
54+
var loginProcessingUrl: String? = null
55+
var tokenGeneratingUrl: String? = null
56+
var showDefaultSubmitPage: Boolean? = true
57+
58+
internal fun get(): (ServerHttpSecurity.OneTimeTokenLoginSpec) -> Unit {
59+
return { oneTimeTokenLogin ->
60+
authenticationManager?.also { oneTimeTokenLogin.authenticationManager(authenticationManager) }
61+
tokenService?.also { oneTimeTokenLogin.tokenService(tokenService) }
62+
authenticationConverter?.also { oneTimeTokenLogin.authenticationConverter(authenticationConverter) }
63+
authenticationFailureHandler?.also {
64+
oneTimeTokenLogin.authenticationFailureHandler(
65+
authenticationFailureHandler
66+
)
67+
}
68+
authenticationSuccessHandler?.also {
69+
oneTimeTokenLogin.authenticationSuccessHandler(
70+
authenticationSuccessHandler
71+
)
72+
}
73+
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
74+
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
75+
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
76+
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }
77+
tokenGeneratingUrl?.also { oneTimeTokenLogin.tokenGeneratingUrl(tokenGeneratingUrl) }
78+
tokenGenerationSuccessHandler?.also {
79+
oneTimeTokenLogin.tokenGenerationSuccessHandler(
80+
tokenGenerationSuccessHandler
81+
)
82+
}
83+
}
84+
}
85+
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ void oneTimeTokenWhenNoTokenGenerationSuccessHandlerThenException() {
189189
.havingRootCause()
190190
.isInstanceOf(IllegalStateException.class)
191191
.withMessage("""
192-
A GeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
192+
A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
193193
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
194194
""");
195195
}

config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,13 @@ void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() {
269269
}
270270

271271
@Test
272-
void oneTimeTokenWhenNoGeneratedOneTimeTokenHandlerThenException() {
272+
void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() {
273273
assertThatException()
274274
.isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire())
275275
.havingRootCause()
276276
.isInstanceOf(IllegalStateException.class)
277277
.withMessage("""
278-
A ServerGeneratedOneTimeTokenHandler is required to enable oneTimeTokenLogin().
278+
A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
279279
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
280280
""");
281281
}

config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class OneTimeTokenLoginDslTests {
6969
.redirectedUrl("/login/ott")
7070
)
7171

72-
val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue
72+
val token = TestOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue
7373

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

94-
val token = TestGeneratedOneTimeTokenHandler.lastToken?.tokenValue
94+
val token = TestOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue
9595

9696
this.mockMvc.perform(
9797
MockMvcRequestBuilders.post("/loginprocessingurl").param("token", token)
@@ -117,7 +117,7 @@ class OneTimeTokenLoginDslTests {
117117
authorize(anyRequest, authenticated)
118118
}
119119
oneTimeTokenLogin {
120-
oneTimeTokenGenerationSuccessHandler = TestGeneratedOneTimeTokenHandler()
120+
oneTimeTokenGenerationSuccessHandler = TestOneTimeTokenGenerationSuccessHandler()
121121
}
122122
}
123123
// @formatter:on
@@ -138,7 +138,7 @@ class OneTimeTokenLoginDslTests {
138138
}
139139
oneTimeTokenLogin {
140140
tokenGeneratingUrl = "/generateurl"
141-
oneTimeTokenGenerationSuccessHandler = TestGeneratedOneTimeTokenHandler("/redirected")
141+
oneTimeTokenGenerationSuccessHandler = TestOneTimeTokenGenerationSuccessHandler("/redirected")
142142
loginProcessingUrl = "/loginprocessingurl"
143143
authenticationSuccessHandler = SimpleUrlAuthenticationSuccessHandler("/authenticated")
144144
}
@@ -156,7 +156,7 @@ class OneTimeTokenLoginDslTests {
156156
InMemoryUserDetailsManager(PasswordEncodedUser.user(), PasswordEncodedUser.admin())
157157
}
158158

159-
private class TestGeneratedOneTimeTokenHandler :
159+
private class TestOneTimeTokenGenerationSuccessHandler :
160160
OneTimeTokenGenerationSuccessHandler {
161161
private val delegate: OneTimeTokenGenerationSuccessHandler
162162

0 commit comments

Comments
 (0)