Add support for One-Time Token Login
Closes gh-15114
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
|
||||
*** xref:servlet/authentication/persistence.adoc[Persistence]
|
||||
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
|
||||
*** xref:servlet/authentication/session-management.adoc[Session Management]
|
||||
*** xref:servlet/authentication/rememberme.adoc[Remember Me]
|
||||
*** xref:servlet/authentication/anonymous.adoc[Anonymous]
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
[[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.authentication.ott.GeneratedOneTimeTokenHandler[] 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.OneTimeTokenService#generate(org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest)[] method requires a javadoc:org.springframework.security.authentication.ott.OneTimeToken[] 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.
|
||||
|
||||
[[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.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
|
||||
|
||||
In the following sections we will explore how to configure OTT Login for your needs.
|
||||
|
||||
- <<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>>
|
||||
|
||||
[[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.authentication.ott.GeneratedOneTimeTokenHandler[] 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
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http, MagicLinkGeneratedOneTimeTokenSuccessHandler magicLinkSender) {
|
||||
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 MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
|
||||
|
||||
private final MailSender mailSender;
|
||||
|
||||
private final GeneratedOneTimeTokenSuccessHandler redirectHandler = new RedirectGeneratedOneTimeTokenSuccessHandler("/ott/sent");
|
||||
|
||||
// constructor omitted
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
|
||||
.replacePath(request.getContextPath())
|
||||
.replaceQuery(null)
|
||||
.fragment(null)
|
||||
.path("/login/ott")
|
||||
.queryParam("token", oneTimeToken.getTokenValue()); <2>
|
||||
String magicLink = builder.toUriString();
|
||||
String email = getUserEmail(oneTimeToken.getUsername()); <3>
|
||||
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); <4>
|
||||
this.redirectHandler.handle(request, response, oneTimeToken); <5>
|
||||
}
|
||||
|
||||
private String getUserEmail() {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
class PageController {
|
||||
|
||||
@GetMapping("/ott/sent")
|
||||
String ottSent() {
|
||||
return "my-template";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
----
|
||||
======
|
||||
|
||||
<1> Make the `MagicLinkGeneratedOneTimeTokenSuccessHandler` 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 `JavaMailSender` API to send the email to the user with the magic link
|
||||
<5> Use the `RedirectGeneratedOneTimeTokenSuccessHandler` 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.authentication.ott.GenerateOneTimeTokenFilter[] 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
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
// ...
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.generateTokenUrl("/ott/my-generate-url")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[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.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] 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
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
// ...
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.oneTimeTokenLogin((ott) -> ott
|
||||
.submitPageUrl("/ott/submit")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[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
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers("/my-ott-submit").permitAll()
|
||||
.anyRequest().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 MagicLinkGeneratedOneTimeTokenSuccessHandler implements GeneratedOneTimeTokenSuccessHandler {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -187,6 +187,10 @@ fun app(val http: HttpSecurity): SecurityFilterChain {
|
||||
======
|
||||
You can read more https://github.com/spring-projects/spring-security/issues/15220[in the related ticket].
|
||||
|
||||
== One-Time Token Login
|
||||
|
||||
Spring Security now xref:servlet/authentication/onetimetoken.adoc[supports One-Time Token Login] via the `oneTimeTokenLogin()` DSL.
|
||||
|
||||
== Kotlin
|
||||
|
||||
* The Kotlin DSL now supports https://github.com/spring-projects/spring-security/issues/14935[SAML 2.0] and https://github.com/spring-projects/spring-security/issues/15171[`GrantedAuthorityDefaults`] and https://github.com/spring-projects/spring-security/issues/15136[`RoleHierarchy`] ``@Bean``s
|
||||
|
||||
Reference in New Issue
Block a user