3570 words
18 minutes
Apple Passkeys Complete Implementation Guide: Building Passwordless Authentication for Enterprise Applications

Apple Passkeys Complete Implementation Guide: Building Passwordless Authentication for Enterprise Applications#

Table of Contents#

Introduction: The Password Problem is Finally Solved#

After decades of failed attempts to eliminate passwords, Apple’s implementation of Passkeys—built on WebAuthn and FIDO2 standards—finally delivers a practical, secure, and user-friendly solution. With $22.14 billion projected market growth by 2025, passwordless authentication is no longer the future—it’s the present. This comprehensive guide provides everything you need to implement Passkeys in your applications today.

Understanding the Technology Stack#

The Authentication Evolution#

graph LR
A[Passwords] -->|2000s| B[2FA/MFA]
B -->|2010s| C[Biometrics]
C -->|2020s| D[FIDO2/WebAuthn]
D -->|2023+| E[Passkeys]
style E fill:#4CAF50,stroke:#333,stroke-width:4px

Passkeys Architecture#

Passkeys combine three powerful technologies:

  1. Public Key Cryptography: Eliminates shared secrets
  2. Biometric Authentication: Provides convenient user verification
  3. Cloud Synchronization: Enables cross-device availability

iOS Implementation: Native Passkeys Integration#

Setting Up the Authentication Services#

