Skip to main content

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 typeExamplesHow it works
Platform authenticatorFace ID, Touch ID, Windows Hello, Android biometricsBuilt into the device — credential never leaves the device
Roaming authenticatorYubiKey 5, Google TitanUSB, NFC, or Bluetooth hardware key
Passkeys (synced)iCloud Keychain, Google Password ManagerPlatform credential synced across devices via cloud
note

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:

BrowserMinimum version
Chrome67+
Safari14+
Firefox60+
Edge18+
Samsung Internet10.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
}
FieldDescription
idUUID of the credential record
nameHuman-readable label set during registration
transportsHow the authenticator communicates: internal, usb, nfc, ble, hybrid
createdAtWhen the credential was registered
lastUsedAtLast successful authentication time
aaguidReadableBase64-encoded AAGUID (identifies the authenticator model)

Delete a Credential

DELETE /api/v1/mfa/webauthn/credentials/&#123;id&#125;

Required header: Authorization: Bearer <accessToken>

Response — 200 OK

{
"message": "WebAuthn credential deleted successfully"
}
warning

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),
});
tip

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 StatusError CodeCause
400VALIDATION_ERRORMissing email in login start/finish
401UNAUTHORIZEDAuthentication required for registration
401UNAUTHORIZEDAssertion verification failed (wrong key, wrong origin)
401UNAUTHORIZEDUser not found for given email
404NOT_FOUNDNo 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