Max Batischev d37d41c130 Polish One-Time Token API Names and Doc
The names of variables and methods have been adjusted in accordance with the names of the one-time token login API components.

Issue gh-15114
2024-10-15 14:04:56 -07:00

549 lines
17 KiB
Plaintext

[[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,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.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<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";
}
}
----
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<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-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 {
// ...
}
----
======