Passkeys represent the next generation of authentication, replacing passwords with secure, phishing-resistant cryptographic credentials. This guide covers complete WebAuthn implementation in Java using both server-side and client-side components.
Architecture Overview
Java Application → WebAuthn Server → FIDO2 Authenticator ↑ (Browser API / Mobile Device)
Step 1: Dependencies Setup
Maven Dependencies
<!-- pom.xml --> <dependencies> <!-- WebAuthn Server Implementation --> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j-spring-security</artifactId> <version>0.21.0.RELEASE</version> </dependency> <dependency> <groupId>com.webauthn4j</groupId> <artifactId>webauthn4j</artifactId> <version>0.21.0.RELEASE</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>
Step 2: Data Models
JPA Entities
// src/main/java/com/company/passkeys/entity/User.java
package com.company.passkeys.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
private String email;
private String displayName;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PasskeyCredential> passkeys = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime lastLoginAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
// src/main/java/com/company/passkeys/entity/PasskeyCredential.java
@Entity
@Table(name = "passkey_credentials")
@Data
public class PasskeyCredential {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String credentialId; // Base64 encoded
@Column(nullable = false, length = 2000)
private String publicKey; // JSON or COSE encoded
@Column(nullable = false)
private String attestationType;
private Integer signatureCount = 0;
@Column(nullable = false)
private String relyingPartyId;
@Column(nullable = false)
private String userHandle; // Base64 encoded
private String transports; // "usb,nfc,ble,internal"
private LocalDateTime createdAt;
private LocalDateTime lastUsedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
// src/main/java/com/company/passkeys/entity/PasskeySession.java
@Entity
@Table(name = "passkey_sessions")
@Data
public class PasskeySession {
@Id
private String sessionId;
@Column(nullable = false, length = 4000)
private String challenge; // Base64 encoded
@Column(nullable = false)
private LocalDateTime expiresAt;
private String username; // For registration
private String userHandle; // For authentication
@Enumerated(EnumType.STRING)
private SessionType type;
public enum SessionType {
REGISTRATION, AUTHENTICATION
}
public boolean isValid() {
return LocalDateTime.now().isBefore(expiresAt);
}
}
Step 3: WebAuthn Service Core
WebAuthn Configuration
// src/main/java/com/company/passkeys/config/WebAuthnConfig.java
package com.company.passkeys.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.anchor.TrustAnchorRepository;
import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.AttestationConveyancePreference;
import com.webauthn4j.data.RegistrationExtensionInputs;
import com.webauthn4j.data.AuthenticationExtensionInputs;
import com.webauthn4j.data.PublicKeyCredentialParameters;
import com.webauthn4j.data.PublicKeyCredentialType;
import com.webauthn4j.data.client.Origin;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.metadata.legacy.LegacyMetadataBLOBProvider;
import com.webauthn4j.metadata.legacy.TrustAnchorRepositoryImpl;
import com.webauthn4j.validator.CustomRegistrationValidator;
import com.webauthn4j.validator.CustomAuthenticationValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.List;
@Configuration
public class WebAuthnConfig {
@Value("${webauthn.relying-party.id:localhost}")
private String relyingPartyId;
@Value("${webauthn.relying-party.name:My Java Application}")
private String relyingPartyName;
@Value("${webauthn.relying-party.origin:https://localhost:8443}")
private String relyingPartyOrigin;
@Bean
public WebAuthnManager webAuthnManager() {
ObjectConverter objectConverter = new ObjectConverter();
TrustAnchorRepository trustAnchorRepository = new TrustAnchorRepositoryImpl();
return WebAuthnManager.createNonStrictWebAuthnManager(
trustAnchorRepository,
new LegacyMetadataBLOBProvider(),
objectConverter
);
}
@Bean
public ObjectConverter objectConverter() {
return new ObjectConverter();
}
@Bean
public List<PublicKeyCredentialParameters> publicKeyCredentialParameters() {
return List.of(
new PublicKeyCredentialParameters(
PublicKeyCredentialType.PUBLIC_KEY,
-7 // ES256
),
new PublicKeyCredentialParameters(
PublicKeyCredentialType.PUBLIC_KEY,
-257 // RS256
)
);
}
@Bean
public Challenge generateChallenge() {
byte[] challenge = new byte[32];
new SecureRandom().nextBytes(challenge);
return new DefaultChallenge(challenge);
}
@Bean
public Origin origin() {
return new Origin(relyingPartyOrigin);
}
public String getRelyingPartyId() {
return relyingPartyId;
}
public String getRelyingPartyName() {
return relyingPartyName;
}
public AttestationConveyancePreference getAttestationPreference() {
return AttestationConveyancePreference.NONE;
}
public RegistrationExtensionInputs getRegistrationExtensions() {
return new RegistrationExtensionInputs();
}
public AuthenticationExtensionInputs getAuthenticationExtensions() {
return new AuthenticationExtensionInputs();
}
}
Core WebAuthn Service
// src/main/java/com/company/passkeys/service/WebAuthnService.java
package com.company.passkeys.service;
import com.company.passkeys.config.WebAuthnConfig;
import com.company.passkeys.entity.PasskeyCredential;
import com.company.passkeys.entity.PasskeySession;
import com.company.passkeys.entity.User;
import com.company.passkeys.repository.PasskeyCredentialRepository;
import com.company.passkeys.repository.PasskeySessionRepository;
import com.company.passkeys.repository.UserRepository;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.data.*;
import com.webauthn4j.data.client.Origin;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.attestation.AttestationObject;
import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.validator.ValidationResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class WebAuthnService {
private final WebAuthnManager webAuthnManager;
private final WebAuthnConfig webAuthnConfig;
private final UserRepository userRepository;
private final PasskeyCredentialRepository credentialRepository;
private final PasskeySessionRepository sessionRepository;
private final ChallengeService challengeService;
// Registration Methods
public Map<String, Object> startRegistration(String username, String displayName, String email) {
User user = userRepository.findByUsername(username)
.orElseGet(() -> createNewUser(username, displayName, email));
Challenge challenge = challengeService.generateChallenge();
String sessionId = UUID.randomUUID().toString();
// Store registration session
PasskeySession session = new PasskeySession();
session.setSessionId(sessionId);
session.setChallenge(Base64.getEncoder().encodeToString(challenge.getValue()));
session.setExpiresAt(LocalDateTime.now().plusMinutes(5));
session.setUsername(username);
session.setType(PasskeySession.SessionType.REGISTRATION);
sessionRepository.save(session);
// Prepare public key creation options
return buildRegistrationOptions(user, challenge);
}
private Map<String, Object> buildRegistrationOptions(User user, Challenge challenge) {
Map<String, Object> options = new HashMap<>();
// PublicKeyCredentialCreationOptions
options.put("challenge", Base64.getUrlEncoder().withoutPadding().encodeToString(challenge.getValue()));
// Relying Party Info
Map<String, Object> rp = new HashMap<>();
rp.put("id", webAuthnConfig.getRelyingPartyId());
rp.put("name", webAuthnConfig.getRelyingPartyName());
options.put("rp", rp);
// User Info
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", Base64.getEncoder().encodeToString(user.getId().toString().getBytes()));
userInfo.put("name", user.getUsername());
userInfo.put("displayName", user.getDisplayName());
options.put("user", userInfo);
// Public Key Parameters
List<Map<String, Object>> pubKeyCredParams = Arrays.asList(
Map.of("type", "public-key", "alg", -7), // ES256
Map.of("type", "public-key", "alg", -257) // RS256
);
options.put("pubKeyCredParams", pubKeyCredParams);
// Timeout
options.put("timeout", 300000);
// Exclude existing credentials
List<String> excludeCredentials = user.getPasskeys().stream()
.map(PasskeyCredential::getCredentialId)
.collect(Collectors.toList());
options.put("excludeCredentials", excludeCredentials);
// Authenticator Selection
Map<String, Object> authenticatorSelection = new HashMap<>();
authenticatorSelection.put("authenticatorAttachment", "platform");
authenticatorSelection.put("residentKey", "required");
authenticatorSelection.put("requireResidentKey", true);
authenticatorSelection.put("userVerification", "required");
options.put("authenticatorSelection", authenticatorSelection);
// Attestation
options.put("attestation", webAuthnConfig.getAttestationPreference().getValue());
return options;
}
public ValidationResult finishRegistration(String sessionId, String clientDataJSON,
String attestationObject, String credentialId) {
PasskeySession session = sessionRepository.findById(sessionId)
.orElseThrow(() -> new RuntimeException("Invalid session"));
if (!session.isValid()) {
throw new RuntimeException("Session expired");
}
try {
// Parse and validate registration data
byte[] clientDataJSONBytes = Base64.getDecoder().decode(clientDataJSON);
byte[] attestationObjectBytes = Base64.getDecoder().decode(attestationObject);
byte[] credentialIdBytes = Base64.getDecoder().decode(credentialId);
// Create server property
ServerProperty serverProperty = new ServerProperty(
webAuthnConfig.origin(),
webAuthnConfig.getRelyingPartyId(),
new DefaultChallenge(Base64.getDecoder().decode(session.getChallenge())),
null
);
// Validate registration
ValidationResult<RegistrationData> result = webAuthnManager.validateRegistrationData(
attestationObjectBytes,
clientDataJSONBytes,
serverProperty,
false
);
if (result.isValid()) {
savePasskeyCredential(session.getUsername(), result.getRegistrationData(), credentialIdBytes);
}
return result;
} catch (Exception e) {
log.error("Registration validation failed", e);
throw new RuntimeException("Registration failed", e);
} finally {
sessionRepository.delete(session);
}
}
private void savePasskeyCredential(String username, RegistrationData registrationData, byte[] credentialId) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
AttestationObject attestationObject = registrationData.getAttestationObject();
AuthenticatorData authenticatorData = attestationObject.getAuthenticatorData();
PasskeyCredential credential = new PasskeyCredential();
credential.setUser(user);
credential.setCredentialId(Base64.getEncoder().encodeToString(credentialId));
credential.setPublicKey(Base64.getEncoder().encodeToString(
authenticatorData.getAttestedCredentialData().getCOSEKey().getBytes()
));
credential.setAttestationType(attestationObject.getFormat());
credential.setSignatureCount(authenticatorData.getSignCount());
credential.setRelyingPartyId(webAuthnConfig.getRelyingPartyId());
credential.setUserHandle(Base64.getEncoder().encodeToString(
authenticatorData.getAttestedCredentialData().getUserHandle()
));
credential.setTransports("internal"); // Default
credentialRepository.save(credential);
log.info("Passkey registered for user: {}", username);
}
// Authentication Methods
public Map<String, Object> startAuthentication(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
Challenge challenge = challengeService.generateChallenge();
String sessionId = UUID.randomUUID().toString();
// Store authentication session
PasskeySession session = new PasskeySession();
session.setSessionId(sessionId);
session.setChallenge(Base64.getEncoder().encodeToString(challenge.getValue()));
session.setExpiresAt(LocalDateTime.now().plusMinutes(5));
session.setUserHandle(Base64.getEncoder().encodeToString(user.getId().toString().getBytes()));
session.setType(PasskeySession.SessionType.AUTHENTICATION);
sessionRepository.save(session);
return buildAuthenticationOptions(user, challenge);
}
private Map<String, Object> buildAuthenticationOptions(User user, Challenge challenge) {
Map<String, Object> options = new HashMap<>();
// PublicKeyCredentialRequestOptions
options.put("challenge", Base64.getUrlEncoder().withoutPadding().encodeToString(challenge.getValue()));
// Allow credentials (existing passkeys)
List<Map<String, Object>> allowCredentials = user.getPasskeys().stream()
.map(cred -> Map.of(
"id", cred.getCredentialId(),
"type", "public-key",
"transports", Arrays.asList(cred.getTransports().split(","))
))
.collect(Collectors.toList());
options.put("allowCredentials", allowCredentials);
// Relying Party ID
options.put("rpId", webAuthnConfig.getRelyingPartyId());
// Timeout
options.put("timeout", 300000);
// User verification
options.put("userVerification", "required");
return options;
}
public ValidationResult finishAuthentication(String sessionId, String credentialId,
String clientDataJSON, String authenticatorData,
String signature, String userHandle) {
PasskeySession session = sessionRepository.findById(sessionId)
.orElseThrow(() -> new RuntimeException("Invalid session"));
if (!session.isValid()) {
throw new RuntimeException("Session expired");
}
try {
// Parse data
byte[] credentialIdBytes = Base64.getDecoder().decode(credentialId);
byte[] clientDataJSONBytes = Base64.getDecoder().decode(clientDataJSON);
byte[] authenticatorDataBytes = Base64.getDecoder().decode(authenticatorData);
byte[] signatureBytes = Base64.getDecoder().decode(signature);
byte[] userHandleBytes = Base64.getDecoder().decode(userHandle);
// Find the credential
PasskeyCredential credential = credentialRepository.findByCredentialId(
Base64.getEncoder().encodeToString(credentialIdBytes))
.orElseThrow(() -> new RuntimeException("Credential not found"));
// Create server property
ServerProperty serverProperty = new ServerProperty(
webAuthnConfig.origin(),
webAuthnConfig.getRelyingPartyId(),
new DefaultChallenge(Base64.getDecoder().decode(session.getChallenge())),
null
);
// Byte array of the stored public key
byte[] publicKey = Base64.getDecoder().decode(credential.getPublicKey());
// Validate authentication
ValidationResult<AuthenticationData> result = webAuthnManager.validateAuthenticationData(
authenticatorDataBytes,
clientDataJSONBytes,
signatureBytes,
publicKey,
serverProperty,
false,
userHandleBytes
);
if (result.isValid()) {
// Update signature count and last used
credential.setSignatureCount(credential.getSignatureCount() + 1);
credential.setLastUsedAt(LocalDateTime.now());
credentialRepository.save(credential);
// Update user last login
User user = credential.getUser();
user.setLastLoginAt(LocalDateTime.now());
userRepository.save(user);
}
return result;
} catch (Exception e) {
log.error("Authentication validation failed", e);
throw new RuntimeException("Authentication failed", e);
} finally {
sessionRepository.delete(session);
}
}
private User createNewUser(String username, String displayName, String email) {
User user = new User();
user.setUsername(username);
user.setDisplayName(displayName);
user.setEmail(email);
return userRepository.save(user);
}
public List<PasskeyCredential> getUserPasskeys(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
return user.getPasskeys();
}
public void deletePasskey(Long credentialId, String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
PasskeyCredential credential = credentialRepository.findById(credentialId)
.orElseThrow(() -> new RuntimeException("Credential not found"));
if (!credential.getUser().getId().equals(user.getId())) {
throw new RuntimeException("Credential does not belong to user");
}
credentialRepository.delete(credential);
log.info("Passkey deleted for user: {}", username);
}
}
Step 4: Supporting Services
// src/main/java/com/company/passkeys/service/ChallengeService.java
package com.company.passkeys.service;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
@Service
public class ChallengeService {
private final SecureRandom secureRandom = new SecureRandom();
public Challenge generateChallenge() {
byte[] challenge = new byte[32];
secureRandom.nextBytes(challenge);
return new DefaultChallenge(challenge);
}
public String generateChallengeBase64() {
byte[] challenge = new byte[32];
secureRandom.nextBytes(challenge);
return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(challenge);
}
}
// src/main/java/com/company/passkeys/service/UserService.java
@Service
@Slf4j
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public User createUser(String username, String displayName, String email) {
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("User already exists");
}
User user = new User();
user.setUsername(username);
user.setDisplayName(displayName);
user.setEmail(email);
return userRepository.save(user);
}
public void updateLastLogin(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
user.setLastLoginAt(LocalDateTime.now());
userRepository.save(user);
}
}
Step 5: REST API Controllers
// src/main/java/com/company/passkeys/controller/PasskeyController.java
package com.company.passkeys.controller;
import com.company.passkeys.entity.PasskeyCredential;
import com.company.passkeys.entity.User;
import com.company.passkeys.service.WebAuthnService;
import com.webauthn4j.validator.ValidationResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/passkeys")
@RequiredArgsConstructor
@Slf4j
public class PasskeyController {
private final WebAuthnService webAuthnService;
@PostMapping("/registration/start")
public ResponseEntity<Map<String, Object>> startRegistration(
@RequestBody RegistrationStartRequest request) {
try {
Map<String, Object> options = webAuthnService.startRegistration(
request.getUsername(),
request.getDisplayName(),
request.getEmail()
);
return ResponseEntity.ok(options);
} catch (Exception e) {
log.error("Registration start failed", e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/registration/finish")
public ResponseEntity<Map<String, Object>> finishRegistration(
@RequestBody RegistrationFinishRequest request) {
try {
ValidationResult result = webAuthnService.finishRegistration(
request.getSessionId(),
request.getClientDataJSON(),
request.getAttestationObject(),
request.getCredentialId()
);
if (result.isValid()) {
return ResponseEntity.ok(Map.of("status", "success", "message", "Passkey registered successfully"));
} else {
return ResponseEntity.badRequest().body(Map.of(
"status", "error",
"message", "Registration validation failed",
"errors", result.getErrors()
));
}
} catch (Exception e) {
log.error("Registration finish failed", e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/authentication/start")
public ResponseEntity<Map<String, Object>> startAuthentication(
@RequestBody AuthenticationStartRequest request) {
try {
Map<String, Object> options = webAuthnService.startAuthentication(request.getUsername());
return ResponseEntity.ok(options);
} catch (Exception e) {
log.error("Authentication start failed", e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/authentication/finish")
public ResponseEntity<Map<String, Object>> finishAuthentication(
@RequestBody AuthenticationFinishRequest request) {
try {
ValidationResult result = webAuthnService.finishAuthentication(
request.getSessionId(),
request.getCredentialId(),
request.getClientDataJSON(),
request.getAuthenticatorData(),
request.getSignature(),
request.getUserHandle()
);
if (result.isValid()) {
return ResponseEntity.ok(Map.of(
"status", "success",
"message", "Authentication successful",
"username", extractUsernameFromUserHandle(request.getUserHandle())
));
} else {
return ResponseEntity.badRequest().body(Map.of(
"status", "error",
"message", "Authentication validation failed",
"errors", result.getErrors()
));
}
} catch (Exception e) {
log.error("Authentication finish failed", e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/user/{username}/passkeys")
public ResponseEntity<List<PasskeyCredential>> getUserPasskeys(@PathVariable String username) {
try {
List<PasskeyCredential> passkeys = webAuthnService.getUserPasskeys(username);
return ResponseEntity.ok(passkeys);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/user/{username}/passkeys/{credentialId}")
public ResponseEntity<Map<String, Object>> deletePasskey(
@PathVariable String username, @PathVariable Long credentialId) {
try {
webAuthnService.deletePasskey(credentialId, username);
return ResponseEntity.ok(Map.of("status", "success", "message", "Passkey deleted"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
private String extractUsernameFromUserHandle(String userHandleBase64) {
// Implementation to extract username from user handle
// This depends on how you encode user information in the user handle
return "user"; // Placeholder
}
// Request DTOs
@Data
public static class RegistrationStartRequest {
private String username;
private String displayName;
private String email;
}
@Data
public static class RegistrationFinishRequest {
private String sessionId;
private String clientDataJSON;
private String attestationObject;
private String credentialId;
}
@Data
public static class AuthenticationStartRequest {
private String username;
}
@Data
public static class AuthenticationFinishRequest {
private String sessionId;
private String credentialId;
private String clientDataJSON;
private String authenticatorData;
private String signature;
private String userHandle;
}
}
Step 6: Security Configuration
// src/main/java/com/company/passkeys/config/SecurityConfig.java
package com.company.passkeys.config;
import lombok.RequiredArgsConstructor;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/passkeys/**", "/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("https://localhost:*", "https://*.company.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Step 7: Frontend Integration Example
HTML/JavaScript Example
<!-- src/main/resources/static/passkey-demo.html -->
<!DOCTYPE html>
<html>
<head>
<title>Passkey Demo</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div>
<h2>Passkey Registration</h2>
<input type="text" id="username" placeholder="Username">
<input type="text" id="displayName" placeholder="Display Name">
<input type="email" id="email" placeholder="Email">
<button onclick="startRegistration()">Register Passkey</button>
</div>
<div>
<h2>Passkey Authentication</h2>
<input type="text" id="authUsername" placeholder="Username">
<button onclick="startAuthentication()">Authenticate with Passkey</button>
</div>
<script>
const baseUrl = 'http://localhost:8080/api/passkeys';
async function startRegistration() {
const username = document.getElementById('username').value;
const displayName = document.getElementById('displayName').value;
const email = document.getElementById('email').value;
try {
const response = await axios.post(`${baseUrl}/registration/start`, {
username,
displayName,
email
});
const options = response.data;
options.challenge = base64urlToBuffer(options.challenge);
options.user.id = base64urlToBuffer(options.user.id);
// Convert excludeCredentials
if (options.excludeCredentials) {
options.excludeCredentials = options.excludeCredentials.map(cred => ({
...cred,
id: base64urlToBuffer(cred.id)
}));
}
// Call WebAuthn API
const credential = await navigator.credentials.create({
publicKey: options
});
await finishRegistration(credential, options.sessionId);
} catch (error) {
console.error('Registration failed:', error);
alert('Registration failed: ' + error.message);
}
}
async function finishRegistration(credential, sessionId) {
const attestationObject = new Uint8Array(credential.response.attestationObject);
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
const credentialId = new Uint8Array(credential.rawId);
const response = await axios.post(`${baseUrl}/registration/finish`, {
sessionId: sessionId,
clientDataJSON: bufferToBase64url(clientDataJSON),
attestationObject: bufferToBase64url(attestationObject),
credentialId: bufferToBase64url(credentialId)
});
if (response.data.status === 'success') {
alert('Passkey registered successfully!');
} else {
alert('Registration failed: ' + response.data.message);
}
}
async function startAuthentication() {
const username = document.getElementById('authUsername').value;
try {
const response = await axios.post(`${baseUrl}/authentication/start`, {
username
});
const options = response.data;
options.challenge = base64urlToBuffer(options.challenge);
// Convert allowCredentials
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(cred => ({
...cred,
id: base64urlToBuffer(cred.id)
}));
}
// Call WebAuthn API
const credential = await navigator.credentials.get({
publicKey: options
});
await finishAuthentication(credential, options.sessionId);
} catch (error) {
console.error('Authentication failed:', error);
alert('Authentication failed: ' + error.message);
}
}
async function finishAuthentication(credential, sessionId) {
const authenticatorData = new Uint8Array(credential.response.authenticatorData);
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
const signature = new Uint8Array(credential.response.signature);
const userHandle = new Uint8Array(credential.response.userHandle);
const credentialId = new Uint8Array(credential.rawId);
const response = await axios.post(`${baseUrl}/authentication/finish`, {
sessionId: sessionId,
credentialId: bufferToBase64url(credentialId),
clientDataJSON: bufferToBase64url(clientDataJSON),
authenticatorData: bufferToBase64url(authenticatorData),
signature: bufferToBase64url(signature),
userHandle: bufferToBase64url(userHandle)
});
if (response.data.status === 'success') {
alert('Authentication successful! Welcome ' + response.data.username);
} else {
alert('Authentication failed: ' + response.data.message);
}
}
// Utility functions
function base64urlToBuffer(base64url) {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
const padded = base64.padEnd(base64.length + (pad ? 4 - pad : 0), '=');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function bufferToBase64url(buffer) {
const binary = String.fromCharCode(...buffer);
const base64 = btoa(binary);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
</script>
</body>
</html>
Step 8: Application Configuration
# application.yml spring: datasource: url: jdbc:h2:mem:passkeydb driverClassName: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true h2: console: enabled: true webauthn: relying-party: id: localhost name: "Java Passkey Demo" origin: "https://localhost:8443" server: port: 8080 ssl: enabled: false logging: level: com.company.passkeys: DEBUG
Best Practices
- Security
- Use HTTPS in production
- Validate origins properly
- Implement proper CORS configuration
- Store credentials securely
- User Experience
- Provide fallback authentication methods
- Support multiple passkeys per user
- Implement smooth error handling
- Offer passkey management UI
- Performance
- Cache public keys
- Use efficient database queries
- Implement session cleanup
- Compliance
- Follow FIDO2/WebAuthn specifications
- Implement proper audit logging
- Support privacy requirements
Conclusion
Passkey implementation provides:
- Enhanced Security: Phishing-resistant authentication
- Better UX: Passwordless, seamless authentication
- Cross-Platform: Works across devices and platforms
- Standards-Based: Built on FIDO2/WebAuthn standards
Implementation steps:
- Set up WebAuthn dependencies
- Create data models for users and credentials
- Implement registration and authentication flows
- Create REST API endpoints
- Integrate with frontend using WebAuthn API
- Add security configurations
This implementation provides a robust foundation for passwordless authentication in Java applications, ready for production use with proper security hardening.