# FIDO2 Web SDK

> JavaScript SDK for WebAuthn-based biometric authentication

> **⚠️ Release candidate.** The current release is `1.0.0-rc.5`, published under the `rc`
> dist-tag. It is also `latest` for now, because npm always tags a package's first publish —
> so a bare install resolves to it today, but will jump to stable once `1.0.0` ships. Pin the
> version or use `@rc` if you want to stay on the release candidate.

[![npm version](https://img.shields.io/npm/v/@privateid/fido2-web-sdk.svg)](https://www.npmjs.com/package/@privateid/fido2-web-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- ✅ **Standalone or Backend Integration** - Works with or without backend
- ✅ **Multi-Authenticator Support** - UltraPass, YubiKey, or any FIDO2 device
- ✅ **Multi-Platform** - iOS, Android, and Windows desktop ([macOS and Linux are not supported yet](#platform-support))
- ✅ **Mobile Deeplink Flow** - Seamless two-phase authentication for iOS/Android
- ✅ **App Installation Detection** - Automatic fallback to App Store/Play Store
- ✅ **State Persistence** - Form values preserved across iOS redirects
- ✅ **Smart Credential Management** - Automatic storage and lookup by username
- ✅ **Debug Tools** - Built-in storage inspection and troubleshooting
- ✅ **TypeScript Support** - Full type definitions
- ✅ **Self-Contained Bundle** - ~76KB minified, CBOR decoder bundled in (no peer installs)

## Installation

```bash
npm install @privateid/fido2-web-sdk@rc
```

Or use via CDN (pin the version — an unpinned URL will move when the stable release ships):
```html
<script src="https://cdn.jsdelivr.net/npm/@privateid/fido2-web-sdk@1.0.0-rc.5/dist/fido2-web-sdk.min.js"></script>
```

The UMD build exposes the whole module namespace as `window.FIDO2SDK`, so destructure the
constructor out of it:

```js
const { FIDO2SDK, detectPlatform, isDeeplinkSupported } = window.FIDO2SDK;
const sdk = new FIDO2SDK({ apiKey: 'your-api-key' });
```

## Quick Start

```typescript
import FIDO2SDK from '@privateid/fido2-web-sdk';

// Initialize SDK (works standalone - no backend required)
const sdk = new FIDO2SDK({
  apiKey: 'your-api-key',
  policyId: 'mobile'
});

// Register - challenges and userIds auto-generated
const credential = await sdk.register({
  username: 'user@example.com',
  domain: 'example.com'
});

// Authenticate - with optional JWE token verification
const result = await sdk.authenticate({
  domain: 'example.com',
  customOptions: {
    checkLiveness: true,
    checkAge: true,
    ageThreshold: 18
  },
  verification: 'default'  // Verify JWE token with backend
});

console.log(result.verifiedData);
```

## API Reference

### SDK Constructor

```typescript
const sdk = new FIDO2SDK({
  apiKey: 'your-api-key',           // Required

  policyId: 'mobile',                // Optional (default: 'mobile') — must match your backend policy

  apiEndpoints: {                    // Optional (for JWE verification)
    verify: 'https://api.example.com/verify'
  },

  authenticators: {                  // Optional (for AAGUID verification)
    ultrapass: {
      aaguid: '00:76:63:1b:d4:a0:42:7f:57:73:0e:c7:1c:9e:02:79',
      name: 'UltraPass'
    }
  },

  timeout: 60000,                    // Optional (default: 60000ms)
  debug: false,                      // Optional (default: false)

  // NEW: App installation detection
  enableAppInstallCheck: true,       // Optional (default: false)
  appStoreUrls: {                    // Optional
    iOS: 'https://apps.apple.com/app/ultrapass/id6504606597',
    Android: 'https://play.google.com/store/apps/details?id=com.privateid.ultrapass'
  }
});
```

### register()

Register a new credential.

```typescript
await sdk.register({
  username: 'user@example.com',      // Required
  domain: 'example.com',             // Required
  displayName: 'John Doe',           // Optional
  authenticator: 'ultrapass',        // Optional (AAGUID verification)
  rpName: 'My App',                  // Optional
  challenge: 'base64String',         // Optional (auto-generated)
  userId: 'base64String',            // Optional (auto-generated)
  timeout: 120000                    // Optional (override SDK timeout)
});
```

### authenticate()

Authenticate with an existing credential.

```typescript
await sdk.authenticate({
  domain: 'example.com',             // Required

  customOptions: {                   // Optional
    checkLiveness: true,
    checkAge: true,
    checkAgeAboveThreshold: true,
    checkGeoLocation: true,
    ageThreshold: 18                 // 0-255
  },

  verification: 'default',           // 'none' | 'default' | {custom: {...}}
  challenge: 'base64String',         // Optional (auto-generated)
  allowCredentials: ['credId1'],     // Optional (base64 strings)
  timeout: 120000                    // Optional
});
```

### Verification Modes

**1. No Verification (Default)**
```typescript
const result = await sdk.authenticate({
  domain: 'example.com',
  verification: 'none'  // or omit
});
// Returns JWE token for manual verification
const jweToken = result.jweToken;
```

**2. Default Verification**
```typescript
const result = await sdk.authenticate({
  domain: 'example.com',
  verification: 'default'
});
// SDK automatically verifies and returns decoded data
console.log(result.verifiedData);
```

**3. Custom Verification**
```typescript
const result = await sdk.authenticate({
  domain: 'example.com',
  verification: {
    custom: {
      endpoint: 'https://api.yourcompany.com/verify',
      apiKey: 'your-api-key'
    }
  }
});
console.log(result.verifiedData);
```

## Mobile Integration (iOS & Android)

The SDK supports a two-phase mobile authentication flow using native apps:

### Quick Start

```typescript
import { detectPlatform, isDeeplinkSupported } from '@privateid/fido2-web-sdk';

// Check if mobile flow is available
const platform = detectPlatform();
const canUseMobile = isDeeplinkSupported(platform); // true on iOS/Android

// Register with mobile flow
if (canUseMobile) {
  await sdk.register({
    username: 'user@example.com',
    domain: 'example.com',
    useMobileFlow: true,
    returnURL: 'https://example.com/callback'
  });
}

// Handle callback after biometric capture
if (sdk.hasPendingOperation()) {
  const result = await sdk.handleDeeplinkCallback();
}
```

### How It Works

**Phase 1 (Deeplink):** Opens native app for biometric capture
**Phase 2 (WebAuthn):** Completes instantly on callback (biometric already done)

### Features

- ✅ Native biometric UX (Face ID, Touch ID, etc.)
- ✅ Seamless web-to-app handoff
- ✅ Custom options (liveness, age verification, geolocation)
- ✅ Unified API for iOS and Android
- ✅ **NEW:** App installation detection with App Store fallback
- ✅ **NEW:** Automatic form state persistence across iOS redirects
- ✅ **NEW:** Credential storage and lookup by username
- ✅ **NEW:** Built-in debugging tools

### Platform Support

| Platform | Status | Requirements |
|----------|--------|--------------|
| iOS 17.0+ | ✅ Supported | AASA file required ([Setup Guide](docs/IOS_SETUP.md)), [UltraPass for iOS](https://apps.apple.com/app/ultrapass/id6504606597) installed |
| Android 8.0+ | ✅ Supported | [UltraPass for Android](https://play.google.com/store/apps/details?id=com.privateid.ultrapass) installed |
| Windows 11 24H2+ | ✅ Supported | UltraPass authenticator (plugin or USB HID); direct WebAuthn flow, no deeplink |
| macOS | ❌ Not supported yet | No UltraPass desktop authenticator for macOS at this time |
| Linux | ❌ Not supported yet | No UltraPass desktop authenticator for Linux at this time |

> **⚠️ Desktop is Windows-only right now.** There is no UltraPass authenticator build for macOS
> or Linux, so desktop browsers on those platforms have no authenticator to talk to and the
> ceremony will fail. On macOS and Linux, use the iOS or Android deeplink flow from a mobile
> device instead. Support for these platforms is planned but not yet available.

📖 **Full Documentation:** [Mobile Integration Guide](docs/MOBILE_INTEGRATION.md)

🎯 **Live Demo:** [examples/html-basic/index.html](examples/html-basic/index.html) (mobile-first demo)

## Utility Functions

```typescript
import { detectPlatform, checkWebAuthnSupport } from '@privateid/fido2-web-sdk';

// Detect platform
const platform = detectPlatform(); // 'iOS' | 'Android' | 'Desktop'

// Check WebAuthn support
if (!checkWebAuthnSupport()) {
  console.error('WebAuthn not supported');
}
```

## Browser Support

- Chrome 67+
- Safari 13+
- Firefox 60+
- Edge 79+
- iOS Safari 13+
- Chrome Android 67+

## Examples

Working examples are available in the `/examples` directory:

- **HTML Basic** (`examples/html-basic/`) - ⭐ **PRIMARY MOBILE DEMO** - Pure HTML/JS
  - `index.html` - Mobile authentication (iOS/Android with deeplinks)
  - `desktop.html` - Desktop/standalone authentication
  - Features: App detection, form persistence, credential management, debug tools

- **React** (`examples/react/`) - React 19 + TypeScript + Vite (with mobile support)
- **Next.js** (`examples/nextjs/`) - Next.js 15 App Router (with mobile support)

To run examples:
```bash
npm install
npm run build
python3 -m http.server 8080
# Open http://localhost:8080/examples/html-basic/
```

## Enterprise Integration

### PingFederate SSO Adapter

For enterprise single sign-on with PingFederate, we provide a production-ready IdP adapter:

👉 **[UltraPass PingFederate Adapter](https://github.com/openinfer/ultrapass-pf-adapter)**

**Features:**
- ✅ WebAuthn/FIDO2 biometric authentication
- ✅ Full PingFederate 13.0+ support
- ✅ Redirect/resume pattern with session management
- ✅ Production-ready deployment scripts
- ✅ Complete documentation & examples

The adapter seamlessly integrates this SDK with PingFederate for passwordless enterprise authentication.

## License

MIT © PrivateID

## Debugging

### Common Issues

**"No credential found for username"**
- Credentials are stored by username (case-sensitive)
- Click "🔍 Debug Storage" button in html-basic demo to see stored usernames
- Check browser console: `JSON.parse(localStorage.getItem('fido2_credentials'))`

**Form cleared after iOS redirect**
- This is expected (page reloads after redirect)
- Form state is automatically restored - wait for page load

**"App may not be installed"**
- UltraPass app not detected
- Click App Store/Play Store link to install
- Enable `enableAppInstallCheck: true` for automatic detection

### Debug Mode

Enable detailed logging:
```javascript
const sdk = new FIDO2SDK({
  apiKey: 'your-key',
  debug: true  // Shows detailed logs in console
});
```

### Manual Storage Inspection

```javascript
// View stored credentials
console.log(JSON.parse(localStorage.getItem('fido2_credentials')));

// View form state
console.log(JSON.parse(localStorage.getItem('fido2_form_state')));

// Clear and start fresh
localStorage.clear();
sessionStorage.clear();
location.reload();
```

## Support

- [GitHub Issues](https://github.com/openinfer/fido2-js/issues)
- Email: support@privateid.com
