You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
580 lines
18 KiB
580 lines
18 KiB
[[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 { |
|
// ... |
|
} |
|
|
|
---- |
|
====== |
|
|
|
[[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)) |
|
} |
|
} |
|
---- |
|
======
|
|
|