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.
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).
-
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.
-
OTT: Usually a custom javadoc:org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler[] must 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.
-
OTT: The javadoc:org.springframework.security.authentication.ott.OneTimeTokenService#generate(org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest)[] method requires a javadoc:org.springframework.security.authentication.ott.OneTimeToken[] to 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.
The oneTimeTokenLogin()
DSL can be used in conjunction with formLogin()
, which will produce an additional One-Time Token Request Form in the default generated login page.
It will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
It is not possible for Spring Security to reasonably determine the way the token should be delivered to your users. Therefore, a custom javadoc:org.springframework.security.web.authentication.ott.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
-
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); return http.build(); } } import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @Component (1) public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { private final MailSender mailSender; private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent"); // constructor omitted @Override public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(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) this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); (4) this.redirectHandler.handle(request, response, oneTimeToken); (5) } private String getUserEmail() { // ... } } @Controller class PageController { @GetMapping("/ott/sent") String ottSent() { return "my-template"; } }
- Kotlin
-
@Configuration @EnableWebSecurity class SecurityConfig { @Bean open fun filterChain(http: HttpSecurity): SecurityFilterChain { http{ formLogin {} oneTimeTokenLogin { } } return http.build() } } import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @Component (1) class MagicLinkOneTimeTokenGenerationSuccessHandler( private val mailSender: MailSender, private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent") ) : OneTimeTokenGenerationSuccessHandler { override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) { val builder = UriComponentsBuilder.fromHttpUrl(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) this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4) this.redirectHandler.handle(request, response, oneTimeToken) (5) } private fun getUserEmail(): String { // ... } } @Controller class PageController { @GetMapping("/ott/sent") fun ottSent(): String { return "my-template" } }
-
Make the
MagicLinkOneTimeTokenGenerationSuccessHandler
a Spring bean -
Create a login processing URL with the
token
as a query param -
Retrieve the user’s email based on the username
-
Use the
JavaMailSender
API to send the email to the user with the magic link -
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.
By default, the javadoc:org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter[] listens to POST /ott/generate
requests.
That URL can be changed by using the generateTokenUrl(String)
DSL method:
- Java
-
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin((ott) -> ott .generateTokenUrl("/ott/my-generate-url") ); return http.build(); } } @Component public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { // ... }
- Kotlin
-
@Configuration @EnableWebSecurity class SecurityConfig { @Bean open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { //... formLogin { } oneTimeTokenLogin { generateTokenUrl = "/ott/my-generate-url" } } return http.build() } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler { // ... }
The default One-Time Token submit page is generated by the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] and listens to GET /login/ott
.
The URL can also be changed, like so:
- Java
-
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin((ott) -> ott .submitPageUrl("/ott/submit") ); return http.build(); } } @Component public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { // ... }
- Kotlin
-
@Configuration @EnableWebSecurity class SecurityConfig { @Bean open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { //... formLogin { } oneTimeTokenLogin { submitPageUrl = "/ott/submit" } } return http.build() } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler { // ... }
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
-
@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(); } } @Controller public class MyController { @GetMapping("/my-ott-submit") public String ottSubmitPage() { return "my-ott-submit"; } } @Component public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { // ... }
- Kotlin
-
@Configuration @EnableWebSecurity 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() } } @Controller class MyController { @GetMapping("/my-ott-submit") fun ottSubmitPage(): String { return "my-ott-submit" } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler { // ... }
The interface that define the common operations for generating and consuming one-time tokens is the javadoc:org.springframework.security.authentication.ott.OneTimeTokenService[]. Spring Security uses the javadoc:org.springframework.security.authentication.ott.InMemoryOneTimeTokenService[] as the default implementation of that interface, if none is provided. For production environments consider using javadoc:org.springframework.security.authentication.ott.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
-
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin(Customizer.withDefaults()); return http.build(); } @Bean public OneTimeTokenService oneTimeTokenService() { return new MyCustomOneTimeTokenService(); } } @Component public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { // ... }
- Kotlin
-
@Configuration @EnableWebSecurity class SecurityConfig { @Bean open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { //... formLogin { } oneTimeTokenLogin { } } return http.build() } @Bean open fun oneTimeTokenService(): OneTimeTokenService { return MyCustomOneTimeTokenService() } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler { // ... }
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
-
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http // ... .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin((ott) -> ott .oneTimeTokenService(new MyCustomOneTimeTokenService()) ); return http.build(); } } @Component public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler { // ... }
- Kotlin
-
@Configuration @EnableWebSecurity class SecurityConfig { @Bean open fun filterChain(http: HttpSecurity): SecurityFilterChain { http { //... formLogin { } oneTimeTokenLogin { oneTimeTokenService = MyCustomOneTimeTokenService() } } return http.build() } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler { // ... }