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

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 ServerOneTimeTokenGenerationSuccessHandler 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 OneTimeTokenSubmitPageGeneratingWebFilter 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 ServerOneTimeTokenGenerationSuccessHandler 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
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity 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 ServerOneTimeTokenGenerationSuccessHandler {

    private final MailSender mailSender;

    private final ServerOneTimeTokenGenerationSuccessHandler redirectHandler = new ServerRedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");

    // constructor omitted

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
        return Mono.just(exchange.getRequest())
				.map((request) ->
					UriComponentsBuilder.fromUri(request.getURI())
						.replacePath(request.getPath().contextPath().value())
						.replaceQuery(null)
						.fragment(null)
						.path("/login/ott")
						.queryParam("token", oneTimeToken.getTokenValue())
						.toUriString() (2)
					)
			.flatMap((uri) -> this.mailSender.send(getUserEmail(oneTimeToken.getUsername()), (3)
					"Use the following link to sign in into the application: " + magicLink)) (4)
			.then(this.redirectHandler.handle(exchange, oneTimeToken)); (5)
    }

    private String getUserEmail() {
        // ...
    }

}

@Controller
class PageController {

    @GetMapping("/ott/sent")
    String ottSent() {
        return "my-template";
    }

}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                authorizeExchange {
                    authorize(anyExchange, authenticated)
                 }
                 oneTimeTokenLogin { }
             }
         }

}

@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {

    private val redirectStrategy: ServerRedirectStrategy = DefaultServerRedirectStrategy()

    override fun handle(exchange: ServerWebExchange, oneTimeToken: OneTimeToken): Mono<Void> {
        val builder = UriComponentsBuilder.fromUri(exchange.request.uri)
            .replacePath(null)
            .replaceQuery(null)
            .fragment(null)
            .path("/login/ott")
            .queryParam("token", oneTimeToken.getTokenValue()) (2)
        val magicLink = builder.toUriString()
        builder.replacePath(null)
            .replaceQuery(null)
            .path("/ott/sent")
        val redirectLink = builder.toUriString()
        return this.mailSender.send(
            getUserEmail(oneTimeToken.getUsername()), (3)
            "Use the following link to sign in into the application: $magicLink") (4)
        .then(this.redirectStrategy.sendRedirect(exchange, URI.create(redirectLink))) (5)
    }

        private String getUserEmail() {
            // ...
        }
}

@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 ServerRedirectStrategy 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 GenerateOneTimeTokenWebFilter 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
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        http
            // ...
            .formLogin(Customizer.withDefaults())
            .oneTimeTokenLogin((ott) -> ott
                .generateTokenUrl("/ott/my-generate-url")
            );
        return http.build();
    }

}

@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                 // ...
                 formLogin { }
                 oneTimeTokenLogin {
                    generateTokenUrl = "/ott/my-generate-url"
                 }
             }
         }

}

@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}

Changing the Default Submit Page URL

The default One-Time Token submit page is generated by the OneTimeTokenSubmitPageGeneratingWebFilter and listens to GET /login/ott. The URL can also be changed, like so:

Configuring the Default Submit Page URL
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        http
            // ...
            .formLogin(Customizer.withDefaults())
            .oneTimeTokenLogin((ott) -> ott
                .submitPageUrl("/ott/submit")
            );
        return http.build();
    }

}

@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                 // ...
                 formLogin { }
                 oneTimeTokenLogin {
                    submitPageUrl = "/ott/submit"
                 }
             }
         }

}

@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}

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
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange((authorize) -> authorize
                .pathMatchers("/my-ott-submit").permitAll()
                .anyExchange().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 MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                authorizeExchange {
                    authorize(pathMatchers("/my-ott-submit"), permitAll)
                    authorize(anyExchange, authenticated)
                 }
                 .formLogin { }
                 oneTimeTokenLogin {
                    showDefaultSubmitPage = false
                 }
             }
         }

}

@Controller
class MyController {

    @GetMapping("/my-ott-submit")
    fun ottSubmitPage(): String {
        return "my-ott-submit"
    }
}

@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}

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 ReactiveOneTimeTokenService. Spring Security uses the InMemoryReactiveOneTimeTokenService as the default implementation of that interface, if none is provided.

Some of the most common reasons to customize the ReactiveOneTimeTokenService 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 ReactiveOneTimeTokenService. One option is to provide it as a bean, so it can be automatically be picked-up by the oneTimeTokenLogin() DSL:

Passing the ReactiveOneTimeTokenService as a Bean
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        http
            // ...
            .formLogin(Customizer.withDefaults())
            .oneTimeTokenLogin(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public ReactiveOneTimeTokenService oneTimeTokenService() {
        return new MyCustomReactiveOneTimeTokenService();
    }

}

@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                 //..
                 .formLogin { }
                 oneTimeTokenLogin { }
             }
         }

         @Bean
         open fun oneTimeTokenService():ReactiveOneTimeTokenService {
             return MyCustomReactiveOneTimeTokenService();
         }

}

@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}

The second option is to pass the ReactiveOneTimeTokenService instance to the DSL, which is useful if there are multiple SecurityWebFilterChains and a different ReactiveOneTimeTokenServices is needed for each of them.

Passing the ReactiveOneTimeTokenService using the DSL
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        http
            // ...
            .formLogin(Customizer.withDefaults())
            .oneTimeTokenLogin((ott) -> ott
                .oneTimeTokenService(new MyCustomReactiveOneTimeTokenService())
            );
        return http.build();
    }

}

@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
             return http {
                 //..
                 .formLogin { }
                 oneTimeTokenLogin {
                    oneTimeTokenService = MyCustomReactiveOneTimeTokenService()
                 }
             }
         }
}

@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {
    // ...
}