|
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 7.1.1! |
One-Time Token Login
Spring Security offers support for One-Time Token (OTT) authentication via the oneTimeTokenLogin() DSL.
Before diving into implementation details, it’s important to clarify the scope of the OTT feature within the framework, highlighting what is supported and what isn’t.
Understanding One-Time Tokens vs. One-Time Passwords
It’s common to confuse One-Time Tokens (OTT) with One-Time Passwords (OTP), but in Spring Security, these concepts differ in several key ways. For clarity, we’ll assume OTP refers to TOTP (Time-Based One-Time Password) or HOTP (HMAC-Based One-Time Password).
Setup Requirements
-
OTT: No initial setup is required. The user doesn’t need to configure anything in advance.
-
OTP: Typically requires setup, such as generating and sharing a secret key with an external tool to produce the one-time passwords.
Token Delivery
-
OTT: Usually a custom
OneTimeTokenGenerationSuccessHandlermust be implemented, responsible for delivering the token to the end user. -
OTP: The token is often generated by an external tool, so there’s no need to send it to the user via the application.
Token Generation
-
OTT: The
OneTimeTokenService.generate(GenerateOneTimeTokenRequest)method requires aOneTimeTokento be returned, emphasizing server-side generation. -
OTP: The token is not necessarily generated on the server side, it’s often created by the client using the shared secret.
In summary, One-Time Tokens (OTT) provide a way to authenticate users without additional account setup, differentiating them from One-Time Passwords (OTP), which typically involve a more complex setup process and rely on external tools for token generation.
The One-Time Token Login works in two major steps.
-
User requests a token by submitting their user identifier, usually the username, and the token is delivered to them, often as a Magic Link, via e-mail, SMS, etc.
-
User submits the token to the one-time token login endpoint and, if valid, the user gets logged in.
In the following sections we will explore how to configure OTT Login for your needs.
Default Login Page and Default One-Time Token Submit Page
When the oneTimeTokenLogin() DSL is used, by default the One-Time Token Login Page is auto-generated by the DefaultLoginPageGeneratingFilter.
The DSL will also set up the DefaultOneTimeTokenSubmitPageGeneratingFilter to generate a default One-Time Token submit page.
Sending the Token to the User
It is not possible for Spring Security to reasonably determine the way the token should be delivered to your users.
Therefore, a custom OneTimeTokenGenerationSuccessHandler must be provided to deliver the token to the user based on your needs.
One of the most common delivery strategies is a Magic Link, via e-mail, SMS, etc.
In the following example, we are going to create a magic link and sent it to the user’s email.
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
}
-
Java
-
Kotlin
@Component (1)
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
public MagicLinkOneTimeTokenGenerationSuccessHandler(MailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); (2)
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); (3)
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("Your Spring Security One Time Token");
message.setText("Use the following link to sign in into the application: " + magicLink);
this.mailSender.send(message); (4)
this.redirectHandler.handle(request, response, oneTimeToken); (5)
}
private String getUserEmail(String username) {
...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender) : OneTimeTokenGenerationSuccessHandler {
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
val builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.contextPath)
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()) (2)
val magicLink = builder.toUriString()
val email = getUserEmail(oneTimeToken.getUsername()) (3)
val message = SimpleMailMessage()
message.setTo(email)
message.subject = "Your Spring Security One Time Token"
message.text = "Use the following link to sign in into the application: $magicLink"
this.mailSender.send(message) (4)
this.redirectHandler.handle(request, response, oneTimeToken) (5)
}
private fun getUserEmail(username: String): String {
...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
| 1 | Make the MagicLinkOneTimeTokenGenerationSuccessHandler a Spring bean |
| 2 | Create a login processing URL with the token as a query param |
| 3 | Retrieve the user’s email based on the username |
| 4 | Use the MailSender API to send the email to the user with the magic link |
| 5 | Use the RedirectOneTimeTokenGenerationSuccessHandler to perform a redirect to your desired URL |
The email content will look similar to:
Use the following link to sign in into the application: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
The default submit page will detect that the URL has the token query param and will automatically fill the form field with the token value.
Changing the One-Time Token Generate URL
By default, the GenerateOneTimeTokenFilter listens to POST /ott/generate requests.
That URL can be changed by using the tokenGeneratingUrl(String) DSL method:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGeneratingUrl("/ott/my-generate-url")
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
tokenGeneratingUrl = "/ott/my-generate-url"
}
}
return http.build()
}
}
-
Java
-
Kotlin
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
|
The URI passed to |
Changing the Default Submit Page URL
The default One-Time Token submit page is generated by the DefaultOneTimeTokenSubmitPageGeneratingFilter and listens to GET /login/ott.
The URL can also be changed, like so:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
defaultSubmitPageUrl = "/ott/submit"
}
}
return http.build()
}
}
-
Java
-
Kotlin
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
Disabling the Default Submit Page
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my-ott-submit").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/my-ott-submit", authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
return http.build()
}
}
-
Java
-
Kotlin
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
-
Java
-
Kotlin
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
Customize How to Generate and Consume One-Time Tokens
The interface that define the common operations for generating and consuming one-time tokens is the OneTimeTokenService.
Spring Security uses the InMemoryOneTimeTokenService as the default implementation of that interface, if none is provided.
For production environments consider using JdbcOneTimeTokenService.
Some of the most common reasons to customize the OneTimeTokenService are, but not limited to:
-
Changing the one-time token expire time
-
Storing more information from the generate token request
-
Changing how the token value is created
-
Additional validation when consuming a one-time token
There are two options to customize the OneTimeTokenService.
One option is to provide it as a bean, so it can be automatically be picked-up by the oneTimeTokenLogin() DSL:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OneTimeTokenServiceBeanSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
@Configuration
@EnableWebSecurity
open class OneTimeTokenServiceBeanSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
@Bean
open fun oneTimeTokenService(): OneTimeTokenService {
return MyCustomOneTimeTokenService()
}
}
-
Java
-
Kotlin
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
The second option is to pass the OneTimeTokenService instance to the DSL, which is useful if there are multiple SecurityFilterChain and a different OneTimeTokenService is needed for each of them.
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class OneTimeTokenServiceDSLSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class OneTimeTokenServiceDSLSecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
tokenService = MyCustomOneTimeTokenService()
}
}
return http.build()
}
}
-
Java
-
Kotlin
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) {
// ...
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
// ...
}
}
Customize GenerateOneTimeTokenRequest Instance
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
You can customize elements of GenerateOneTimeTokenRequest by publishing an GenerateOneTimeTokenRequestResolver as a @Bean, like so:
-
Java
-
Kotlin
@Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
return (request) -> {
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofMinutes(10));
};
}
@Bean
open fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
return DefaultGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
}
Validating Account Status
By default, OneTimeTokenAuthenticationProvider does not validate the status of the authenticated account — for example, whether the account is locked, disabled, or expired.
This keeps the provider’s behavior consistent with earlier versions of Spring Security; this default may change in future versions of Spring Security.
If you would like OneTimeTokenAuthenticationProvider to reject One-Time Token authentication for such accounts, provide it a UserDetailsChecker, such as AccountStatusUserDetailsChecker, with setUserDetailsChecker:
-
Java
-
Kotlin
@Bean
OneTimeTokenAuthenticationProvider oneTimeTokenAuthenticationProvider(OneTimeTokenService oneTimeTokenService,
UserDetailsService userDetailsService) {
OneTimeTokenAuthenticationProvider provider = new OneTimeTokenAuthenticationProvider(oneTimeTokenService,
userDetailsService);
provider.setUserDetailsChecker(new AccountStatusUserDetailsChecker());
return provider;
}
@Bean
fun oneTimeTokenAuthenticationProvider(
oneTimeTokenService: OneTimeTokenService,
userDetailsService: UserDetailsService
): OneTimeTokenAuthenticationProvider {
val provider = OneTimeTokenAuthenticationProvider(oneTimeTokenService, userDetailsService)
provider.setUserDetailsChecker(AccountStatusUserDetailsChecker())
return provider
}