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