
Passwordless Authentication Unlocked: One-Time Tokens in Spring Security 6.4

Be sure to check out my previous article on JWT Authentication and Authorization with Spring Boot 3 and Spring Security 6
Spring Security 6.4 just came out with some exciting features, you can check out the release notes if you’re curious. I currently use the latest Spring Boot version (3.4.x) and have previously written about Spring Security 6, so this update really caught my interest. In this article, I’ll focus on one feature that stood out to me: One-Time Token Login.
This article demonstrates how to build a basic web application with authentication using Spring Security. Instead of delving deep into introducing Spring Security concepts, as they are readily available on the official website, I will focus on explaining the project components step by step. At the end, you will find a link to the GitHub repository containing the functional code for reference.
Let’s get started! 💪
Application Architecture
Scenarios

- The user requests to log in to the service.
- The user requests a One-Time Token, which is sent via email using a Magic Link.
- The user clicks the Magic Link to log in.
Flowchart: Secure One-Time Token Login Flow

- The flow begins at the default logic page, generated by Spring Security using formLogin(). The login page includes the One-Time Token Request form (via oneTimeTokenLogin()) for requesting a one-time token.
- The user submits the request to receive a token.
- The generated token (generated by OneTimeTokenGenerationSuccessHandler) in the form of a Magic Link is delivered to the user's email.
- The user clicks the Magic Link to open a submission page (generated by DefaultOneTimeTokenSubmitPageGeneratingFilter) that auto-fills the token if it’s in the URL.
- If authentication fails, show an error.
- If authentication succeeds, grant access to the user.
Source Code Demonstration
For some of the trickier parts, I’ll provide detailed explanations and reference links. However, for the remaining parts, you can find the relevant information on GitHub.
Create Demo Service as a Spring Boot project with the dependencies provided inside the POM file. I have named it spring-security-6.4-one-time-token.
Below is the directory structure of the project. Feel free to set it up as you prefer.
├── java
│ └── github
│ └── io
│ └── truongbn
│ └── spring_security_6
│ └── __one_time_token # Back-end code for the project
│ ├── SpringSecurityOneTimeTokenApp.java
│ ├── config
│ │ └── SecurityConfig.java
│ ├── controller
│ │ └── PageController.java
│ ├── exception
│ │ └── EmailSendException.java
│ ├── handler
│ │ └── OneTimeTokenHandler.java
│ └── service
│ └── EmailService.java
├── jte # Front-end code for the project
│ ├── home.jte
│ └── token-one-time.jte
└── resources # Project configuration properties
└── application.yml
Project Configuration Properties
gg:
jte:
development-mode: true
spring:
application:
name: spring-security-6.4-one-time-token
sendgrid:
api-key: ${API_Key}
This YAML file configures the JTE (Java Template Engine) under the gg namespace. By setting development-mode to true, it enables a development-friendly mode for JTE. In this mode, changes to my templates are usually reloaded automatically, which can help speed up the development process by allowing me to see updates immediately without needing to restart the application.
This YAML file also configures the SendGrid API key, which is used for sending the email. The key is currently a placeholder, so you’ll need to replace it with your own. But don’t worry, I’ll explain everything in detail when I set up the Email Service later in this article.
Front-end Implementation
The web app has two simple pages.
Home page

Token Notification page

