google-site-verification: google61fe8ba583a51912.html
TiPasswordless Future: Implementing Passkeys with WebAuthn in Java Applications

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

  1. Security
  • Use HTTPS in production
  • Validate origins properly
  • Implement proper CORS configuration
  • Store credentials securely
  1. User Experience
  • Provide fallback authentication methods
  • Support multiple passkeys per user
  • Implement smooth error handling
  • Offer passkey management UI
  1. Performance
  • Cache public keys
  • Use efficient database queries
  • Implement session cleanup
  1. 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:

  1. Set up WebAuthn dependencies
  2. Create data models for users and credentials
  3. Implement registration and authentication flows
  4. Create REST API endpoints
  5. Integrate with frontend using WebAuthn API
  6. Add security configurations

This implementation provides a robust foundation for passwordless authentication in Java applications, ready for production use with proper security hardening.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper