This guide demonstrates how to:
- Generate a registration challenge
- Verify a registration response
- Save the required credential data for future authentication
- credential_id: String identifier for the credential (base64url encoded)
- public_key: The credential's public key (base64url encoded)
- sign_count: The initial sign count (usually starts at 0)
- Most authenticators (e.g. from Apple, Microsoft, Google) will not be incrementing this value.
- For those that does, this can help to detect cloning of authenticators.
These values are returned in RegVerifyResult when verification succeeds.
Server-side (Zig):
Generate challenge and pass it to client-side code.
const std = @import("std");
const passcay = @import("passcay");
// Create an allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Generate a cryptographically secure random challenge for this registration attempt
// Store this challenge in your session or database temporarily to verify the response
// IMPORTANT: This challenge must be unique for each registration attempt to prevent replay attacks
const challenge = try passcay.challenge.generate(allocator);
defer allocator.free(challenge);Client-side (JavaScript):
// Configure registration options using the challenge from the server
const registrationOptions = {
challenge: base64UrlDecode(challenge),
rp: {
name: "Your App Name",
id: "yourdomain.com" // Must match your domain
},
user: {
id: new TextEncoder().encode(user_id_or_username), // Convert to ArrayBuffer
name: "username@example.com",
displayName: "User's Display Name"
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256 (most widely supported)
{ alg: -257, type: "public-key" } // RS256
],
timeout: 60000,
attestation: "none", // Default for privacy with no security downsides
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
requireResidentKey: false,
},
};
// Trigger passkey registration
const credential = await navigator.credentials.create({ publicKey: registrationOptions });Upon successful Passkey registration, the JavaScript credential object will contain:
id: The credential ID (base64url encoded)rawId: The raw ID of the credential (base64url encoded)response: The response object containing attestation data:response.attestationObject: The attestation object (base64url encoded)response.clientDataJSON: The client data JSON (base64url encoded)
You'll only be needing attestationObject and clientDataJSON for the server-side verification at Step 3.
Server-side (Zig):
Verify the registration response with the challenge generated in Step 1.
// Input from the above JavaScript `credential`.
const reg_input = passcay.register.RegVerifyInput{
.attestation_object = response.attestationObject, // Base64URL encoded
.client_data_json = response.clientDataJSON, // Base64URL encoded
};
// Verify against these known expectations
// It is important to verify the challenge and origin to prevent replay attacks.
const reg_expectations = passcay.register.RegVerifyExpectations{
.challenge = challenge_from_session, // The challenge generated in Step 1
.origin = "https://yourdomain.com", // Origin of your web app
.rp_id = "yourdomain.com", // RP ID for your domain
.require_user_verification = true, // Whether user verification is required
};
// Perform verification. It returns error if verification fails.
const result = try passcay.register.verify(allocator, reg_input, reg_expectations);
defer result.deinit(allocator); // Clean up resources when doneUpon successful verification, and thus registration, the result contains these fields that you should store in database for future authentication:
credential_id: (base64url string)public_key: (base64url string)sign_count: (u32) usually starts at 0, may not be incremented, safe to ignoreaaguid: (base64url string) may not be useful, safe to ignore as many authenticators anonymize this
Link the above to the user in database.