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.