| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 | [[one-time-token-login]]= One-Time Token LoginSpring 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 PasswordsIt'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,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 PageThe `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 UserIt 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@EnableWebFluxSecuritypublic 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() {        // ...    }}@Controllerclass PageController {    @GetMapping("/ott/sent")    String ottSent() {        return "my-template";    }}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass 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() {            // ...        }}@Controllerclass 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 URLThe 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-6aba7b22fe5bThe 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 URLBy 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@EnableWebFluxSecuritypublic class SecurityConfig {    @Bean    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {        http            // ...            .formLogin(Customizer.withDefaults())            .oneTimeTokenLogin((ott) -> ott                .generateTokenUrl("/ott/my-generate-url")            );        return http.build();    }}@Componentpublic class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {    // ...}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass SecurityConfig {         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {             return http {                 // ...                 formLogin { }                 oneTimeTokenLogin {                    generateTokenUrl = "/ott/my-generate-url"                 }             }         }}@Componentclass MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {    // ...}----======[[changing-submit-page-url]]== Changing the Default Submit Page URLThe 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@EnableWebFluxSecuritypublic class SecurityConfig {    @Bean    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {        http            // ...            .formLogin(Customizer.withDefaults())            .oneTimeTokenLogin((ott) -> ott                .submitPageUrl("/ott/submit")            );        return http.build();    }}@Componentpublic class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {    // ...}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass SecurityConfig {         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {             return http {                 // ...                 formLogin { }                 oneTimeTokenLogin {                    submitPageUrl = "/ott/submit"                 }             }         }}@Componentclass MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {    // ...}----======[[disabling-default-submit-page]]== Disabling the Default Submit PageIf 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@EnableWebFluxSecuritypublic 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();    }}@Controllerpublic class MyController {    @GetMapping("/my-ott-submit")    public String ottSubmitPage() {        return "my-ott-submit";    }}@Componentpublic class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {    // ...}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass SecurityConfig {         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {             return http {                authorizeExchange {                    authorize(pathMatchers("/my-ott-submit"), permitAll)                    authorize(anyExchange, authenticated)                 }                 .formLogin { }                 oneTimeTokenLogin {                    showDefaultSubmitPage = false                 }             }         }}@Controllerclass MyController {    @GetMapping("/my-ott-submit")    fun ottSubmitPage(): String {        return "my-ott-submit"    }}@Componentclass MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {    // ...}----======[[customize-generate-consume-token]]== Customize How to Generate and Consume One-Time TokensThe 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 tokenThere 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@EnableWebFluxSecuritypublic 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();    }}@Componentpublic class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {    // ...}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass SecurityConfig {         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {             return http {                 //..                 .formLogin { }                 oneTimeTokenLogin { }             }         }         @Bean         open fun oneTimeTokenService():ReactiveOneTimeTokenService {             return MyCustomReactiveOneTimeTokenService();         }}@Componentclass 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@EnableWebFluxSecuritypublic class SecurityConfig {    @Bean    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {        http            // ...            .formLogin(Customizer.withDefaults())            .oneTimeTokenLogin((ott) -> ott                .oneTimeTokenService(new MyCustomReactiveOneTimeTokenService())            );        return http.build();    }}@Componentpublic class MagicLinkOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler {    // ...}----Kotlin::+[source,kotlin,role="secondary"]----@Configuration@EnableWebFluxSecurityclass SecurityConfig {         open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {             return http {                 //..                 .formLogin { }                 oneTimeTokenLogin {                    oneTimeTokenService = MyCustomReactiveOneTimeTokenService()                 }             }         }}@Componentclass MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ServerOneTimeTokenGenerationSuccessHandler {    // ...}----======
 |