WebAuthn & Passkeys
WebAuthn (Web Authentication API) provides phishing-resistant authentication using cryptographic credentials stored on the user's device or a roaming hardware key. It is the technology behind passkeys, Face ID, Touch ID, Windows Hello, and YubiKeys.
Ithbat IAM implements the full FIDO2/WebAuthn specification. Credentials are stored per user, and authentication uses a challenge-response ceremony that cannot be phished.
What WebAuthn Provides
| Authenticator type | Examples | How it works |
|---|---|---|
| Platform authenticator | Face ID, Touch ID, Windows Hello, Android biometrics | Built into the device — credential never leaves the device |
| Roaming authenticator | YubiKey 5, Google Titan | USB, NFC, or Bluetooth hardware key |
| Passkeys (synced) | iCloud Keychain, Google Password Manager | Platform credential synced across devices via cloud |
WebAuthn credentials registered in Ithbat IAM function as a standalone primary authentication factor (no password needed) or as a strong second factor after password login.
Browser Compatibility
WebAuthn is supported in all modern browsers:
| Browser | Minimum version |
|---|---|
| Chrome | 67+ |
| Safari | 14+ |
| Firefox | 60+ |
| Edge | 18+ |
| Samsung Internet | 10.2+ |
Registration Ceremony
A user must be authenticated (have a valid access token) to register a WebAuthn credential.
sequenceDiagram
participant User
participant App as Your App (Browser)
participant Ithbat as Ithbat IAM
participant Auth as Authenticator (Device / Key)
User->>App: Click "Add passkey"
App->>Ithbat: POST /api/v1/mfa/webauthn/register/start
Ithbat->>Ithbat: Generate registration challenge
Ithbat-->>App: { options: { publicKey: { challenge, rp, user, ... } } }
App->>Auth: navigator.credentials.create(options.publicKey)
Auth-->>User: Biometric prompt / key tap
User->>Auth: Approve
Auth-->>App: PublicKeyCredential (attestation)
App->>Ithbat: POST /api/v1/mfa/webauthn/register/finish (raw credential response)
Ithbat->>Ithbat: Verify attestation, store public key
Ithbat-->>App: { credentialId, message }
Step 1 — Begin Registration
POST /api/v1/mfa/webauthn/register/start
Required header: Authorization: Bearer <accessToken>
{
"credentialName": "My MacBook Pro"
}
The credentialName is optional. When provided, it is stored as a human-readable label for the credential.
Response — 200 OK
{
"options": {
"publicKey": {
"challenge": "dGhpcyBpcyBhIGNoYWxsZW5nZQ==",
"rp": {
"name": "Ithbat IAM",
"id": "api.ithbat.io"
},
"user": {
"id": "dXNlcl9pZA==",
"name": "[email protected]",
"displayName": "Sara Al-Rashidi"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"timeout": 60000,
"attestation": "none",
"authenticatorSelection": {
"residentKey": "preferred",
"userVerification": "preferred"
}
}
},
"message": "Present your authenticator to register"
}
Pass the entire options.publicKey object to the WebAuthn API.
Step 2 — Finish Registration
After the user approves the biometric prompt, the browser returns a PublicKeyCredential. Send the raw credential response to:
POST /api/v1/mfa/webauthn/register/finish
Required header: Authorization: Bearer <accessToken>
The request body should be the raw PublicKeyCredential JSON from navigator.credentials.create(). This is handled automatically by the Ithbat SDK.
Response — 200 OK
{
"credentialId": "dGhpcyBpcyBhIGNyZWRlbnRpYWwgaWQ=",
"message": "WebAuthn credential registered successfully"
}
Authentication Ceremony
WebAuthn login does not require a prior password entry. The user identifies themselves by email, and the authenticator proves possession of the private key.
sequenceDiagram
participant User
participant App as Your App (Browser)
participant Ithbat as Ithbat IAM
participant Auth as Authenticator
User->>App: Enter email
App->>Ithbat: POST /api/v1/mfa/webauthn/login/start { email }
Ithbat->>Ithbat: Lookup credentials for user, generate challenge
Ithbat-->>App: { options: { publicKey: { challenge, allowCredentials, ... } } }
App->>Auth: navigator.credentials.get(options.publicKey)
Auth-->>User: Biometric prompt / key tap
User->>Auth: Approve
Auth-->>App: PublicKeyCredential (assertion)
App->>Ithbat: POST /api/v1/mfa/webauthn/login/finish { email, <credential response> }
Ithbat->>Ithbat: Verify assertion signature against stored public key
Ithbat-->>App: { accessToken, refreshToken, expiresIn }
App-->>User: Signed in
Step 1 — Begin Login
POST /api/v1/mfa/webauthn/login/start
{
"email": "[email protected]"
}
Response — 200 OK
{
"options": {
"publicKey": {
"challenge": "cmFuZG9tQ2hhbGxlbmdl",
"allowCredentials": [
{
"type": "public-key",
"id": "dGhpcyBpcyBhIGNyZWRlbnRpYWwgaWQ=",
"transports": ["internal", "hybrid"]
}
],
"timeout": 60000,
"userVerification": "preferred"
}
},
"message": "Present your authenticator to sign in"
}
Pass options.publicKey to navigator.credentials.get().
Step 2 — Finish Login
POST /api/v1/mfa/webauthn/login/finish
Send the raw assertion response from navigator.credentials.get(), plus the email:
{
"email": "[email protected]"
}
Include the credential assertion as the full request body (the SDK handles this merging automatically).
Response — 200 OK
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"expiresIn": 3600,
"message": "WebAuthn login successful"
}
Managing Credentials
List Credentials
GET /api/v1/mfa/webauthn/credentials
Required header: Authorization: Bearer <accessToken>
Response — 200 OK
{
"credentials": [
{
"id": "cred_01J8X...",
"name": "My MacBook Pro",
"transports": ["internal"],
"createdAt": "2026-02-20T08:30:00Z",
"lastUsedAt": "2026-02-24T10:00:00Z",
"aaguidReadable": "YWFndWlkX2Jhc2U2NA=="
}
],
"total": 1
}
| Field | Description |
|---|---|
id | UUID of the credential record |
name | Human-readable label set during registration |
transports | How the authenticator communicates: internal, usb, nfc, ble, hybrid |
createdAt | When the credential was registered |
lastUsedAt | Last successful authentication time |
aaguidReadable | Base64-encoded AAGUID (identifies the authenticator model) |
Delete a Credential
DELETE /api/v1/mfa/webauthn/credentials/{id}
Required header: Authorization: Bearer <accessToken>
Response — 200 OK
{
"message": "WebAuthn credential deleted successfully"
}
If a user deletes their only WebAuthn credential and has no password or other MFA method, they may be locked out. Your UI should warn users before deleting their last credential.
Code Examples
Full Registration Flow (JavaScript)
import { IthbatSDK } from '@ithbatiam/sdk';
const ithbat = new IthbatSDK({ tenantId: 'ten_01J8X...' });
async function registerPasskey(credentialName) {
// Step 1: Get registration options from Ithbat IAM
const { options } = await ithbat.webauthn.beginRegistration({
credentialName,
});
// Step 2: Call the WebAuthn API — browser prompts for biometric/key
const credential = await navigator.credentials.create(options);
// Step 3: Send the result to Ithbat IAM
const result = await ithbat.webauthn.finishRegistration(credential);
console.log('Passkey registered:', result.credentialId);
}
Full Authentication Flow (JavaScript)
async function loginWithPasskey(email) {
// Step 1: Get authentication options
const { options } = await ithbat.webauthn.beginLogin({ email });
// Step 2: Call the WebAuthn API — browser prompts for biometric/key
const assertion = await navigator.credentials.get(options);
// Step 3: Verify with Ithbat IAM
const session = await ithbat.webauthn.finishLogin({ email, assertion });
console.log('Signed in:', session.accessToken);
}
Managing Credentials
// List all registered passkeys
const { credentials } = await ithbat.webauthn.listCredentials();
for (const cred of credentials) {
console.log(`${cred.name} — last used ${cred.lastUsedAt}`);
}
// Delete a credential by ID
await ithbat.webauthn.deleteCredential('cred_01J8X...');
Using the Raw WebAuthn API (No SDK)
// Begin registration
const startResp = await fetch('/api/v1/mfa/webauthn/register/start', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ credentialName: 'My Device' }),
});
const { options } = await startResp.json();
// Decode base64url challenge and user.id
options.publicKey.challenge = base64urlDecode(options.publicKey.challenge);
options.publicKey.user.id = base64urlDecode(options.publicKey.user.id);
// Create credential
const credential = await navigator.credentials.create(options);
// Encode response for transmission
const credentialForTransmit = {
id: credential.id,
rawId: base64urlEncode(credential.rawId),
type: credential.type,
response: {
attestationObject: base64urlEncode(credential.response.attestationObject),
clientDataJSON: base64urlEncode(credential.response.clientDataJSON),
},
};
// Finish registration
await fetch('/api/v1/mfa/webauthn/register/finish', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(credentialForTransmit),
});
The Ithbat SDK handles all base64url encoding/decoding automatically. Use the SDK unless you have a specific reason to call the raw API.
Error Handling
| HTTP Status | Error Code | Cause |
|---|---|---|
400 | VALIDATION_ERROR | Missing email in login start/finish |
401 | UNAUTHORIZED | Authentication required for registration |
401 | UNAUTHORIZED | Assertion verification failed (wrong key, wrong origin) |
401 | UNAUTHORIZED | User not found for given email |
404 | NOT_FOUND | No WebAuthn credentials registered for user |
Next Steps
- MFA — use WebAuthn as a second factor alongside a password
- Token Lifecycle — manage the tokens issued after WebAuthn login
- Passwordless — magic link as an alternative passwordless method