Overview

WebAuthn (Web Authentication) provides passwordless authentication using passkeys, biometric authentication (fingerprint, Face ID), and hardware security keys. It offers a more secure and user-friendly alternative to traditional password-based authentication.

Key Concepts

WebAuthn Components

  • Authenticator: The device or app that creates and stores credentials (e.g., smartphone, hardware key, laptop with fingerprint reader)
  • Credential: A cryptographic key pair stored in the authenticator
  • Registration: Process of creating a new credential for a user
  • Authentication: Process of verifying a user using an existing credential

Benefits

  • Passwordless: No password to remember or steal
  • Phishing Resistant: Credentials are bound to the origin (domain)
  • Strong Security: Based on public-key cryptography
  • User-Friendly: Quick authentication with biometrics or security keys
  • Multi-Device: Sync credentials across devices with passkeys

Get WebAuthn Status

Check if the user has WebAuthn enabled and see registered credentials. Endpoint: GET /api/auth/webauthn/status Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
  "code": 200,
  "message": "WebAuthn status retrieved successfully",
  "data": {
    "is_enabled": true,
    "credentials_count": 2,
    "has_passkey": true
  }
}
Example:
async function getWebAuthnStatus() {
  const accessToken = localStorage.getItem('access_token');
  
  const response = await fetch('https://api.rcoinx.com/api/auth/webauthn/status', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data.data;
}

Registration Flow

Step 1: Begin Registration

Start the WebAuthn registration process to create a new credential. Endpoint: POST /api/auth/webauthn/registration/begin Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
  "code": 200,
  "message": "WebAuthn registration initiated",
  "data": {
    "challenge": "base64-encoded-challenge...",
    "rp": {
      "name": "RcoinX",
      "id": "rcoinx.com"
    },
    "user": {
      "id": "base64-user-id",
      "name": "johndoe",
      "displayName": "John Doe"
    },
    "pubKeyCredParams": [
      { "type": "public-key", "alg": -7 },
      { "type": "public-key", "alg": -257 }
    ],
    "timeout": 60000,
    "attestation": "none",
    "authenticatorSelection": {
      "authenticatorAttachment": "platform",
      "requireResidentKey": false,
      "userVerification": "preferred"
    }
  }
}

Step 2: Create Credential

Use the browser’s WebAuthn API to create a credential with the challenge from Step 1.
async function createWebAuthnCredential(publicKeyCredentialCreationOptions) {
  try {
    const credential = await navigator.credentials.create({
      publicKey: publicKeyCredentialCreationOptions
    });
    
    return credential;
  } catch (error) {
    console.error('Failed to create credential:', error);
    throw error;
  }
}

Step 3: Finish Registration

Complete the registration by sending the created credential to the server. Endpoint: POST /api/auth/webauthn/registration/finish Headers:
Authorization: Bearer <access_token>
Request:
{
  "id": "credential-id",
  "rawId": "base64-raw-id",
  "type": "public-key",
  "response": {
    "attestationObject": "base64-attestation-object",
    "clientDataJSON": "base64-client-data-json"
  }
}
Response: 200 OK
{
  "code": 200,
  "message": "WebAuthn registration completed successfully",
  "data": {
    "credential_id": "credential-id",
    "created_at": "2025-10-13T12:00:00Z"
  }
}

Complete Registration Example

async function registerWebAuthn() {
  const accessToken = localStorage.getItem('access_token');
  
  try {
    // Step 1: Begin registration
    const beginResponse = await fetch(
      'https://api.rcoinx.com/api/auth/webauthn/registration/begin',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    const beginData = await beginResponse.json();
    const publicKeyOptions = beginData.data;
    
    // Convert base64 strings to ArrayBuffers
    publicKeyOptions.challenge = base64ToArrayBuffer(publicKeyOptions.challenge);
    publicKeyOptions.user.id = base64ToArrayBuffer(publicKeyOptions.user.id);
    
    // Step 2: Create credential using browser API
    const credential = await navigator.credentials.create({
      publicKey: publicKeyOptions
    });
    
    if (!credential) {
      throw new Error('Failed to create credential');
    }
    
    // Step 3: Finish registration
    const finishResponse = await fetch(
      'https://api.rcoinx.com/api/auth/webauthn/registration/finish',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          id: credential.id,
          rawId: arrayBufferToBase64(credential.rawId),
          type: credential.type,
          response: {
            attestationObject: arrayBufferToBase64(
              credential.response.attestationObject
            ),
            clientDataJSON: arrayBufferToBase64(
              credential.response.clientDataJSON
            )
          }
        })
      }
    );
    
    const finishData = await finishResponse.json();
    
    if (!finishResponse.ok) {
      throw new Error(finishData.message);
    }
    
    console.log('WebAuthn registration successful');
    return finishData.data;
    
  } catch (error) {
    console.error('WebAuthn registration failed:', error);
    throw error;
  }
}