You can find the full front-end implementation here
Back-end Implementation
Start with the security configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http)
throws Exception {
return http
.authorizeHttpRequests(auth ->
auth.requestMatchers("/token/one-time").permitAll()
.requestMatchers("/login/ott").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults())
.build();
}
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
final var user = User.withUsername("username")
.password("password")
.build();
return new InMemoryUserDetailsManager(user);
}
}
I use SecurityFilterChain to enable One-Time Token for the web app. The method oneTimeTokenLogin() was introduced in version 6.4 as part of the DSL configuration. With this SecurityFilterChain bean, the web app uses the Default Login Page and Default One-Time Token Submit Page.
I also use InMemoryUserDetailManager bean to define an in-memory user store for authentication. It includes a default user with the username "username" and password "password", without any specific roles or authorities. This setup is intended for testing the web app.
Three new concepts are introduced within this configuration. For more information, please refer to the provided reference links: Default Generated Login Page, Default One-Time Token Submit Page, InMemoryUserDetailManager.
Sending the Token to the User
Spring Security does not have a built-in method to deliver the token to users, so I must provide a custom OneTimeTokenGenerationSuccessHandler to handle token delivery.
@Component
@RequiredArgsConstructor
public class OneTimeTokenHandler implements OneTimeTokenGenerationSuccessHandler {
private final EmailService emailService;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new
RedirectOneTimeTokenGenerationSuccessHandler("/token/one-time");
@Override
public void handle(final HttpServletRequest request,
final HttpServletResponse response,
final OneTimeToken oneTimeToken)
throws IOException, ServletException {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null).fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue());
final String magicLink = builder.toUriString();
final String body = generateEmailBody(magicLink);
// TODO: Add the logic to generate email
final String toEmail = "ToEmail";
emailService.sendEmail(toEmail, "One Time Token Login", body);
this.redirectHandler.handle(request, response, oneTimeToken);
}
private String generateEmailBody(final String magicLink) {
return """
Your login link is ready.
For a safe experience, use this secure link: %s
""".formatted(magicLink);
}
}
The "/login/ott" argument in RedirectOneTimeTokenGenerationSuccessHandler ensures users are redirected to /token/one-time after processing.
The OneTimeTokenGenerationSuccessHandler interface defines a single method, handle(), which takes the current servlet request, response, and a OneTimeToken object.
I use UriComponentsBuilder to create a login processing URL with the token as a query param. AndEmailService to send the email to the user with the Magic Link. Finally, RedirectOneTimeTokenGenerationSuccessHandler handles the redirect to /token/one-time.
Email Service Implementation
To send emails in the web app, I use Twilio SendGrid.
@Service
@RequiredArgsConstructor
public class EmailService {
@Value("${sendgrid.api-key}")
private String sendgridApiKey;
public void sendEmail(final String to, final String subject,
final String content) {
// TODO: Update the from email address
final Email from = new Email("FromEmail");
final Email toEmail = new Email(to);
final Content emailContent = new Content("text/plain", content);
final Mail mail = new Mail(from, subject, toEmail, emailContent);
final SendGrid sg = new SendGrid(sendgridApiKey);
final Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sg.api(request);
} catch (final IOException ex) {
throw new EmailSendException("Failed to send the email", ex);
}
}
}
Since Twilio provides a comprehensive guide for sending emails in Java, I won’t go into details. You can follow their guide, which covers everything you need.
Please note that follow the guide to create the SendGrid API key, then update the key in the application.yml file.
Controller Implementation
@Controller
public class PageController {
@GetMapping()
public String home() {
return "home";
}
@GetMapping("/token/one-time")
public String sendOneTimeToken() {
return "token-one-time";
}
}
This controller is responsible for handling HTTP requests and returning the appropriate views (Pages). It handles two routes:
- The root URL (/) maps to the home page.
- The /token/one-time URL maps to the token-one-time page.
Time to test the Web App
Run the application (it should run on 8080).
Hit the URL http://localhost:8080/login to access the default login page, where I can also find the One-Time Token Request Form.

Enter the username "username" in the One-Time Token Request Form, and then click the "Send Token" button. As you can remember "username" is the default user defined earlier using the InMemoryUserDetailsManager.
It should be redirected to the Token Notification page, confirming that the token has been sent to the user’s email.

Check the inbox for an email similar to the one below, which contains the Magic Link.

Click the Magic Link to open the submission page, which will automatically fill in the token.

Click the “Sign in” button to gain access to the Home page. 😍

I’ve just completed a basic demonstration of authentication using One-Time Token in Spring Security 6.4. I hope it works as expected for you all!
Please don’t hesitate to share your thoughts or concerns in the comments section.
The completed source code can be found in this GitHub repository: https://github.com/buingoctruong/spring-security-6.4-one-time-token