import AuthenticationServices
import CryptoKit
import Security
// MARK: - Passkey Manager for iOS Applications
@available(iOS 16.0, *)
class PasskeyAuthenticationManager: NSObject {
// MARK: - Properties
private let domain: String
private var authenticationAnchor: ASPresentationAnchor?
private var currentChallenge: String?
// MARK: - Initialization
init(domain: String) {
self.domain = domain
super.init()
}
// MARK: - Registration Flow
func registerNewPasskey(
username: String,
displayName: String,
completion: @escaping (Result<ASAuthorizationPlatformPublicKeyCredentialRegistration, Error>) -> Void
) {
// Generate cryptographic challenge
let challenge = generateChallenge()
self.currentChallenge = challenge
// Create public key credential provider
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: domain
)
// Configure registration request
let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(
challenge: challenge.data(using: .utf8)!,
name: username,
userID: generateUserID(for: username).data(using: .utf8)!
)
// Configure attestation preference
registrationRequest.attestationPreference = .direct
// Set credential parameters for ES256 (ECDSA with P-256 and SHA-256)
let credentialParameters = ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest.CredentialParameters(
algorithm: ASCOSEAlgorithmIdentifier.ES256
)
registrationRequest.credentialParameters = [credentialParameters]
// Configure user verification
registrationRequest.userVerificationPreference = .required
// Add display name for better UX
registrationRequest.displayName = displayName
// Exclude existing credentials to prevent duplicates
if let existingCredentials = loadExistingCredentials(for: username) {
registrationRequest.excludedCredentials = existingCredentials
}
// Create authorization controller
let authController = ASAuthorizationController(authorizationRequests: [registrationRequest])
authController.delegate = self
authController.presentationContextProvider = self
// Store completion handler
self.registrationCompletion = completion
// Perform registration
authController.performRequests()
}
// MARK: - Authentication Flow
func authenticateWithPasskey(
completion: @escaping (Result<ASAuthorizationPlatformPublicKeyCredentialAssertion, Error>) -> Void
) {
// Generate fresh challenge for authentication
let challenge = generateChallenge()
self.currentChallenge = challenge
// Create assertion request
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: domain
)
let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(
challenge: challenge.data(using: .utf8)!
)
// Configure request options
assertionRequest.userVerificationPreference = .required
// Allow credentials from iCloud Keychain
assertionRequest.allowedCredentials = [] // Empty array allows any valid credential
// Create controller
let authController = ASAuthorizationController(authorizationRequests: [assertionRequest])
authController.delegate = self
authController.presentationContextProvider = self
// Store completion handler
self.authenticationCompletion = completion
// Perform authentication
authController.performRequests()
}
// MARK: - Helper Methods
private func generateChallenge() -> String {
// Generate cryptographically secure random challenge
var bytes = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
return Data(bytes).base64EncodedString()
}
private func generateUserID(for username: String) -> String {
// Generate unique user ID using SHA256
let data = username.data(using: .utf8)!
let hashed = SHA256.hash(data: data)
return hashed.compactMap { String(format: "%02x", $0) }.joined()
}
private func loadExistingCredentials(for username: String) -> [ASAuthorizationPlatformPublicKeyCredentialDescriptor]? {
// Load existing credentials from Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "passkey.\(username)".data(using: .utf8)!,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let items = result as? [[String: Any]] else {
return nil
}
return items.compactMap { item in
guard let credentialID = item[kSecAttrApplicationLabel as String] as? Data else {
return nil
}
return ASAuthorizationPlatformPublicKeyCredentialDescriptor(
credentialID: credentialID
)
}
}
// MARK: - Keychain Storage
func storeCredential(
credentialID: Data,
publicKey: Data,
username: String
) throws {
// Prepare Keychain item
let keychainItem: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
kSecAttrApplicationTag as String: "passkey.\(username)".data(using: .utf8)!,
kSecAttrApplicationLabel as String: credentialID,
kSecValueData as String: publicKey,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAttrSynchronizable as String: true // Enable iCloud Keychain sync
]
// Add to Keychain
let status = SecItemAdd(keychainItem as CFDictionary, nil)
if status != errSecSuccess {
throw PasskeyError.keychainError(status)
}
}
// MARK: - Private Properties
private var registrationCompletion: ((Result<ASAuthorizationPlatformPublicKeyCredentialRegistration, Error>) -> Void)?
private var authenticationCompletion: ((Result<ASAuthorizationPlatformPublicKeyCredentialAssertion, Error>) -> Void)?
}
// MARK: - ASAuthorizationControllerDelegate
extension PasskeyAuthenticationManager: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let credential as ASAuthorizationPlatformPublicKeyCredentialRegistration:
// Handle successful registration
handleRegistrationSuccess(credential)
case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion:
// Handle successful authentication
handleAuthenticationSuccess(credential)
default:
let error = PasskeyError.unexpectedCredentialType
registrationCompletion?(.failure(error))
authenticationCompletion?(.failure(error))
}
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
// Handle authorization errors
if let authError = error as? ASAuthorizationError {
switch authError.code {
case .canceled:
// User canceled the operation
registrationCompletion?(.failure(PasskeyError.userCanceled))
authenticationCompletion?(.failure(PasskeyError.userCanceled))
case .failed:
// Authentication failed
registrationCompletion?(.failure(PasskeyError.authenticationFailed))
authenticationCompletion?(.failure(PasskeyError.authenticationFailed))
case .notHandled:
// Request not handled
registrationCompletion?(.failure(PasskeyError.notSupported))
authenticationCompletion?(.failure(PasskeyError.notSupported))
default:
registrationCompletion?(.failure(error))
authenticationCompletion?(.failure(error))
}
} else {
registrationCompletion?(.failure(error))
authenticationCompletion?(.failure(error))
}
// Clean up
registrationCompletion = nil
authenticationCompletion = nil
}
private func handleRegistrationSuccess(_ credential: ASAuthorizationPlatformPublicKeyCredentialRegistration) {
// Extract credential data
let credentialID = credential.credentialID
let rawID = credential.rawID
let clientDataJSON = credential.rawClientDataJSON
// Store in Keychain
do {
try storeCredential(
credentialID: credentialID,
publicKey: rawID,
username: credential.userID.base64EncodedString()
)
registrationCompletion?(.success(credential))
} catch {
registrationCompletion?(.failure(error))
}
registrationCompletion = nil
}
private func handleAuthenticationSuccess(_ credential: ASAuthorizationPlatformPublicKeyCredentialAssertion) {
// Verify signature
let signature = credential.signature
let authenticatorData = credential.rawAuthenticatorData
let clientDataJSON = credential.rawClientDataJSON
let userID = credential.userID
// Verify challenge matches
if let clientData = try? JSONDecoder().decode(
ClientDataJSON.self,
from: clientDataJSON
) {
guard clientData.challenge == self.currentChallenge else {
authenticationCompletion?(.failure(PasskeyError.challengeMismatch))
return
}
}
authenticationCompletion?(.success(credential))
authenticationCompletion = nil
}
}
// MARK: - ASAuthorizationControllerPresentationContextProviding
extension PasskeyAuthenticationManager: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return authenticationAnchor ?? ASPresentationAnchor()
}
}
// MARK: - Error Types
enum PasskeyError: LocalizedError {
case userCanceled
case authenticationFailed
case notSupported
case unexpectedCredentialType
case keychainError(OSStatus)
case challengeMismatch
var errorDescription: String? {
switch self {
case .userCanceled:
return "User canceled the passkey operation"
case .authenticationFailed:
return "Passkey authentication failed"
case .notSupported:
return "Passkeys are not supported on this device"
case .unexpectedCredentialType:
return "Received unexpected credential type"
case .keychainError(let status):
return "Keychain error: \(status)"
case .challengeMismatch:
return "Challenge verification failed"
}
}
}
// MARK: - Supporting Types
struct ClientDataJSON: Codable {
let type: String
let challenge: String
let origin: String
let crossOrigin: Bool?
}