// Helper functions
function base64ToArrayBuffer(base64) {
  const binaryString = window.atob(base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
}

function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return window.btoa(binary);
}

Authentication Flow

Step 1: Begin Authentication

Start the WebAuthn authentication process. Endpoint: POST /api/auth/webauthn/authentication/begin Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
  "code": 200,
  "message": "WebAuthn authentication initiated",
  "data": {
    "challenge": "base64-encoded-challenge...",
    "timeout": 60000,
    "rpId": "rcoinx.com",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "base64-credential-id"
      }
    ],
    "userVerification": "preferred"
  }
}

Step 2: Get Assertion

Use the browser’s WebAuthn API to get an assertion (authenticate).
async function getWebAuthnAssertion(publicKeyCredentialRequestOptions) {
  try {
    const assertion = await navigator.credentials.get({
      publicKey: publicKeyCredentialRequestOptions
    });
    
    return assertion;
  } catch (error) {
    console.error('Failed to get assertion:', error);
    throw error;
  }
}

Step 3: Finish Authentication

Complete the authentication by sending the assertion to the server. Endpoint: POST /api/auth/webauthn/authentication/finish Headers:
Authorization: Bearer <access_token>
Request:
{
  "id": "credential-id",
  "rawId": "base64-raw-id",
  "type": "public-key",
  "response": {
    "authenticatorData": "base64-authenticator-data",
    "clientDataJSON": "base64-client-data-json",
    "signature": "base64-signature",
    "userHandle": "base64-user-handle"
  }
}
Response: 200 OK
{
  "code": 200,
  "message": "WebAuthn authentication successful",
  "data": {
    "verified": true,
    "credential_id": "credential-id",
    "authenticated_at": "2025-10-13T12:00:00Z"
  }
}

Complete Authentication Example

async function authenticateWebAuthn() {
  const accessToken = localStorage.getItem('access_token');
  
  try {
    // Step 1: Begin authentication
    const beginResponse = await fetch(
      'https://api.rcoinx.com/api/auth/webauthn/authentication/begin',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    const beginData = await beginResponse.json();
    const publicKeyOptions = beginData.data;
    
    // Convert base64 strings to ArrayBuffers
    publicKeyOptions.challenge = base64ToArrayBuffer(publicKeyOptions.challenge);
    publicKeyOptions.allowCredentials = publicKeyOptions.allowCredentials.map(
      cred => ({
        ...cred,
        id: base64ToArrayBuffer(cred.id)
      })
    );
    
    // Step 2: Get assertion using browser API
    const assertion = await navigator.credentials.get({
      publicKey: publicKeyOptions
    });
    
    if (!assertion) {
      throw new Error('Failed to get assertion');
    }
    
    // Step 3: Finish authentication
    const finishResponse = await fetch(
      'https://api.rcoinx.com/api/auth/webauthn/authentication/finish',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          id: assertion.id,
          rawId: arrayBufferToBase64(assertion.rawId),
          type: assertion.type,
          response: {
            authenticatorData: arrayBufferToBase64(
              assertion.response.authenticatorData
            ),
            clientDataJSON: arrayBufferToBase64(
              assertion.response.clientDataJSON
            ),
            signature: arrayBufferToBase64(assertion.response.signature),
            userHandle: assertion.response.userHandle 
              ? arrayBufferToBase64(assertion.response.userHandle)
              : null
          }
        })
      }
    );
    
    const finishData = await finishResponse.json();
    
    if (!finishResponse.ok) {
      throw new Error(finishData.message);
    }
    
    console.log('WebAuthn authentication successful');
    return finishData.data;
    
  } catch (error) {
    console.error('WebAuthn authentication failed:', error);
    throw error;
  }
}

Get Credentials

