123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- [[one-time-token-login]]
- = 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 https://en.wikipedia.org/wiki/One-time_password[One-Time Passwords] (OTP), but in Spring Security, these concepts differ in several key ways.
- For clarity, we'll assume OTP refers to https://en.wikipedia.org/wiki/Time-based_one-time_password[TOTP] (Time-Based One-Time Password) or https://en.wikipedia.org/wiki/HMAC-based_one-time_password[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 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.
- === Token Generation
- - 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.
- 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-pages,Understanding the integration with the default generated login page>>
- - <<sending-token-to-user,Sending the token to the user>>
- - <<changing-submit-page-url,Configuring the One-Time Token submit page>>
- - <<changing-generate-url,Changing the One-Time Token generate URL>>
- - <<customize-generate-consume-token,Customize how to generate and consume tokens>>
- [[default-pages]]
- == 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 xref:servlet/authentication/passwords/form.adoc[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.
- [[sending-token-to-user]]
- == 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 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.
- .One-Time Token Login Configuration
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @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"
- }
- }
- ----
- ======
- <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 `JavaMailSender` 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-generate-url]]
- == Changing the One-Time Token Generate URL
- 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:
- .Changing the Generate URL
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @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 {
- // ...
- }
- ----
- ======
- [[changing-submit-page-url]]
- == Changing the Default Submit Page URL
- 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:
- .Configuring the Default Submit Page URL
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @Configuration
- @EnableWebSecurity
- class SecurityConfig {
- @Bean
- open fun filterChain(http: HttpSecurity): SecurityFilterChain {
- http {
- //...
- formLogin { }
- oneTimeTokenLogin {
- submitPageUrl = "/ott/submit"
- }
- }
- return http.build()
- }
- }
- @Component
- class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
- // ...
- }
- ----
- ======
- [[disabling-default-submit-page]]
- == 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
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @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 {
- // ...
- }
- ----
- ======
- [[customize-generate-consume-token]]
- == 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 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:
- .Passing the OneTimeTokenService as a Bean
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @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.
- .Passing the OneTimeTokenService using the DSL
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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::
- +
- [source,kotlin,role="secondary"]
- ----
- @Configuration
- @EnableWebSecurity
- class SecurityConfig {
- @Bean
- open fun filterChain(http: HttpSecurity): SecurityFilterChain {
- http {
- //...
- formLogin { }
- oneTimeTokenLogin {
- oneTimeTokenService = MyCustomOneTimeTokenService()
- }
- }
- return http.build()
- }
- }
- @Component
- class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
- // ...
- }
- ----
- ======
- [[customize-generate-token-request]]
- == 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:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Bean
- GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
- DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
- return (request) -> {
- GenerateOneTimeTokenRequest generate = delegate.resolve(request);
- return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
- };
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Bean
- fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
- return DefaultGenerateOneTimeTokenRequestResolver().apply {
- this.setExpiresIn(Duration.ofMinutes(10))
- }
- }
- ----
- ======
|