Spring Security – Integrate Passkeys Example
With the increasing demand for passwordless authentication, passkeys have emerged as a modern, secure alternative. Let us delve into understanding how to integrate passkeys in Spring Security.
1. Introduction to Passkeys
Passkeys are a modern, passwordless authentication method designed to replace traditional passwords with a more secure and user-friendly alternative. Passkeys are based on public-key cryptography, where each user’s device generates a unique key pair consisting of a public key and a private key.
When a user registers with a service using a passkey, the public key is sent to the server and stored, while the private key never leaves the user’s device. During authentication, the server sends a challenge that is signed by the private key, and the signature is verified using the stored public key.
This method makes passkeys inherently resistant to phishing, credential stuffing, and brute-force attacks, since there’s no shared secret that can be reused or stolen. It also eliminates the need to remember complex passwords or use third-party password managers.
Passkeys are built on top of the Web Authentication API (WebAuthn) and the FIDO2 standard, developed by the FIDO Alliance. These standards enable strong, cryptographic authentication on the web and across devices.
Support for passkeys is rapidly growing across major ecosystems. They are natively supported by:
Because passkeys are synced via cloud providers (e.g., iCloud Keychain or Google Password Manager), they can also be used seamlessly across multiple devices, enhancing both convenience and security.
2. Adding Passkeys to Spring Boot Applications
To integrate passkeys, we use the WebAuthn4J library that enables WebAuthn/FIDO2 support for Spring Boot applications.
2.1 Add Dependencies
To start using passkeys in your Spring Boot application, you need to include the necessary dependency for WebAuthn support. The webauthn-spring-security library integrates WebAuthn into Spring Security, enabling secure passwordless authentication using public-key cryptography.
<dependency> <groupId>com.webauthn4j.springframework.security</groupId> <artifactId>webauthn-spring-security</artifactId> <version>latest__jar__version</version> </dependency>
2.2 Main File
This is the entry point of your Spring Boot application. The PasskeyApplication class contains the main method that launches the application using Spring Boot’s auto-configuration mechanism.
package com.example.passkey;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PasskeyApplication {
public static void main(String[] args) {
SpringApplication.run(PasskeyApplication.class, args);
}
}
2.3 Security Configuration File
The security configuration class integrates WebAuthn into Spring Security by applying the WebAuthnSecurityFilterChainConfigurer. It configures CSRF protection, defines open endpoints (like register and authenticate), and ensures authenticated access to other routes.
package com.example.passkey.config;
import com.webauthn4j.springframework.security.WebAuthnSecurityConfigurer;
import com.webauthn4j.springframework.security.WebAuthnSecurityFilterChainConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class WebAuthnSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
WebAuthnSecurityFilterChainConfigurer webAuthnConfigurer = new WebAuthnSecurityFilterChainConfigurer();
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/register", "/authenticate").permitAll()
.anyRequest().authenticated())
.apply(webAuthnConfigurer);
return http.build();
}
}
2.4 Entity File
The CredentialEntity class represents a persisted user credential. It stores essential fields such as the credential ID, user ID, public key, and signature count, and maps to a database table using JPA annotations.
package com.example.passkey.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class CredentialEntity {
@Id
private String credentialId;
private String userId;
private byte[] publicKey;
private long signatureCount;
}
2.5 Repository File
The CredentialRepository interface provides data access methods for managing CredentialEntity objects using Spring Data JPA. It includes a custom finder method to retrieve credentials by user ID.
package com.example.passkey.repository;
import com.example.passkey.entity.CredentialEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CredentialRepository extends JpaRepository<CredentialEntity, String> {
Optional<CredentialEntity> findByUserId(String userId);
}
2.6 Register Controller
This controller handles user registration using passkeys. It receives the client’s credential data, validates it with the WebAuthn registration manager, and stores the generated public key and related metadata in the database.
package com.example.passkey.controller;
import com.example.passkey.entity.CredentialEntity;
import com.example.passkey.repository.CredentialRepository;
import com.webauthn4j.data.RegistrationRequest;
import com.webauthn4j.data.RegistrationData;
import com.webauthn4j.data.RegistrationParameters;
import com.webauthn4j.springframework.security.WebAuthnRegistrationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Base64;
@Data
class RegistrationRequestDTO {
private String userId;
private CredentialDTO credential;
}
@RestController
@RequestMapping("/register")
public class RegisterController {
@Autowired
private WebAuthnRegistrationManager registrationManager;
@Autowired
private CredentialRepository credentialRepository;
@PostMapping
public ResponseEntity<?> register(@RequestBody RegistrationRequestDTO request) {
try {
RegistrationRequest registrationRequest = new RegistrationRequest(
request.getCredential().getClientDataJSON(),
request.getCredential().getAttestationObject()
);
RegistrationData registrationData = registrationManager.parse(registrationRequest);
RegistrationParameters registrationParameters = new RegistrationParameters(
registrationData.getAttestationObject().getAuthenticatorData().getRpIdHash(),
false
);
registrationManager.validate(registrationData, registrationParameters);
String credentialId = Base64.getEncoder().encodeToString(registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getCredentialId());
byte[] publicKeyCose = registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getCredentialPublicKey().getBytes();
CredentialEntity credential = new CredentialEntity();
credential.setCredentialId(credentialId);
credential.setUserId(request.getUserId());
credential.setPublicKey(publicKeyCose);
credential.setSignatureCount(registrationData.getAttestationObject().getAuthenticatorData().getSignCount());
credentialRepository.save(credential);
return ResponseEntity.ok("Registration successful");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Registration failed: " + e.getMessage());
}
}
}
2.6.1 Code Explanation
This code implements passkey-based user registration using Spring Boot and the WebAuthn4J library. The RegistrationRequestDTO class encapsulates the user ID and credential information required for registration. The RegisterController exposes a POST endpoint at /register that receives a WebAuthn registration payload from the client. It creates a RegistrationRequest object using the clientDataJSON and attestationObject received, and parses it into RegistrationData using the WebAuthnRegistrationManager. The controller then validates the registration data against expected parameters, such as relying party ID hash. Upon successful validation, it extracts the credential ID and public key, encodes them appropriately, and stores them in a CredentialEntity object along with the user ID and signature count. This credential is then saved in the database via the CredentialRepository. If any exception occurs during parsing or validation, it returns a 400 Bad Request with an appropriate error message; otherwise, it confirms successful registration.
2.7 Authentication Controller
The authentication controller verifies login attempts by validating the credentials against stored data. It uses the WebAuthn authentication manager to parse and validate the incoming credential, updating the signature count upon successful authentication.
package com.example.passkey.controller;
import com.example.passkey.entity.CredentialEntity;
import com.example.passkey.repository.CredentialRepository;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.AuthenticationRequest;
import com.webauthn4j.springframework.security.WebAuthnAuthenticationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Data
class CredentialDTO {
private String id;
private byte[] clientDataJSON;
private byte[] attestationObject;
private byte[] authenticatorData;
private byte[] signature;
}
@Data
class AuthenticationRequestDTO {
private CredentialDTO credential;
}
@RestController
@RequestMapping("/authenticate")
public class AuthController {
@Autowired
private WebAuthnAuthenticationManager authenticationManager;
@Autowired
private CredentialRepository credentialRepository;
@PostMapping
public ResponseEntity<?> authenticate(@RequestBody AuthenticationRequestDTO request) {
try {
String credentialId = request.getCredential().getId();
CredentialEntity storedCredential = credentialRepository.findById(credentialId)
.orElseThrow(() -> new RuntimeException("Credential not found"));
AuthenticationRequest authenticationRequest = new AuthenticationRequest(
credentialId,
request.getCredential().getClientDataJSON(),
request.getCredential().getAuthenticatorData(),
request.getCredential().getSignature()
);
AuthenticationParameters authenticationParameters = new AuthenticationParameters(
storedCredential.getPublicKey(),
storedCredential.getSignatureCount()
);
AuthenticationData authenticationData = authenticationManager.parse(authenticationRequest);
authenticationManager.validate(authenticationData, authenticationParameters);
// Update signature counter
storedCredential.setSignatureCount(authenticationData.getAuthenticatorData().getSignCount());
credentialRepository.save(storedCredential);
return ResponseEntity.ok("Authentication successful");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication failed: " + e.getMessage());
}
}
}
2.7.1 Code Explanation
This code defines a passkey-based authentication workflow using Spring Boot and WebAuthn4J. The CredentialDTO class acts as a data carrier for WebAuthn credential fields received from the client, including the credential ID, client data, attestation object, authenticator data, and signature—all in byte array form. The AuthenticationRequestDTO wraps this credential for incoming requests. The AuthController exposes a POST endpoint /authenticate that receives these credentials, locates the corresponding stored credential in the database via CredentialRepository, and constructs an AuthenticationRequest to parse and validate the incoming WebAuthn data using the WebAuthnAuthenticationManager. If validation succeeds, it updates the signature counter in the database to prevent replay attacks and responds with a success message. If validation fails due to incorrect or tampered data, it returns a 401 Unauthorized error with the failure reason.
2.8 Configuration File
This is the application’s configuration file using Spring Boot’s application.yml style in plain format. It sets up an in-memory H2 database for development, enables SQL logging, and exposes the H2 console for easy inspection.
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
2.9 Code Output
The AuthController is responsible for handling the authentication process using passkeys (WebAuthn). When a client application sends a POST request to the /authenticate endpoint with credential data such as the credential ID, client data JSON, authenticator data, and a signature, the controller begins by extracting the credential ID. It then attempts to retrieve the corresponding CredentialEntity from the database using the CredentialRepository. If the credential is not found, the controller responds with an HTTP 401 Unauthorized status and a descriptive error message.
If the credential exists, the controller constructs an AuthenticationRequest object from the request payload. It also creates an AuthenticationParameters object using the stored public key and signature counter, which are essential for verifying the signature and ensuring the request isn’t a replay attack. The controller then uses the WebAuthnAuthenticationManager to parse and validate the authentication request.
If the authentication data is valid, it means the cryptographic signature is correct and the authenticator is trustworthy. The controller proceeds to update the stored signature counter to reflect the most recent authentication event and persists the updated CredentialEntity back to the database. Upon success, it returns an HTTP 200 OK response with a message indicating successful authentication.
In case of any failure—such as an invalid signature, tampered client data, a mismatch in the signature counter, or a missing credential—the controller catches the exception and returns an HTTP 401 Unauthorized response, along with an error message explaining the failure. This implementation ensures a secure, passwordless login mechanism by leveraging the WebAuthn standard and public-key cryptography.
3. Conclusion
Passkeys represent a future-ready approach to secure authentication. Integrating them into Spring Security with WebAuthn4J enables passwordless flows that enhance both security and user experience. With WebAuthn support expanding across ecosystems, adopting passkeys now can position your application ahead of the curve. For production systems, consider secure storage, multi-device sync, and user experience enhancements. The WebAuthn4J library is flexible, enabling custom implementations of challenge storage, credential resolution, and validation.