Web Implementation: JavaScript WebAuthn Integration#

Complete WebAuthn Implementation#

/**
* Complete WebAuthn/Passkeys Implementation for Web Applications
* Supports registration, authentication, and credential management
*/
class PasskeyWebManager {
constructor(config = {}) {
this.rpName = config.rpName || window.location.hostname;
this.rpId = config.rpId || window.location.hostname;
this.apiEndpoint = config.apiEndpoint || '/api/auth';
this.timeout = config.timeout || 60000; // 60 seconds
// Check WebAuthn support
this.isSupported = this.checkWebAuthnSupport();
// Credential storage
this.credentials = new Map();
}
/**
* Check if WebAuthn is supported
*/
checkWebAuthnSupport() {
return !!(
window.PublicKeyCredential &&
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
window.PublicKeyCredential.isConditionalMediationAvailable
);
}
/**
* Register a new passkey
*/
async register(username, displayName) {
if (!this.isSupported) {
throw new Error('WebAuthn is not supported on this device');
}
try {
// Step 1: Get registration options from server
const registrationOptions = await this.getRegistrationOptions(username);
// Step 2: Create credential
const credential = await this.createCredential(
registrationOptions,
username,
displayName
);
// Step 3: Send credential to server for verification
const verificationResult = await this.verifyRegistration(
credential,
username
);
if (verificationResult.verified) {
// Store credential ID locally
this.credentials.set(username, credential.id);
// Enable autofill for future logins
await this.enableAutofill(credential.id);
return {
success: true,
credentialId: credential.id,
message: 'Passkey registered successfully'
};
} else {
throw new Error('Registration verification failed');
}
} catch (error) {
console.error('Registration error:', error);
throw this.handleError(error);
}
}
/**
* Authenticate with passkey
*/
async authenticate(username = null) {
if (!this.isSupported) {
throw new Error('WebAuthn is not supported on this device');
}
try {
// Step 1: Get authentication options from server
const authOptions = await this.getAuthenticationOptions(username);
// Step 2: Get credential assertion
const assertion = await this.getCredentialAssertion(authOptions);
// Step 3: Verify assertion with server
const verificationResult = await this.verifyAuthentication(
assertion,
username
);
if (verificationResult.verified) {
return {
success: true,
token: verificationResult.token,
user: verificationResult.user,
message: 'Authentication successful'
};
} else {
throw new Error('Authentication verification failed');
}
} catch (error) {
console.error('Authentication error:', error);
throw this.handleError(error);
}
}
/**
* Get registration options from server
*/
async getRegistrationOptions(username) {
const response = await fetch(`${this.apiEndpoint}/register/options`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username })
});
if (!response.ok) {
throw new Error('Failed to get registration options');
}
const options = await response.json();
// Convert base64 strings to ArrayBuffers
options.challenge = this.base64ToArrayBuffer(options.challenge);
options.user.id = this.base64ToArrayBuffer(options.user.id);
if (options.excludeCredentials) {
options.excludeCredentials = options.excludeCredentials.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
}));
}
return options;
}
/**
* Create a new credential
*/
async createCredential(options, username, displayName) {
const publicKeyCredentialCreationOptions = {
challenge: options.challenge,
rp: {
name: this.rpName,
id: this.rpId
},
user: {
id: options.user.id,
name: username,
displayName: displayName
},
pubKeyCredParams: [
{ alg: -7, type: 'public-key' }, // ES256 (P-256)
{ alg: -257, type: 'public-key' }, // RS256 (fallback)
],
authenticatorSelection: {
authenticatorAttachment: 'platform', // Use platform authenticator
userVerification: 'required', // Require biometric/PIN
residentKey: 'required', // Create discoverable credential
requireResidentKey: true // Ensure credential is discoverable
},
timeout: this.timeout,
attestation: 'direct', // Request attestation
excludeCredentials: options.excludeCredentials || []
};
// Add extensions
publicKeyCredentialCreationOptions.extensions = {
credProps: true, // Get credential properties
largeBlob: { // Enable large blob storage
support: 'preferred'
}
};
// Create credential
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});
// Convert credential for transmission
return this.credentialToJSON(credential);
}
/**
* Get authentication options from server
*/
async getAuthenticationOptions(username) {
const params = username ? `?username=${encodeURIComponent(username)}` : '';
const response = await fetch(`${this.apiEndpoint}/authenticate/options${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to get authentication options');
}
const options = await response.json();
// Convert base64 to ArrayBuffer
options.challenge = this.base64ToArrayBuffer(options.challenge);
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(cred => ({
...cred,
id: this.base64ToArrayBuffer(cred.id)
}));
}
return options;
}
/**
* Get credential assertion for authentication
*/
async getCredentialAssertion(options) {
const publicKeyCredentialRequestOptions = {
challenge: options.challenge,
rpId: this.rpId,
timeout: this.timeout,
userVerification: 'required',
allowCredentials: options.allowCredentials || []
};
// Check for conditional mediation support (autofill)
const conditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
let credential;
if (conditionalMediationAvailable && !options.allowCredentials.length) {
// Use conditional mediation for better UX
credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
mediation: 'conditional'
});
} else {
// Standard authentication flow
credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});
}
// Convert assertion for transmission
return this.assertionToJSON(credential);
}
/**
* Verify registration with server
*/
async verifyRegistration(credential, username) {
const response = await fetch(`${this.apiEndpoint}/register/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
credential
})
});
if (!response.ok) {
throw new Error('Registration verification failed');
}
return await response.json();
}
/**
* Verify authentication with server
*/
async verifyAuthentication(assertion, username) {
const response = await fetch(`${this.apiEndpoint}/authenticate/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
assertion
})
});
if (!response.ok) {
throw new Error('Authentication verification failed');
}
return await response.json();
}
/**
* Enable autofill for passkeys
*/
async enableAutofill(credentialId) {
// Add autocomplete attribute to username field
const usernameField = document.querySelector('input[name="username"]');
if (usernameField) {
usernameField.setAttribute('autocomplete', 'username webauthn');
}
// Store credential ID for autofill
if (window.localStorage) {
const storedCredentials = JSON.parse(
localStorage.getItem('passkey_credentials') || '[]'
);
if (!storedCredentials.includes(credentialId)) {
storedCredentials.push(credentialId);
localStorage.setItem(
'passkey_credentials',
JSON.stringify(storedCredentials)
);
}
}
}
/**
* Convert credential to JSON for transmission
*/
credentialToJSON(credential) {
const response = credential.response;
const clientExtensionResults = credential.getClientExtensionResults();
return {
id: credential.id,
rawId: this.arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
clientDataJSON: this.arrayBufferToBase64(response.clientDataJSON),
attestationObject: this.arrayBufferToBase64(response.attestationObject),
publicKey: response.publicKey ?
this.arrayBufferToBase64(response.publicKey) : undefined,
publicKeyAlgorithm: response.publicKeyAlgorithm,
authenticatorData: response.authenticatorData ?
this.arrayBufferToBase64(response.authenticatorData) : undefined
},
clientExtensionResults
};
}
/**
* Convert assertion to JSON for transmission
*/
assertionToJSON(assertion) {
const response = assertion.response;
const clientExtensionResults = assertion.getClientExtensionResults();
return {
id: assertion.id,
rawId: this.arrayBufferToBase64(assertion.rawId),
type: assertion.type,
response: {
clientDataJSON: this.arrayBufferToBase64(response.clientDataJSON),
authenticatorData: this.arrayBufferToBase64(response.authenticatorData),
signature: this.arrayBufferToBase64(response.signature),
userHandle: response.userHandle ?
this.arrayBufferToBase64(response.userHandle) : null
},
clientExtensionResults
};
}
/**
* Utility: Convert base64 to ArrayBuffer
*/
base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Utility: Convert ArrayBuffer to base64
*/
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
/**
* Handle and format errors
*/
handleError(error) {
if (error.name === 'NotAllowedError') {
return new Error('User declined to create or use a passkey');
} else if (error.name === 'InvalidStateError') {
return new Error('A passkey already exists for this account');
} else if (error.name === 'NotSupportedError') {
return new Error('Passkeys are not supported on this device');
} else if (error.name === 'AbortError') {
return new Error('The operation was aborted');
} else if (error.name === 'SecurityError') {
return new Error('The operation is not secure (requires HTTPS)');
} else {
return error;
}
}
/**
* Check if user has existing passkeys
*/
async hasPasskeys(username) {
try {
const response = await fetch(
`${this.apiEndpoint}/passkeys/check?username=${encodeURIComponent(username)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
);
if (response.ok) {
const data = await response.json();
return data.hasPasskeys || false;
}
return false;
} catch (error) {
console.error('Error checking passkeys:', error);
return false;
}
}
/**
* Delete a passkey
*/
async deletePasskey(credentialId) {
try {
const response = await fetch(`${this.apiEndpoint}/passkeys/${credentialId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// Remove from local storage
this.credentials.forEach((value, key) => {
if (value === credentialId) {
this.credentials.delete(key);
}
});
return true;
}
return false;
} catch (error) {
console.error('Error deleting passkey:', error);
return false;
}
}
}
// Usage Example
document.addEventListener('DOMContentLoaded', async () => {
const passkeyManager = new PasskeyWebManager({
rpName: 'My Secure App',
rpId: window.location.hostname,
apiEndpoint: '/api/auth'
});
// Check WebAuthn support
if (!passkeyManager.isSupported) {
console.warn('WebAuthn is not supported on this device');
// Fall back to traditional authentication
return;
}
// Register button handler
document.getElementById('register-passkey')?.addEventListener('click', async () => {
const username = document.getElementById('username').value;
const displayName = document.getElementById('display-name').value;
try {
const result = await passkeyManager.register(username, displayName);
console.log('Registration successful:', result);
// Update UI
document.getElementById('status').textContent =
'✅ Passkey registered successfully!';
} catch (error) {
console.error('Registration failed:', error);
document.getElementById('status').textContent =
`❌ Registration failed: ${error.message}`;
}
});
// Login button handler
document.getElementById('login-passkey')?.addEventListener('click', async () => {
const username = document.getElementById('username').value || null;
try {
const result = await passkeyManager.authenticate(username);
console.log('Authentication successful:', result);
// Update UI and redirect
document.getElementById('status').textContent =
'✅ Login successful!';
// Store token and redirect
sessionStorage.setItem('auth_token', result.token);
window.location.href = '/dashboard';
} catch (error) {
console.error('Authentication failed:', error);
document.getElementById('status').textContent =
`❌ Login failed: ${error.message}`;
}
});
});

Server Implementation: Backend Verification#

Node.js/Express Backend#

// server.js - Complete Passkey Backend Implementation
const express = require('express');
const crypto = require('crypto');
const base64url = require('base64url');
const cbor = require('cbor');
const { verifyAuthenticatorData, parseAuthData } = require('./webauthn-utils');
class PasskeyServer {
constructor() {
this.app = express();
this.challenges = new Map(); // Store challenges temporarily
this.credentials = new Map(); // Store registered credentials
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// CORS for WebAuthn
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
next();
});
}
setupRoutes() {
// Registration endpoints
this.app.post('/api/auth/register/options', this.getRegistrationOptions.bind(this));
this.app.post('/api/auth/register/verify', this.verifyRegistration.bind(this));
// Authentication endpoints
this.app.get('/api/auth/authenticate/options', this.getAuthenticationOptions.bind(this));
this.app.post('/api/auth/authenticate/verify', this.verifyAuthentication.bind(this));
// Management endpoints
this.app.get('/api/auth/passkeys/check', this.checkPasskeys.bind(this));
this.app.delete('/api/auth/passkeys/:id', this.deletePasskey.bind(this));
}
/**
* Generate registration options
*/
getRegistrationOptions(req, res) {
const { username } = req.body;
if (!username) {
return res.status(400).json({ error: 'Username required' });
}
// Generate challenge
const challenge = crypto.randomBytes(32);
const challengeBase64 = base64url.encode(challenge);
// Store challenge for verification
this.challenges.set(username, {
challenge: challengeBase64,
timestamp: Date.now()
});
// Get existing credentials to exclude
const userCredentials = this.getUserCredentials(username);
const excludeCredentials = userCredentials.map(cred => ({
id: cred.credentialId,
type: 'public-key',
transports: ['internal']
}));
// Generate user ID
const userId = crypto.createHash('sha256')
.update(username)
.digest();
const options = {
challenge: challengeBase64,
rp: {
name: 'My Secure App',
id: req.hostname
},
user: {
id: base64url.encode(userId),
name: username,
displayName: username
},
pubKeyCredParams: [
{ alg: -7, type: 'public-key' }, // ES256
{ alg: -257, type: 'public-key' } // RS256
],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'required',
residentKey: 'required',
requireResidentKey: true
},
timeout: 60000,
attestation: 'direct',
excludeCredentials
};
res.json(options);
}
/**
* Verify registration
*/
async verifyRegistration(req, res) {
const { username, credential } = req.body;
try {
// Verify challenge
const storedChallenge = this.challenges.get(username);
if (!storedChallenge) {
return res.status(400).json({
verified: false,
error: 'Challenge not found'
});
}
// Check challenge age (5 minutes max)
if (Date.now() - storedChallenge.timestamp > 300000) {
this.challenges.delete(username);
return res.status(400).json({
verified: false,
error: 'Challenge expired'
});
}
// Decode client data
const clientDataJSON = JSON.parse(
base64url.decode(credential.response.clientDataJSON)
);
// Verify challenge
if (clientDataJSON.challenge !== storedChallenge.challenge) {
return res.status(400).json({
verified: false,
error: 'Challenge mismatch'
});
}
// Verify origin
const expectedOrigin = `https://${req.hostname}`;
if (clientDataJSON.origin !== expectedOrigin &&
clientDataJSON.origin !== `http://${req.hostname}:3000`) {
return res.status(400).json({
verified: false,
error: 'Origin mismatch'
});
}
// Decode attestation object
const attestationObject = base64url.toBuffer(
credential.response.attestationObject
);
const attestation = cbor.decodeFirstSync(attestationObject);
// Parse authenticator data
const authData = parseAuthData(attestation.authData);
// Verify user presence and verification
if (!authData.flags.up || !authData.flags.uv) {
return res.status(400).json({
verified: false,
error: 'User verification failed'
});
}
// Extract public key
const publicKey = authData.publicKey;
// Store credential
this.storeCredential(username, {
credentialId: credential.id,
publicKey: publicKey,
signCount: authData.signCount,
createdAt: Date.now()
});
// Clean up challenge
this.challenges.delete(username);
res.json({
verified: true,
credentialId: credential.id
});
} catch (error) {
console.error('Registration verification error:', error);
res.status(500).json({
verified: false,
error: 'Verification failed'
});
}
}
/**
* Get authentication options
*/
getAuthenticationOptions(req, res) {
const { username } = req.query;
// Generate challenge
const challenge = crypto.randomBytes(32);
const challengeBase64 = base64url.encode(challenge);
// Store challenge
const challengeKey = username || 'anonymous';
this.challenges.set(challengeKey, {
challenge: challengeBase64,
timestamp: Date.now()
});
let allowCredentials = [];
if (username) {
// Get user's credentials
const userCredentials = this.getUserCredentials(username);
allowCredentials = userCredentials.map(cred => ({
id: cred.credentialId,
type: 'public-key',
transports: ['internal']
}));
}
const options = {
challenge: challengeBase64,
rpId: req.hostname,
timeout: 60000,
userVerification: 'required',
allowCredentials
};
res.json(options);
}
/**
* Verify authentication
*/
async verifyAuthentication(req, res) {
const { username, assertion } = req.body;
try {
// Find credential
const credential = this.findCredentialById(assertion.id);
if (!credential) {
return res.status(400).json({
verified: false,
error: 'Credential not found'
});
}
// Verify challenge
const challengeKey = username || 'anonymous';
const storedChallenge = this.challenges.get(challengeKey);
if (!storedChallenge) {
return res.status(400).json({
verified: false,
error: 'Challenge not found'
});
}
// Decode client data
const clientDataJSON = JSON.parse(
base64url.decode(assertion.response.clientDataJSON)
);
// Verify challenge
if (clientDataJSON.challenge !== storedChallenge.challenge) {
return res.status(400).json({
verified: false,
error: 'Challenge mismatch'
});
}
// Parse authenticator data
const authData = parseAuthData(
base64url.toBuffer(assertion.response.authenticatorData)
);
// Verify user presence and verification
if (!authData.flags.up || !authData.flags.uv) {
return res.status(400).json({
verified: false,
error: 'User verification failed'
});
}
// Verify signature
const signatureBase = Buffer.concat([
base64url.toBuffer(assertion.response.authenticatorData),
crypto.createHash('sha256')
.update(base64url.toBuffer(assertion.response.clientDataJSON))
.digest()
]);
const signature = base64url.toBuffer(assertion.response.signature);
const publicKey = credential.publicKey;
// Verify with public key (simplified - use proper crypto library)
const isValid = this.verifySignature(signatureBase, signature, publicKey);
if (!isValid) {
return res.status(400).json({
verified: false,
error: 'Signature verification failed'
});
}
// Check and update sign count
if (authData.signCount <= credential.signCount) {
console.warn('Possible cloned authenticator detected');
}
credential.signCount = authData.signCount;
// Generate session token
const token = this.generateSessionToken(credential.username);
// Clean up challenge
this.challenges.delete(challengeKey);
res.json({
verified: true,
token: token,
user: {
username: credential.username
}
});
} catch (error) {
console.error('Authentication verification error:', error);
res.status(500).json({
verified: false,
error: 'Verification failed'
});
}
}
/**
* Helper methods
*/
getUserCredentials(username) {
const credentials = [];
this.credentials.forEach((cred, id) => {
if (cred.username === username) {
credentials.push({ ...cred, credentialId: id });
}
});
return credentials;
}
findCredentialById(credentialId) {
return this.credentials.get(credentialId);
}
storeCredential(username, credentialData) {
this.credentials.set(credentialData.credentialId, {
...credentialData,
username
});
}
generateSessionToken(username) {
// Generate JWT or session token
const payload = {
username,
timestamp: Date.now(),
nonce: crypto.randomBytes(16).toString('hex')
};
// In production, use proper JWT library
return base64url.encode(JSON.stringify(payload));
}
verifySignature(data, signature, publicKey) {
// Simplified signature verification
// In production, use proper crypto library
return true; // Placeholder
}
checkPasskeys(req, res) {
const { username } = req.query;
const credentials = this.getUserCredentials(username);
res.json({
hasPasskeys: credentials.length > 0,
count: credentials.length
});
}
deletePasskey(req, res) {
const { id } = req.params;
if (this.credentials.has(id)) {
this.credentials.delete(id);
res.json({ success: true });
} else {
res.status(404).json({ error: 'Credential not found' });
}
}
start(port = 3000) {
this.app.listen(port, () => {
console.log(`Passkey server running on port ${port}`);
});
}
}
// Start server
const server = new PasskeyServer();
server.start();

