How to Add Passkey Authentication to Spring Boot with Spring Security 7.1
This guide demonstrates integrating Passkey/WebAuthn authentication into a Spring Boot application using Spring Security 7.1's built-in support, covering dependency setup, security configuration, frontend JavaScript implementation for credential registration and authentication, database persistence with JDBC repositories, and a migration strategy that retains password login while adding Passkey as a second factor.
Background: Legacy Login Complexity
The author describes a typical backend login module that started with username/password and gradually accumulated SMS verification, login failure limits, password reset, CAPTCHA, password strength checks, and step-up authentication for high-risk operations. The login feature creates little business value but the codebase grows, especially around password handling: forgotten passwords, strength enforcement, BCrypt storage, brute-force and credential-stuffing protection, plus SMS vendor integration.
Why Passkey?
Passkey (WebAuthn) uses asymmetric cryptography: the website stores a public key while the private key remains on the user's device or password manager. During login the browser calls navigator.credentials.get() and the server verifies the signature — no password is transmitted. Spring Security 7.1 now includes first-class WebAuthn support, encapsulating both registration and authentication flows.
Technology Stack
Java 25
Spring Boot 4.1.1
Spring Security 7.1.x
MySQL 8
Chrome / Safari
Dependencies
Add to pom.xml:
org.springframework.security
spring-security-webauthnFor MySQL persistence also add:
org.springframework.boot
spring-boot-starter-jdbc
com.mysql
mysql-connector-j
runtimeSpring Boot manages the Spring Security version, so no explicit version for spring-security-webauthn is needed.
Security Configuration
The core Passkey configuration is a few lines in the SecurityFilterChain bean:
.webAuthn(webAuthn -> webAuthn
.rpName("Passkey Demo")
.rpId("localhost")
.allowedOrigins("http://localhost:8080"))rpName is the site name shown in the OS Passkey prompt. rpId and allowedOrigins are critical: for production https://login.example.com use rpId("example.com") and allowedOrigins("https://login.example.com"). Never use allowedOrigins("*") — WebAuthn relies on origin binding to prevent phishing. Local development works with http://localhost because browsers treat localhost as a secure context.
Built-in Endpoints
Spring Security provides standard endpoints; no custom controllers are required:
Registration: POST /webauthn/register/options → navigator.credentials.create() → POST /webauthn/register Authentication: POST /webauthn/authenticate/options → navigator.credentials.get() → POST /login/webauthn The author emphasizes that security protocols should be delegated to mature frameworks rather than re-implemented.
Frontend Implementation
CSRF Token Endpoint
Because the option endpoints are POST and mutate server state, they require CSRF tokens. A simple controller exposes the token:
@RestController
public class CsrfController {
@GetMapping("/api/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}Base64URL Conversion Utilities
WebAuthn uses binary fields (challenge, user.id, credential.id) transmitted as Base64URL strings. Two helpers convert between Base64URL and ArrayBuffer:
function base64UrlToBuffer(value) {
const padding = "=".repeat((4 - value.length % 4) % 4);
const base64 = (value + padding).replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;
}
function bufferToBase64Url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
bytes.forEach(byte => binary += String.fromCharCode(byte));
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}Register Passkey (after password login)
The user is already authenticated via password, then binds a Passkey in "Security Settings". The flow:
Fetch CSRF token.
POST to /webauthn/register/options with CSRF header.
Convert challenge, user.id, and excludeCredentials[].id from Base64URL to buffers.
Call navigator.credentials.create({ publicKey: options }) — browser shows OS authenticator UI (Touch ID, Face ID, etc.).
Convert the returned credential fields back to Base64URL and POST to /webauthn/register with CSRF header.
Full code is included in the article.
Login with Passkey
Similar flow but uses navigator.credentials.get() and posts to /login/webauthn. The server verifies challenge, authenticatorData, clientDataJSON, and signature. On success Spring Security establishes a normal Authentication object, so existing controllers like @GetMapping("/api/me") using Authentication or Principal continue working unchanged.
Database Persistence
By default Spring Security stores credentials in memory; a restart loses them. Two JDBC repositories are provided: JdbcPublicKeyCredentialUserEntityRepository — stores the WebAuthn User Entity. JdbcUserCredentialRepository — stores the credential: public key, signature counter, transports, label, creation time.
Configuration:
@Configuration
public class PasskeyRepositoryConfig {
@Bean
JdbcPublicKeyCredentialUserEntityRepository publicKeyCredentialUserEntityRepository(JdbcOperations jdbc) {
return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
}
@Bean
JdbcUserCredentialRepository userCredentialRepository(JdbcOperations jdbc) {
return new JdbcUserCredentialRepository(jdbc);
}
}DataSource properties (example):
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/passkey_demo
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.DriverThe author recommends migrating the official schema into versioned Flyway/Liquibase scripts (e.g., V2__create_webauthn_user.sql, V3__create_user_credentials.sql) rather than relying on auto-DDL, so schema changes are controlled during Spring Security upgrades.
Migration Strategy: Coexistence, Not Replacement
The Passkey repositories only link a business account to a WebAuthn credential. The existing UserDetailsService still loads users from the application's own tables ( sys_user, user_account, etc.). The migration approach:
Keep original user table, roles, permissions, JWT/Session infrastructure.
Add a second authentication entry point: Password + Passkey.
Observe Passkey adoption before considering password removal.
Account recovery remains essential (lost device, broken sync).
Resulting architecture:
┌─ Password
User Login ────────────┤
└─ Passkey
↓
Spring Security
↓
Authentication
↓
Session / JWT
↓
Original Authorization SystemAnnotations like @PreAuthorize, hasRole(), and SecurityContextHolder are untouched.
Spring Security 7.1 Enhancements
Spring Security 7.1 further supports conditional multi-factor authentication, including conditions based on whether a user has registered a WebAuthn credential — indicating Passkey is now a first-class citizen, not an experimental add-on.
Conclusion
Much of the traditional login code exists to patch password weaknesses: reset flows, rate limiting, rotation policies, complexity rules, SMS second factors. Passkey shifts the model: the server never sees a reusable secret; it only verifies a signature generated by a user gesture on a trusted device. For Spring Boot projects, the key change is that WebAuthn integration has moved from assembling WebAuthn4J, filters, converters, and custom repositories into a single http.webAuthn(...) DSL configuration. The author recommends starting a branch to trial Passkey alongside password login, focusing engineering effort on account systems, recovery mechanisms, permissions, and UX rather than cryptographic plumbing.
References
Spring Boot: https://spring.io/projects/spring-boot/
Spring Security Passkeys: https://docs.spring.io/spring-security/reference/servlet/authentication/passkeys.html
Spring Security What's New: https://docs.spring.io/spring-security/reference/whats-new.html
MDN Web Authentication API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
MDN Secure Contexts: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
