# Face ID / Biometric Login via WebAuthn

How to trigger native Face ID / Touch ID on iPhone from a web page using the Web Authentication API (WebAuthn).

## Key Insight

To trigger Face ID (not an external authenticator app), you MUST:
1. **Register** a `platform` credential first (on first visit)
2. **Subsequently request** that same credential with `allowCredentials`
3. Set `authenticatorSelection.authenticatorAttachment: 'platform'`
4. Set `userVerification: 'required'`

Omitting `allowCredentials` or using `authenticatorAttachment: 'cross-platform'` causes iOS to prompt for an external authenticator (YubiKey, etc.) instead of Face ID.

## Single Button Pattern (Recommended)

Trigger the entire flow from one button click. This avoids iOS restrictions on automatic WebAuthn calls and gives the user a clear action.

```javascript
(async function() {
  const btn = document.getElementById('login-btn');
  const STORAGE_KEY = 'demo-cred-id';

  function base64urlToBuffer(str) { /* ... */ }
  function bufferToBase64url(buffer) { /* ... */ }

  async function createCredential() {
    const challenge = new Uint8Array(32);
    crypto.getRandomValues(challenge);
    return await navigator.credentials.create({
      publicKey: {
        challenge,
        rp: { name: 'App Name', id: window.location.hostname },
        user: { id: new Uint8Array([1,2,3,4]), name: 'user@demo.local', displayName: 'User' },
        pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
        authenticatorSelection: { authenticatorAttachment: 'platform', userVerification: 'required' },
        timeout: 60000,
        attestation: 'none'
      }
    });
  }

  async function authenticate(storedId) {
    const challenge = new Uint8Array(32);
    crypto.getRandomValues(challenge);
    return await navigator.credentials.get({
      publicKey: {
        challenge,
        rpId: window.location.hostname,
        allowCredentials: [{ id: base64urlToBuffer(storedId), type: 'public-key' }],
        userVerification: 'required',
        timeout: 60000,
      }
    });
  }

  btn.addEventListener('click', async function() {
    try {
      if (!window.PublicKeyCredential) {
        window.location.href = 'next-page.html';
        return;
      }

      const storedId = localStorage.getItem(STORAGE_KEY);
      let result;

      if (storedId) {
        // Return visit: Face ID scan
        result = await authenticate(storedId);
      } else {
        // First visit: register Face ID, then scan
        result = await createCredential();
        if (result) {
          localStorage.setItem(STORAGE_KEY, bufferToBase64url(result.rawId));
        }
      }

      if (result) {
        window.location.href = 'next-page.html';
      }
    } catch (err) {
      if (err.name === 'NotAllowedError') return; // user cancelled
      window.location.href = 'next-page.html'; // fallback for demo
    }
  });
})();
```

### Why This Pattern Works

- WebAuthn on iOS **must** be initiated by a user gesture. Auto-calling `navigator.credentials.create()` on `DOMContentLoaded` often fails or is silently blocked.
- Combining register + authenticate behind one button means the first tap sets up Face ID and the second tap scans it.
- `allowCredentials` with the stored credential ID is what forces the native Face ID prompt instead of an authenticator app.

## Two-Step Pattern (Legacy)

If you register the credential on page load and authenticate on a later click, the first visit can fail because iOS rejects automatic WebAuthn calls. Prefer the single-button pattern above.

## Base64url Helpers

```javascript
function base64urlToBuffer(str) {
  const padding = '='.repeat((4 - str.length % 4) % 4);
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
  const binary = atob(base64);
  const buffer = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) buffer[i] = binary.charCodeAt(i);
  return buffer;
}

function bufferToBase64url(buffer) {
  const binary = String.fromCharCode(...new Uint8Array(buffer));
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
```

## Requirements

- HTTPS (Tailscale Funnel URL qualifies)
- iOS 14+ / Safari
- `apple-mobile-web-app-capable` meta tag for home-screen launch
- Page added to home screen for best experience

## Pitfalls

- **Asking for authenticator app**: You forgot `authenticatorAttachment: 'platform'` or omitted `allowCredentials`
- **Face ID never shows**: You called WebAuthn without a user gesture. Bind it to a button click.
- **"NotAllowedError" on first visit**: User cancelled the registration prompt — handle gracefully
- **No Face ID on Brave**: Safari works best; Brave may fall through to redirect
- **Credential lost on clear**: If user clears Safari data, re-registration is needed

## Home Screen Web App Meta Tags

```html
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
```

Add to home screen: Share → "Add to Home Screen". Launch from icon for no-URL-bar experience.

## PWA Navigation Note

When pages are added to the home screen, internal `<a href="...">` links sometimes open Safari instead of staying in the standalone app. Use a small JS helper to force `window.location.replace()` or `window.location.href` from a click handler, and add a `manifest.json` with `display: standalone` for the most reliable behavior.