List all registered WebAuthn credentials for the current user. Endpoint: GET /api/auth/webauthn/credentials Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
  "code": 200,
  "message": "Credentials retrieved successfully",
  "data": [
    {
      "id": 1,
      "credential_id": "base64-credential-id",
      "name": "iPhone Face ID",
      "created_at": "2025-10-01T10:00:00Z",
      "last_used_at": "2025-10-13T12:00:00Z",
      "aaguid": "00000000-0000-0000-0000-000000000000"
    },
    {
      "id": 2,
      "credential_id": "base64-credential-id-2",
      "name": "YubiKey 5",
      "created_at": "2025-10-05T14:00:00Z",
      "last_used_at": "2025-10-12T09:30:00Z",
      "aaguid": "cb69481e-8ff7-4039-93ec-0a2729a154a8"
    }
  ]
}
Example:
async function getWebAuthnCredentials() {
  const accessToken = localStorage.getItem('access_token');
  
  const response = await fetch(
    'https://api.rcoinx.com/api/auth/webauthn/credentials',
    {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data.data;
}

Delete Credential

Remove a WebAuthn credential. Endpoint: DELETE /api/auth/webauthn/credentials Headers:
Authorization: Bearer <access_token>
Request:
{
  "credential_id": "base64-credential-id"
}
Response: 200 OK
{
  "code": 200,
  "message": "Credential deleted successfully"
}
Example:
async function deleteWebAuthnCredential(credentialId) {
  const accessToken = localStorage.getItem('access_token');
  
  const response = await fetch(
    'https://api.rcoinx.com/api/auth/webauthn/credentials',
    {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ credential_id: credentialId })
    }
  );
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data;
}

Browser Compatibility

Check if WebAuthn is supported in the user’s browser:
function isWebAuthnSupported() {
  return window.PublicKeyCredential !== undefined &&
         navigator.credentials !== undefined;
}

async function isWebAuthnAvailable() {
  if (!isWebAuthnSupported()) {
    return false;
  }
  
  // Check if platform authenticator is available
  if (window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
    return await window.PublicKeyCredential
      .isUserVerifyingPlatformAuthenticatorAvailable();
  }
  
  return true;
}

// Usage
if (await isWebAuthnAvailable()) {
  console.log('WebAuthn is available');
  showWebAuthnSetupButton();
} else {
  console.log('WebAuthn is not available');
  hideWebAuthnSetupButton();
}

Error Handling

Common Errors

async function handleWebAuthnError(error) {
  if (error.name === 'NotAllowedError') {
    // User cancelled or timeout
    return 'Authentication was cancelled or timed out';
  } else if (error.name === 'InvalidStateError') {
    // Credential already registered
    return 'This authenticator is already registered';
  } else if (error.name === 'NotSupportedError') {
    // Not supported
    return 'WebAuthn is not supported on this device';
  } else if (error.name === 'SecurityError') {
    // Security issue (wrong domain, insecure context)
    return 'Security error: Please use HTTPS';
  } else {
    return 'An unknown error occurred';
  }
}

// Usage
try {
  await registerWebAuthn();
} catch (error) {
  const message = await handleWebAuthnError(error);
  showError(message);
}

Best Practices

1. Require HTTPS

WebAuthn only works over HTTPS (except localhost for development):
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
  console.error('WebAuthn requires HTTPS');
  showError('WebAuthn is only available over HTTPS');
}

2. Provide Fallback

Always provide alternative authentication methods:
function AuthenticationOptions() {
  const [supportsWebAuthn, setSupportsWebAuthn] = useState(false);
  
  useEffect(() => {
    isWebAuthnAvailable().then(setSupportsWebAuthn);
  }, []);
  
  return (
    <div>
      {supportsWebAuthn && (
        <button onClick={authenticateWebAuthn}>
          Sign in with Passkey
        </button>
      )}
      
      <button onClick={authenticateWithPassword}>
        Sign in with Password
      </button>
    </div>
  );
}

3. Name Credentials

Let users give meaningful names to their credentials:
async function registerWebAuthnWithName(name) {
  const credential = await registerWebAuthn();
  
  // Update credential name
  await fetch('https://api.rcoinx.com/api/auth/webauthn/credentials/name', {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      credential_id: credential.credential_id,
      name: name
    })
  });
}

4. Handle Timeout

Set appropriate timeouts for user interaction:
const DEFAULT_TIMEOUT = 60000; // 60 seconds

// Show countdown during authentication
function WebAuthnAuth() {
  const [timeLeft, setTimeLeft] = useState(60);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setTimeLeft(prev => prev - 1);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <div>
      <p>Please authenticate with your device</p>
      <p>Time remaining: {timeLeft}s</p>
    </div>
  );
}