Skip to main content

Overview

WebAuthn (Web Authentication) enables passwordless authentication using biometrics (fingerprint, Face ID) or hardware security keys (YubiKey, Titan). SuperTokens Core implements the WebAuthn standard for secure, phishing-resistant authentication. Standards: W3C Web Authentication API (WebAuthn Level 2) Implementation: Uses webauthn4j library for credential verification

Core Concepts

Relying Party (RP)

Your application acts as the relying party:
  • RP ID: Your domain (e.g., “example.com”)
  • RP Name: Display name (e.g., “My App”)
  • Origin: Full URL (e.g., “https://example.com”)

Credentials

Each user can have multiple credentials:
  • Biometric authenticators (Touch ID, Face ID, Windows Hello)
  • Security keys (YubiKey, Titan Key)
  • Platform authenticators (TPM, Secure Enclave)

Challenge-Response

WebAuthn uses cryptographic challenge-response:
  1. Server generates random challenge
  2. Client signs challenge with private key
  3. Server verifies signature with public key

Registration Flow

Generate Registration Options

Create options for credential registration. Implementation: io.supertokens.webauthn.WebAuthN.generateRegisterOptions() - View source API Endpoint: POST /recipe/webauthn/options/register Request Body:
{
  "email": "[email protected]",
  "displayName": "John Doe",
  "relyingPartyName": "My App",
  "relyingPartyId": "example.com",
  "origin": "https://example.com",
  "timeout": 60000,
  "attestation": "none",
  "residentKey": "preferred",
  "userVerification": "preferred",
  "supportedAlgorithmIds": [-7, -257],
  "userPresenceRequired": true
}
Response:
{
  "status": "OK",
  "optionsId": "generated-options-id",
  "createdAt": 1234567890,
  "expiresAt": 1234627890,
  "publicKey": {
    "challenge": "base64-encoded-challenge",
    "rp": {
      "name": "My App",
      "id": "example.com"
    },
    "user": {
      "id": "base64-user-id",
      "name": "[email protected]",
      "displayName": "John Doe"
    },
    "pubKeyCredParams": [
      {"type": "public-key", "alg": -7},
      {"type": "public-key", "alg": -257}
    ],
    "timeout": 60000,
    "attestation": "none",
    "authenticatorSelection": {
      "residentKey": "preferred",
      "userVerification": "preferred"
    }
  }
}
Supported Algorithms:
  • -7: ES256 (ECDSA with SHA-256)
  • -257: RS256 (RSASSA-PKCS1-v1_5 with SHA-256)

Register Credential

Verify and store the credential. Implementation: io.supertokens.webauthn.WebAuthN.registerCredentials() - View source Process:
  1. Retrieve generated options
  2. Validate credential format
  3. Verify attestation signature
  4. Extract public key
  5. Store credential
API Endpoint: POST /recipe/webauthn/credentials/register Request Body:
{
  "userId": "user-id",
  "optionsId": "options-id",
  "credential": {
    "id": "credential-id",
    "rawId": "base64-raw-id",
    "type": "public-key",
    "response": {
      "clientDataJSON": "base64-client-data",
      "attestationObject": "base64-attestation"
    }
  }
}
Response:
{
  "status": "OK",
  "credential": {
    "credentialId": "credential-id",
    "userId": "user-id",
    "relyingPartyId": "example.com",
    "createdAt": 1234567890
  }
}

Sign Up with Credential

Create a new user and credential in one transaction. Implementation: io.supertokens.webauthn.WebAuthN.signUp() - View source API Endpoint: POST /recipe/webauthn/signup Request Body:
{
  "optionsId": "options-id",
  "credential": {
    "id": "credential-id",
    "rawId": "base64-raw-id",
    "type": "public-key",
    "response": {
      "clientDataJSON": "base64-client-data",
      "attestationObject": "base64-attestation"
    }
  }
}
Response:
{
  "status": "OK",
  "user": {
    "id": "user-id",
    "email": "[email protected]",
    "timeJoined": 1234567890
  },
  "credential": {
    "credentialId": "credential-id"
  }
}

Authentication Flow

Generate Sign-In Options

Create options for authentication. Implementation: io.supertokens.webauthn.WebAuthN.generateSignInOptions() - View source API Endpoint: POST /recipe/webauthn/options/signin Request Body:
{
  "relyingPartyId": "example.com",
  "relyingPartyName": "My App",
  "origin": "https://example.com",
  "timeout": 60000,
  "userVerification": "preferred",
  "userPresenceRequired": true
}
Response:
{
  "status": "OK",
  "optionsId": "options-id",
  "createdAt": 1234567890,
  "publicKey": {
    "challenge": "base64-challenge",
    "timeout": 60000,
    "rpId": "example.com",
    "userVerification": "preferred"
  }
}

Sign In

Verify credential and authenticate user. Implementation: io.supertokens.webauthn.WebAuthN.signIn() - View source Process:
  1. Retrieve sign-in options
  2. Load credential by ID
  3. Verify authentication data
  4. Update signature counter
  5. Return user information
API Endpoint: POST /recipe/webauthn/signin Request Body:
{
  "optionsId": "options-id",
  "credential": {
    "id": "credential-id",
    "rawId": "base64-raw-id",
    "type": "public-key",
    "response": {
      "clientDataJSON": "base64-client-data",
      "authenticatorData": "base64-authenticator-data",
      "signature": "base64-signature",
      "userHandle": "base64-user-handle"
    }
  }
}
Response:
{
  "status": "OK",
  "user": {
    "id": "user-id",
    "email": "[email protected]",
    "timeJoined": 1234567890
  }
}

Credential Management

List Credentials

Get all credentials for a user. Implementation: io.supertokens.webauthn.WebAuthN.listCredentialsForUser() - View source API Endpoint: GET /recipe/webauthn/credentials/list?userId={userId} Response:
{
  "status": "OK",
  "credentials": [
    {
      "credentialId": "credential-1",
      "userId": "user-id",
      "relyingPartyId": "example.com",
      "createdAt": 1234567890,
      "counter": 5
    },
    {
      "credentialId": "credential-2",
      "userId": "user-id",
      "relyingPartyId": "example.com",
      "createdAt": 1234567891,
      "counter": 3
    }
  ]
}

Get Credential

Retrieve specific credential details. Implementation: io.supertokens.webauthn.WebAuthN.loadCredentialByIdForUser() - View source API Endpoint: GET /recipe/webauthn/credential?credentialId={id}&userId={userId}

Remove Credential

Delete a credential. Implementation: io.supertokens.webauthn.WebAuthN.removeCredential() - View source API Endpoint: POST /recipe/webauthn/credentials/remove Request Body:
{
  "userId": "user-id",
  "credentialId": "credential-id"
}

Account Recovery

WebAuthn accounts can be recovered via email.

Generate Recovery Token

Implementation: io.supertokens.webauthn.WebAuthN.generateRecoverAccountToken() - View source API Endpoint: POST /recipe/webauthn/account/recover/token Request Body:
{
  "email": "[email protected]",
  "userId": "user-id"
}
Response:
{
  "status": "OK",
  "token": "recovery-token"
}

Consume Recovery Token

Implementation: io.supertokens.webauthn.WebAuthN.consumeRecoverAccountToken() - View source API Endpoint: POST /recipe/webauthn/account/recover/token/consume Request Body:
{
  "token": "recovery-token"
}
Response:
{
  "status": "OK",
  "userId": "user-id",
  "email": "[email protected]"
}

User Management

Update User Email

Implementation: io.supertokens.webauthn.WebAuthN.updateUserEmail() - View source API Endpoint: PUT /recipe/webauthn/user/email Request Body:
{
  "userId": "user-id",
  "email": "[email protected]"
}

Configuration

WebAuthn Settings

# Recovery token lifetime (1 hour)
webauthn_recover_account_token_lifetime: 3600000

Relying Party Configuration

Configure in your application: RP ID:
  • Must match the domain
  • Can be the root domain or subdomain
  • Example: “example.com” or “auth.example.com”
Origin:

Security Features

Attestation Verification

Verify authenticator authenticity during registration. Implementation: io.supertokens.webauthn.WebAuthN.verifyRegistrationData() - View source Attestation Types:
  • none: No attestation (most common)
  • indirect: Anonymized attestation
  • direct: Full attestation chain

Signature Counter

Detect cloned authenticators:
if (newCounter <= storedCounter && storedCounter != 0) {
    // Possible authenticator cloning
    throw new SecurityException("Counter decreased");
}

Challenge Validation

Challenges are:
  • 32 random bytes
  • One-time use
  • Time-limited (default: 60 seconds)
  • Cryptographically signed by authenticator

Origin Validation

Strict origin checking:
  • Must match exactly
  • No wildcards allowed
  • HTTPS required in production

WebAuthn Options

User Verification

Controls biometric/PIN verification:
  • required: Always verify user (biometric/PIN)
  • preferred: Verify if possible
  • discouraged: Skip verification
Recommendation: Use preferred for good UX and security balance.

Resident Keys

Also known as “discoverable credentials” or “passkeys”:
  • required: Must be stored on authenticator
  • preferred: Store if possible
  • discouraged: Don’t store on authenticator
Benefits:
  • Usernameless login
  • Better UX on mobile
  • Works across devices (with sync)

Attestation

Control authenticator verification:
  • none: No attestation (fastest)
  • indirect: Anonymized attestation
  • direct: Full attestation (privacy concerns)
Recommendation: Use none unless you need to restrict authenticator types.

Client-Side Integration

Registration (JavaScript)

// 1. Get options from server
const optionsResponse = await fetch('/recipe/webauthn/options/register', {
  method: 'POST',
  body: JSON.stringify({
    email: '[email protected]',
    displayName: 'John Doe',
    relyingPartyName: 'My App',
    relyingPartyId: 'example.com',
    origin: window.location.origin
  })
});
const { optionsId, publicKey } = await optionsResponse.json();

// 2. Convert challenge and user.id from base64
publicKey.challenge = base64ToArrayBuffer(publicKey.challenge);
publicKey.user.id = base64ToArrayBuffer(publicKey.user.id);

// 3. Create credential
const credential = await navigator.credentials.create({ publicKey });

// 4. Register with server
const registerResponse = await fetch('/recipe/webauthn/credentials/register', {
  method: 'POST',
  body: JSON.stringify({
    userId: 'user-id',
    optionsId,
    credential: {
      id: credential.id,
      rawId: arrayBufferToBase64(credential.rawId),
      type: credential.type,
      response: {
        clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
        attestationObject: arrayBufferToBase64(credential.response.attestationObject)
      }
    }
  })
});

Authentication (JavaScript)

// 1. Get options from server
const optionsResponse = await fetch('/recipe/webauthn/options/signin', {
  method: 'POST',
  body: JSON.stringify({
    relyingPartyId: 'example.com',
    origin: window.location.origin
  })
});
const { optionsId, publicKey } = await optionsResponse.json();

// 2. Convert challenge from base64
publicKey.challenge = base64ToArrayBuffer(publicKey.challenge);

// 3. Get credential
const credential = await navigator.credentials.get({ publicKey });

// 4. Sign in with server
const signinResponse = await fetch('/recipe/webauthn/signin', {
  method: 'POST',
  body: JSON.stringify({
    optionsId,
    credential: {
      id: credential.id,
      rawId: arrayBufferToBase64(credential.rawId),
      type: credential.type,
      response: {
        clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
        authenticatorData: arrayBufferToBase64(credential.response.authenticatorData),
        signature: arrayBufferToBase64(credential.response.signature),
        userHandle: arrayBufferToBase64(credential.response.userHandle)
      }
    }
  })
});

Error Handling

Common Exceptions

  • WebauthNVerificationFailedException: Signature verification failed
  • WebauthNInvalidFormatException: Malformed credential data
  • WebauthNOptionsNotExistsException: Options expired or not found
  • InvalidWebauthNOptionsException: Invalid or expired options
  • DuplicateCredentialException: Credential already registered
  • InvalidTokenException: Recovery token invalid or expired

Best Practices

  1. HTTPS only: WebAuthn requires HTTPS (except localhost)
  2. User verification: Use preferred for best UX
  3. Resident keys: Enable for passkey support
  4. Multiple credentials: Allow users to register multiple devices
  5. Recovery flow: Implement email-based recovery
  6. Clear naming: Help users identify their credentials
  7. Error handling: Provide helpful error messages
  8. Browser support: Check for WebAuthn support
  9. Timeout: Use reasonable timeouts (60 seconds)
  10. Testing: Test on multiple browsers and devices

Browser Support

WebAuthn is supported in:
  • Chrome 67+
  • Firefox 60+
  • Safari 14+
  • Edge 18+
Platform authenticators:
  • Windows Hello (Windows 10+)
  • Touch ID / Face ID (iOS 14+, macOS)
  • Android biometrics (Android 9+)

Multi-Tenancy

WebAuthn credentials are tenant-specific:
  • Credentials are isolated per tenant
  • Relying Party ID can vary by tenant
  • Options validation respects tenant context

Migration

Importing WebAuthn users from other systems is not currently supported due to the cryptographic nature of credentials. Users must re-register their authenticators.

Build docs developers (and LLMs) love