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.

LuTiao Programming
LuTiao Programming
LuTiao Programming
How to Add Passkey Authentication to Spring Boot with Spring Security 7.1

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-webauthn

For MySQL persistence also add:

org.springframework.boot
  spring-boot-starter-jdbc
com.mysql
  mysql-connector-j
  runtime

Spring 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.Driver

The 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 System

Annotations 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootSecurityauthenticationJDBCSpring SecurityWebAuthnPasskey
LuTiao Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.