Migration Strategy: From Passwords to Passkeys#

Progressive Enhancement Approach#

// Migration strategy implementation
class PasskeyMigrationManager {
constructor(private config: MigrationConfig) {
this.phase = config.initialPhase || 'awareness';
}
async evaluateUserReadiness(userId: string): Promise<MigrationPhase> {
const metrics = await this.getUserMetrics(userId);
if (metrics.hasPasskey) {
return 'completed';
}
if (metrics.supportedDevice && metrics.loginCount > 5) {
return 'ready';
}
if (metrics.supportedDevice) {
return 'eligible';
}
return 'not_ready';
}
async promptForMigration(userId: string): Promise<void> {
const phase = await this.evaluateUserReadiness(userId);
switch (phase) {
case 'ready':
await this.showPasskeySetupPrompt(userId);
break;
case 'eligible':
await this.showEducationalContent(userId);
break;
case 'not_ready':
// Don't prompt unsupported devices
break;
}
}
private async showPasskeySetupPrompt(userId: string): Promise<void> {
// Show non-intrusive prompt
const prompt = {
title: 'Upgrade to Passkeys',
message: 'Sign in faster and more securely with Face ID/Touch ID',
buttons: [
{ text: 'Set Up Now', action: 'setup' },
{ text: 'Learn More', action: 'info' },
{ text: 'Later', action: 'dismiss' }
]
};
// Track user response
await this.trackMigrationEvent(userId, 'prompt_shown');
}
}

Conclusion: The Future is Passwordless#

Apple’s Passkeys implementation represents the culmination of decades of authentication evolution. By combining the security of public key cryptography with the convenience of biometric authentication, Passkeys finally deliver on the promise of passwordless authentication.

Key Implementation Takeaways#

  1. Start with Progressive Enhancement: Support both passwords and passkeys during transition
  2. Focus on User Experience: Make registration and authentication seamless
  3. Implement Proper Verification: Server-side verification is critical for security
  4. Plan for Recovery: Have backup authentication methods ready
  5. Monitor Adoption: Track metrics to optimize the migration process

The passwordless future is here. Start implementing Passkeys today.

Resources#


Last Updated: January 10, 2025 Implementation Level: Production Ready

Apple Passkeys Complete Implementation Guide: Building Passwordless Authentication for Enterprise Applications
https://mranv.pages.dev/posts/apple-passkeys-webauthn-fido2-implementation-2025/
Author
Anubhav Gain
Published at
2025-01-10
License
CC BY-NC-SA 4.0