Passkeys / WebAuthn
WebAuthn (Web Authentication, part of the FIDO2 standard) enables passwordless and second-factor authentication using device biometrics (Touch ID, Face ID, Windows Hello), platform authenticators built into the operating system, or roaming hardware security keys (YubiKey, Titan Key).
Auris implements the WebAuthn API as both a standalone passwordless authentication method and as a 2FA second factor following email/password login.
Browser Support
WebAuthn is supported in all modern browsers:
| Browser | Version | Platform Authenticator | Roaming Keys |
|---|---|---|---|
| Chrome | 67+ | Yes (Windows Hello, Touch ID) | Yes (USB, NFC, BLE) |
| Safari | 14+ | Yes (Touch ID, Face ID) | Yes |
| Firefox | 60+ | Limited | Yes (USB) |
| Edge | 18+ | Yes (Windows Hello) | Yes |
| Safari (iOS) | 14+ | Yes (Face ID, Touch ID) | Yes |
| Chrome (Android) | 70+ | Yes (fingerprint) | Yes (NFC) |
Passkeys (synced credentials via iCloud Keychain, Google Password Manager, or 1Password) are a form of WebAuthn. Auris supports passkey registration and authentication when the device’s authenticator supports the residentKey requirement.
How It Works
Registration Flow
Server generates a registration challenge
Auris generates a cryptographically random challenge and returns it along with registration options (relying party info, user info, allowed credential types, authenticator selection criteria).
Browser creates a credential
The browser calls navigator.credentials.create() with the registration options. The platform authenticator (or security key) prompts the user for biometric or PIN confirmation, generates a public/private key pair, and returns the public key and an attestation object.
Server stores the credential
Your application sends the credential data to Auris. Auris verifies the attestation, extracts the public key, and stores the credential ID and public key in the database associated with the user’s account.
Authentication Flow
Server generates an authentication challenge
Auris generates a new challenge and returns authentication options including the list of credential IDs registered for the user (or empty for discoverable/resident key flow).
Browser performs an assertion
The browser calls navigator.credentials.get(). The authenticator finds a matching credential, signs the challenge with the private key, and returns the assertion.
Server verifies the assertion
Auris verifies the assertion signature using the stored public key. If valid, the authentication is complete and tokens are issued.
Client-Side Library
Auris uses @simplewebauthn/browser on the client side to handle the browser WebAuthn API calls. This library abstracts browser compatibility quirks and provides typed wrappers around navigator.credentials.create() and navigator.credentials.get().
npm install @simplewebauthn/browserImplementation
Registering a Passkey
JavaScript
import { startRegistration } from '@simplewebauthn/browser'
async function registerPasskey(accessToken) {
// Step 1: Get registration options from Auris
const optionsResponse = await fetch('/api/user/2fa/webauthn/challenge', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'registration' }),
})
const options = await optionsResponse.json()
// Step 2: Browser prompts user for biometric/security key
let credential
try {
credential = await startRegistration(options)
} catch (err) {
if (err.name === 'NotAllowedError') {
console.error('User cancelled or timed out')
return
}
throw err
}
// Step 3: Send credential to Auris
const verifyResponse = await fetch('/api/user/2fa/webauthn', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential,
name: 'My MacBook', // User-provided name for the credential
}),
})
const result = await verifyResponse.json()
if (result.verified) {
console.log('Passkey registered successfully')
}
}Authenticating with a Passkey
import { startAuthentication } from '@simplewebauthn/browser'
async function authenticateWithPasskey() {
// Step 1: Get authentication options from Auris
const optionsResponse = await fetch('/api/auth/webauthn/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const options = await optionsResponse.json()
// Step 2: Browser prompts user (biometric or key tap)
let assertion
try {
assertion = await startAuthentication(options)
} catch (err) {
if (err.name === 'NotAllowedError') {
console.error('User cancelled or no matching credential found')
return
}
throw err
}
// Step 3: Verify assertion and receive tokens
const verifyResponse = await fetch('/api/auth/webauthn/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assertion }),
})
const result = await verifyResponse.json()
if (result.accessToken) {
// Store tokens and redirect
console.log('Authenticated as:', result.user)
}
}WebAuthn as 2FA
When configured as a second factor (rather than a standalone passwordless method), WebAuthn is used after the first factor (email/password or social) is completed.
Configuration: Enable WebAuthn 2FA in Console → Authentication → Multi-Factor Authentication → Passkeys.
2FA flow:
- User completes first-factor authentication
- Auris issues a partial session token (not a full access token)
- Your application uses the partial token to call the WebAuthn challenge endpoint
- User taps their security key or scans biometric
- Assertion is verified; Auris issues full access and refresh tokens
/api/user/2fa/webauthnRegister a new WebAuthn credential for the authenticated user. Requires a valid registration challenge from the challenge endpoint.
/api/user/2fa/webauthn/challengeGenerate a WebAuthn registration or authentication challenge. Body: { type: 'registration' | 'authentication' }.
/api/user/2fa/webauthn/:credentialIdRemove a registered WebAuthn credential by its ID.
Credential Management
Users can register multiple credentials — one per device or security key. Each credential can be given a friendly name for identification.
Best practice is to allow users to register at least two credentials (e.g., a laptop’s Touch ID and a hardware key) so they have a backup if one device is lost.
Auris stores per credential:
- Credential ID (base64url-encoded, unique identifier)
- Public key (COSE format)
- Sign count (incremented on each use; Auris detects authenticator cloning if the count regresses)
- User-provided name
- Last used timestamp
- Creation timestamp
- Authenticator attachment type (platform or cross-platform)
Security Considerations
Phishing resistance — WebAuthn credentials are bound to the relying party origin (rpId, which is your domain). A phishing site on a different domain cannot use credentials issued for your domain. This is a significant advantage over passwords and OTP codes.
No shared secrets — The private key never leaves the authenticator. Auris stores only the public key. A database breach does not expose any credential material.
Sign counter validation — Auris tracks the authenticator’s internal signature counter. If an assertion arrives with a counter equal to or less than the stored value, Auris flags the credential as potentially cloned and requires review.
Resident keys and discoverable credentials — When residentKey: required is set, the credential is stored on the authenticator itself and can be used without providing a username first (username-less login). This is the basis for modern passkey flows.
For maximum security, combine WebAuthn with a platform authenticator (built into the device) rather than a roaming security key alone. Platform authenticators require device unlock (biometric or PIN), adding a possession + knowledge factor without user friction.
Related Guides
- SMS OTP — Phone-based second factor
- Hosted Login (PKCE) — Standard interactive authentication flow
- Magic Links — Email-based passwordless authentication