Skip to main content

Passkeys (WebAuthn)

FIDO2 WebAuthn implementation for passwordless authentication.

Overview

Passkeys allow users to authenticate using:

  • Platform authenticators: Touch ID, Face ID, Windows Hello
  • Roaming authenticators: Security keys (YubiKey, etc.)

The implementation uses @simplewebauthn/server for WebAuthn operations.

Architecture

Passkeys Architecture

Database Schema

PasskeyCredential

Stores registered WebAuthn credentials.

ColumnTypeDescription
idUUIDPrimary key
userIdUUIDOwner user ID
credentialIdBytesWebAuthn credential ID (unique)
credentialPublicKeyBytesCOSE public key
counterBigIntSignature counter (replay protection)
credentialDeviceTypeStringsingleDevice or multiDevice
credentialBackedUpBooleanSynced to cloud (iCloud Keychain, etc.)
transportsString[]internal, usb, ble, nfc, hybrid
deviceNameString?User-provided label
aaguidString?Authenticator AAGUID
revokedAtDateTime?Revocation timestamp

PasskeyChallenge

Temporary challenge storage for registration/authentication.

ColumnTypeDescription
idUUIDPrimary key (challengeId)
typeEnumREGISTRATION or AUTHENTICATION
challengeStringBase64URL challenge value
userIdUUID?Associated user (null for discoverable)
expiresAtDateTimeChallenge expiry (default 5 min)
usedAtDateTime?When consumed

API Endpoints

Registration Flow (Authenticated)

POST /auth/passkeys/register/options
Authorization: Bearer <token>
→ { challengeId, options }

POST /auth/passkeys/register/verify
Authorization: Bearer <token>
{ challengeId, response, deviceName? }
→ { id, deviceName, credentialDeviceType, ... }

Authentication Flow (Public)

POST /auth/passkeys/authenticate/options
{ email? }
→ { challengeId, options }

POST /auth/passkeys/authenticate/verify
X-Client-Audience: teetime-admin
X-Token-Delivery: cookie (optional)
{ challengeId, response }
→ { accessToken, refreshToken, jti } | { ok: true }

Management (Authenticated)

GET /auth/passkeys
→ { items: [...] }

PATCH /auth/passkeys/:id
{ deviceName }
→ { ... }

DELETE /auth/passkeys/:id
{ reason? }
→ 204 No Content

Configuration

VariableDefaultDescription
WEBAUTHN_RP_IDlocalhostRelying party ID (domain)
WEBAUTHN_RP_NAMEDigiWedgeDisplay name
WEBAUTHN_ORIGINhttp://localhost:3000,http://localhost:4200Allowed origins (CSV)
IDP_PASSKEYS_ENABLED1Enable passkey endpoints (0 disables)
WEBAUTHN_ATTESTATION_TYPEnonenone, direct, indirect
WEBAUTHN_USER_VERIFICATIONpreferredrequired, preferred, discouraged
WEBAUTHN_CHALLENGE_TIMEOUT_MS300000Challenge validity (5 min)
WEBAUTHN_RESIDENT_KEYpreferredrequired, preferred, discouraged

Feature flags

  • IDP_PASSKEYS_ENABLED=0 disables all passkey endpoints (registration, authentication, management).
  • VITE_PASSKEYS_ENABLED=0 hides passkey UI in Access Control Admin.

Production Configuration

WEBAUTHN_RP_ID=digiwedge.com
WEBAUTHN_RP_NAME=DigiWedge
WEBAUTHN_ORIGIN=https://admin.digiwedge.com,https://access-control-admin.digiwedge.com
WEBAUTHN_ATTESTATION_TYPE=none
WEBAUTHN_USER_VERIFICATION=preferred

Attestation Policy

See ADR-0001: Passkey Attestation Policy.

Default: none — No attestation verification. Maximizes device compatibility and user privacy.

Attestation objects and AAGUIDs are stored for audit purposes, enabling future policy upgrades.

Audit Events

EventDescription
PASSKEY_REGISTEREDNew passkey registered
PASSKEY_AUTHENTICATION_SUCCESSSuccessful passkey login
PASSKEY_AUTHENTICATION_FAILUREFailed passkey login attempt
PASSKEY_UPDATEDPasskey renamed
PASSKEY_REVOKEDPasskey revoked

Metrics

Prometheus counters are emitted via the default registry:

MetricLabelsDescription
idp_passkey_registrations_totalstatusPasskey registration outcomes
idp_passkey_authentications_totalstatus, reasonPasskey authentication outcomes
idp_passkey_management_totalactionPasskey update/revoke actions

Security Considerations

Challenge Handling

  • Challenges are single-use and expire after 5 minutes
  • Challenge ID is used for lookup (not extracted from clientDataJSON)
  • Challenge mismatch between stored and client-provided triggers rejection

Counter Verification

  • Signature counter is validated on each authentication
  • Counter must be greater than stored value (replay protection)
  • Counter is updated after successful verification

Credential Revocation

  • Revoked credentials cannot authenticate
  • Revocation is soft-delete (revokedAt timestamp)
  • Revoked passkeys remain visible in management UI

Troubleshooting

"Unknown credential"

  • Credential not found in database
  • User may be using a passkey registered on a different RP ID

"Credential revoked"

  • Passkey has been revoked by user or admin
  • User should register a new passkey

"Challenge mismatch"

  • Challenge in clientDataJSON doesn't match stored challenge
  • May indicate replay attack or corrupted request

"User is inactive"

  • User account is deactivated
  • Contact administrator to reactivate

Registration failures

  • Check browser console for WebAuthn errors
  • Ensure RP ID matches the domain
  • Verify HTTPS is used in production

Rollout Checklist

Pre-Production

  • WEBAUTHN_RP_ID set to production domain
  • WEBAUTHN_ORIGIN includes all client origins
  • IDP_PASSKEYS_ENABLED=1 in environment
  • Grafana dashboard imported (grafana/dashboards/idp-passkeys.json)
  • Prometheus scraping IDP metrics endpoint
  • Database migrations applied (PasskeyCredential, PasskeyChallenge)

UAT Validation

  • Registration flow works in Chrome, Safari, Firefox
  • Authentication with registered passkey succeeds
  • Management UI lists, renames, and revokes passkeys
  • Passkey disabled state shows appropriate message
  • Health endpoint includes passkey status
  • Challenge cleanup cron is running (check logs hourly)

Production Go-Live

  • Feature flag enabled: IDP_PASSKEYS_ENABLED=1
  • Frontend flag enabled: VITE_PASSKEYS_ENABLED=1
  • Monitor idp_passkey_registrations_total for activity
  • Monitor idp_passkey_authentications_total{status="failure"} for issues
  • Support team briefed on passkey troubleshooting

Post-Launch

  • Review Grafana dashboard for anomalies
  • Check challenge cleanup logs for expired challenge volume
  • Collect user feedback on passkey onboarding UX