Spring Boot Tutorial
Passkeys and One-Time Token Login
Passwords are the weakest part of most login systems: users reuse them, phishing sites steal them, and databases leak them. The industry is moving to passwordless authentication, and Spring Security now supports the two most common approaches out of the box.
Passkeys (WebAuthn) let users sign in with a fingerprint, face scan or device PIN, using a public/private key pair that never leaves their device and cannot be phished. One-time token (OTT) login — often called "magic links" — emails the user a single-use link. This lesson shows how to enable both in Spring Boot 4 with Spring Security 7.
How Passkeys Work
During registration, the user's device (phone, laptop, security key) creates a new key pair for your site. The private key stays in the device's secure hardware; your server stores only the public key and a credential id. During login, your server sends a random challenge, the device signs it after the user unlocks it with biometrics or a PIN, and the server verifies the signature with the stored public key.
Because the browser binds each credential to your exact domain (the "relying party id"), a look-alike phishing site cannot use it. There is no shared secret for attackers to steal from your database.
Enabling Passkeys in Spring Security
Add the WebAuthn4J dependency and call http.webAuthn(...) with your relying party name, id and allowed origins. Spring Security then provides the registration page at /webauthn/register, the JavaScript and endpoints for the browser ceremony, and a "Sign in with a passkey" button on the default login page. Users must first log in another way (for example with a password or OTT) to register a passkey for their account.
For production, store credentials in the database using JdbcPublicKeyCredentialUserEntityRepository and JdbcUserCredentialRepository; the default in-memory stores lose all passkeys on restart.
One-Time Token Login
With http.oneTimeTokenLogin(...), the login page shows a "Send me a login link" form. Spring Security generates a random token that expires after five minutes by default and passes it to your OneTimeTokenGenerationSuccessHandler. Your handler is responsible for delivering the link — usually by email or SMS. When the user opens the link and submits it, the token is consumed and the user is logged in.
Use JdbcOneTimeTokenService in production so tokens work across multiple instances.
Multi-Factor Authentication
Spring Security 7 adds first-class support for requiring more than one factor. You can declare which authorities a request needs — for example one granted by password login and one granted by OTT — and Spring Security will redirect users who have completed only one factor to complete the next. This lets you require a second factor for sensitive areas such as /admin/** without writing a custom flow.
Examples
Enabling passkeys
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
<!-- Spring Boot does not manage this version: use the one listed in the
Spring Security "Passkeys" reference page for your release -->
<version>${webauthn4j.version}</version>
</dependency>
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/**", "/webauthn/**").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.webAuthn(webAuthn -> webAuthn
.rpName("Webnest Studio")
.rpId("webneststudio.co.in") // use "localhost" in development
.allowedOrigins("https://www.webneststudio.co.in"));
return http.build();
}
// Production storage for passkeys
@Bean
PublicKeyCredentialUserEntityRepository passkeyUsers(JdbcOperations jdbc) {
return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
}
@Bean
UserCredentialRepository passkeyCredentials(JdbcOperations jdbc) {
return new JdbcUserCredentialRepository(jdbc);
}
1. Log in with a password, then visit /webauthn/register -> "Register" -> Windows Hello / Touch ID prompt
2. Log out; on /login click "Sign in with a passkey" -> device prompt -> 302 / (authenticated, no password typed)
One-time token (magic link) login delivered by email
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/**", "/ott/sent").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Component
public class MagicLinkSender implements OneTimeTokenGenerationSuccessHandler {
private final JavaMailSender mail;
private final RedirectStrategy redirect = new DefaultRedirectStrategy();
public MagicLinkSender(JavaMailSender mail) {
this.mail = mail;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
OneTimeToken token) throws IOException {
String link = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath() + "/login/ott")
.replaceQuery(null)
.queryParam("token", token.getTokenValue())
.toUriString();
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(token.getUsername()); // username is the user's email in this app
msg.setSubject("Your Webnest login link");
msg.setText("Sign in within 5 minutes: " + link);
mail.send(msg);
redirect.sendRedirect(request, response, "/ott/sent");
}
}
@Bean
OneTimeTokenService oneTimeTokenService(JdbcOperations jdbc) {
return new JdbcOneTimeTokenService(jdbc); // survives restarts, works with multiple instances
}
POST /ott/generate username=asha@webnest.in -> 302 /ott/sent
Email: "Sign in within 5 minutes: https://www.webneststudio.co.in/login/ott?token=4f9c1e..."
Open link and submit -> 302 / (authenticated as asha@webnest.in)
Open the same link again -> 302 /login?error (token already used)
Common Mistakes
- Setting rpId to a different domain than the one users visit — browsers refuse to create or use the passkey.
- Testing passkeys over plain http on a non-localhost host; WebAuthn requires a secure context (HTTPS or localhost).
- Keeping passkeys or one-time tokens in the default in-memory stores in production.
- Revealing whether an email address has an account on the "send me a link" page; always show the same "check your inbox" message.
- Removing password login before users have registered passkeys, locking them out with no recovery path.
Key Points to Remember
- Passkeys use device-held private keys and domain-bound credentials, making them resistant to phishing and database leaks.
- http.webAuthn(...) adds passkey registration and login; store credentials with the JDBC repositories.
- http.oneTimeTokenLogin(...) adds magic-link login; you implement OneTimeTokenGenerationSuccessHandler to deliver the link.
- Spring Security 7 supports multi-factor requirements so sensitive areas can demand a second factor.
- Always provide a fallback and recovery path when introducing passwordless login.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.