[[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.server.authentication.ott.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 - OTT: The javadoc:org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService#generate(org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest)[] method requires a javadoc:org.springframework.security.authentication.ott.OneTimeToken[], wrapped in Mono, 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]] == 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.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] 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.server.authentication.ott.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 [tabs] ====== Java:: + [source,java,role="primary"] ---- @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 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"; } } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @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 { 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-generate-url]] == Changing the One-Time Token Generate URL By default, the javadoc:org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter[] 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 @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 { // ... } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @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-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.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter[] 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 @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 { // ... } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @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-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 @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 { // ... } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @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-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.reactive.ReactiveOneTimeTokenService[]. Spring Security uses the javadoc:org.springframework.security.authentication.ott.reactive.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 [tabs] ====== Java:: + [source,java,role="primary"] ---- @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 { // ... } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @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 ``SecurityWebFilterChain``s and a different ``ReactiveOneTimeTokenService``s is needed for each of them. .Passing the ReactiveOneTimeTokenService using the DSL [tabs] ====== Java:: + [source,java,role="primary"] ---- @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 { // ... } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @Configuration @EnableWebFluxSecurity class SecurityConfig { open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http { //.. .formLogin { } oneTimeTokenLogin { oneTimeTokenService = MyCustomReactiveOneTimeTokenService() } } } } @Component class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler { // ... } ---- ====== [[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 ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so: [tabs] ====== Java:: + [source,java,role="primary"] ---- @Bean ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() { DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver(); resolver.setExpiresIn(Duration.ofSeconds(600)); return resolver; } ---- Kotlin:: + [source,kotlin,role="secondary"] ---- @Bean fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver { return DefaultServerGenerateOneTimeTokenRequestResolver().apply { this.setExpiresIn(Duration.ofMinutes(10)) } } ---- ======