This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 6.3.3!

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 GeneratedOneTimeTokenHandler 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.

Token Generation

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.

  1. 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.

  2. 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

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 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 GeneratedOneTimeTokenHandler 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.

One-Time Token Login Configuration
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, MagicLinkGeneratedOneTimeTokenHandler magicLinkSender) {
        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 MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {

    private final MailSender mailSender;

    private final GeneratedOneTimeTokenHandler redirectHandler = new RedirectGeneratedOneTimeTokenHandler("/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";
    }

}
@Configuration
@EnableWebSecurity
class SecurityConfig {

        @Bean
        open fun filterChain(
            http: HttpSecurity,
            magicLinkSender: MagicLinkGeneratedOneTimeTokenSuccessHandler?
        ): SecurityFilterChain {
            http{
                formLogin {}
                oneTimeTokenLogin {  }
            }
            return http.build()
        }
}

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

@Component (1)
class MagicLinkGeneratedOneTimeTokenSuccessHandler(
    private val mailSender: MailSender,
    private val redirectHandler: GeneratedOneTimeTokenHandler = RedirectGeneratedOneTimeTokenHandler("/ott/sent")
) : GeneratedOneTimeTokenHandler {

    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"
    }
}
1 Make the MagicLinkGeneratedOneTimeTokenHandler 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 JavaMailSender API to send the email to the user with the magic link
5 Use the RedirectGeneratedOneTimeTokenHandler 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 generateTokenUrl(String) DSL method:

Changing the Generate URL
  • Java

  • Kotlin

@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 MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {
    // ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {

        @Bean
        open fun filterChain(http: HttpSecurity): SecurityFilterChain {
            http {
                //...
                formLogin { }
                oneTimeTokenLogin {
                    generateTokenUrl = "/ott/my-generate-url"
                }
            }
            return http.build()
        }
}

@Component
class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
     // ...
}

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:

Configuring the Default Submit Page URL
  • Java

  • Kotlin

@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 MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {
    // ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {

        @Bean
        open fun filterChain(http: HttpSecurity): SecurityFilterChain {
            http {
                //...
                formLogin { }
                oneTimeTokenLogin {
                    submitPageUrl = "/ott/submit"
                }
            }
            return http.build()
        }
}

@Component
class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
     // ...
}

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.

Disabling the Default Submit Page
  • 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();
    }

}

@Controller
public class MyController {

    @GetMapping("/my-ott-submit")
    public String ottSubmitPage() {
        return "my-ott-submit";
    }

}

@Component
public class MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {
    // ...
}
@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 MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
     // ...
}

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.

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:

Passing the OneTimeTokenService as a Bean
  • Java

  • Kotlin

@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 MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {
    // ...
}
@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 MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
     // ...
}

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.

Passing the OneTimeTokenService using the DSL
  • Java

  • Kotlin

@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 MagicLinkGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenSuccessHandler {
    // ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            //...
            formLogin { }
            oneTimeTokenLogin {
                oneTimeTokenService = MyCustomOneTimeTokenService()
            }
        }
        return http.build()
    }

}

@Component
class MagicLinkGeneratedOneTimeTokenSuccessHandler : GeneratedOneTimeTokenHandler {
     // ...
}