npm package: @pixpilot/pkce-auth-chrome
Description: Chrome Extension PKCE authentication package with seamless OAuth 2.0 integration
Author: m.doaie
Latest version: 0.10.0

# @pixpilot/pkce-auth-chrome

PKCE OAuth 2.0 authentication for Chrome extensions with automatic state management.

## Installation

```bash
pnpm add @pixpilot/pkce-auth-chrome
```

## Quick Start

### Recommended: Class-Based API

The easiest way to use this library. Configure once and use for both steps:

```typescript
import { ChromePKCEAuthClient } from '@pixpilot/pkce-auth-chrome';

// Configure once with shared settings
const auth = new ChromePKCEAuthClient({
  loginUrl: 'https://oauth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  clientId: 'your_client_id',
  scope: 'read write',
});

// In background script: start flow
await auth.startAuthFlow();

// In callback.html: handle callback
const params = new URLSearchParams(window.location.search);
const tokens = await auth.handleAuthCallback({
  authCode: params.get('code')!,
  receivedState: params.get('state')!,
});
```

### Alternative: Individual Functions

Use individual functions if you need more control:

```typescript
import { handleAuthCallback, startAuthFlow } from '@pixpilot/pkce-auth-chrome';

// 1. Start auth flow (generates PKCE, saves state, opens auth tab)
await startAuthFlow({
  loginUrl: 'https://oauth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  clientId: 'your_client_id',
  scope: 'read write',
});

// 2. In callback.html, exchange code for tokens
const params = new URLSearchParams(window.location.search);
const tokens = await handleAuthCallback({
  authCode: params.get('code')!,
  receivedState: params.get('state')!,
  exchangeUrl: 'https://oauth.example.com/token',
});
```

### Complete Example

**manifest.json:**

```json
{
  "permissions": ["storage", "tabs"],
  "host_permissions": ["https://api.example.com/*"]
}
```

**background.js (start flow):**

```typescript
import { ChromePKCEAuthClient } from '@pixpilot/pkce-auth-chrome';

const auth = new ChromePKCEAuthClient({
  loginUrl: 'https://oauth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  clientId: 'your_client_id',
  scope: 'read write',
});

await auth.startAuthFlow();
```

**callback.html:**

```typescript
import { ChromePKCEAuthClient } from '@pixpilot/pkce-auth-chrome';

const auth = new ChromePKCEAuthClient({
  loginUrl: 'https://oauth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  clientId: 'your_client_id',
  scope: 'read write',
});

const params = new URLSearchParams(window.location.search);
if (params.get('code') && params.get('state')) {
  await auth.handleAuthCallback({
    authCode: params.get('code')!,
    receivedState: params.get('state')!,
  });
  window.close();
}
```

## API

### Auth Flow

- **`startAuthFlow(config)`** - Start OAuth flow
  - `config.allowHttpLocalhost?: boolean` - Allow HTTP localhost for development per RFC 8252 (default: `false`)
- **`handleAuthCallback(config)`** - Exchange code for tokens
  - `config.authCode: string` - The authorization code from the OAuth provider
  - `config.receivedState: string` - The state parameter from the OAuth provider
  - `config.exchangeUrl: string` - The OAuth token exchange endpoint URL
  - `config.clientId?: string` - Optional OAuth client ID for token exchange
  - `config.redirectUrl?: string` - Optional redirect URI for token exchange
  - `config.allowHttpLocalhost?: boolean` - Allow HTTP localhost for development per RFC 8252

### Token Management

- **`getTokenData()`** - Get tokens with metadata
- **`saveTokens(accessToken, refreshToken, expiresIn?, tokenType?)`** - Save tokens
- **`isTokenExpired()`** - Check if token is expired
- **`clearAuthData()`** - Clear all auth data

### Storage

- **`saveAuthState(state, codeVerifier)`** - Save auth state
- **`getAuthState(state)`** - Get auth state by state parameter
- **`markCodeAsUsed(code)`** - Mark authorization code as used
- **`isCodeUsed(code)`** - Check if code was used

### Tab Management

- **`openAuthTab(url)`** - Open URL in new tab

## URL Validation

This package uses **secure-by-default** URL validation to protect against common OAuth vulnerabilities.

### Security Defaults

**Default behavior** (secure by default):

- **validateLoginUrl** & **validateRedirectUrl**: HTTPS and chrome-extension:// only
- **validateExchangeUrl**: HTTPS only

💡 **RFC 8252 Compliance**: Set `allowHttpLocalhost: true` for development to enable OAuth 2.0 standard localhost support.

### Secure Defaults (Production-Ready)

```typescript
import { startAuthFlow } from '@pixpilot/pkce-auth-chrome';

// Default validators are secure (HTTPS-only)
await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize', // ✓ Secure
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  // allowHttpLocalhost: false (default) - Production-safe
});

// For local development, enable RFC 8252 localhost support
await startAuthFlow({
  loginUrl: 'http://localhost:3000/authorize', // ✓ Allowed per RFC 8252
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://oauth.example.com/token',
  allowHttpLocalhost: true, // OAuth 2.0 for Native Apps standard
});
```

✅ Allowed URLs:

- HTTPS URLs (production APIs)
- `chrome-extension://` URLs (your extension)
- `http://localhost` or `http://127.0.0.1` (local dev servers)

❌ Blocked URLs:

- Other HTTP URLs (security risk)

### Production Mode (Recommended)

Production extensions should use the secure defaults (no localhost allowed):

```typescript
import { startAuthFlow } from '@pixpilot/pkce-auth-chrome';

await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://auth.example.com/token',
  // allowHttpLocalhost: false (default) - Production-safe
});
```

Or use explicit production validators:

```typescript
import { getProductionValidator, startAuthFlow } from '@pixpilot/pkce-auth-chrome';

await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize',
  redirectUrl: chrome.runtime.getURL('callback.html'),
  exchangeUrl: 'https://auth.example.com/token',
  validateLoginUrl: getProductionValidator(), // No localhost allowed
  validateExchangeUrl: getProductionValidator(), // CRITICAL: Secure token exchange
});
```

Or use specific validators:

```typescript
import { isHttpsOrChromeExtensionUrl, isHttpsUrl } from '@pixpilot/pkce-auth-chrome';

await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize',
  validateLoginUrl: isHttpsOrChromeExtensionUrl, // HTTPS or chrome-extension://
  validateExchangeUrl: isHttpsUrl, // HTTPS only
});
```

### Available Validators

```typescript
import {
  getDefaultValidator,
  getProductionValidator,
  isChromeExtensionUrl,
  isHttpsOrChromeExtensionOrLocalhostUrl,
  isHttpsOrChromeExtensionUrl,
  isHttpsUrl,
} from '@pixpilot/pkce-auth-chrome';

// Recommended: Production-safe validator with auto-detection
const validator = getProductionValidator();

// Development: Default with production warnings
const devValidator = getDefaultValidator();

// Custom combinations
await startAuthFlow({
  loginUrl: 'https://auth.example.com',
  validateLoginUrl: isHttpsUrl, // HTTPS only
  validateRedirectUrl: isHttpsOrChromeExtensionUrl, // HTTPS or chrome-extension://
  validateExchangeUrl: isHttpsOrChromeExtensionOrLocalhostUrl, // Dev mode
});
```

### Custom Validators

```typescript
import type { UrlValidator } from '@pixpilot/pkce-auth-chrome';

// Your custom validation logic
const myValidator: UrlValidator = async (url: string) =>
  url.startsWith('https://trusted-domain.com');

await startAuthFlow({
  loginUrl: 'https://trusted-domain.com/auth',
  validateLoginUrl: myValidator,
});
```

### Security Best Practices

1. **Never use localhost validation in production builds**

   ```typescript
   // ❌ BAD: Allows HTTP interception in production
   await startAuthFlow({
     exchangeUrl: 'http://localhost:3000/token',
   });

   // ✅ GOOD: Use getProductionValidator() or explicit HTTPS
   await startAuthFlow({
     exchangeUrl: 'https://auth.example.com/token',
     validateExchangeUrl: getProductionValidator(),
   });
   ```

2. **Token exchange URL is most critical**
   - The `exchangeUrl` receives your authorization code
   - **Must** be HTTPS in production to prevent code interception
   - Consider using `isHttpsUrl` for strictest validation

3. **Use environment detection**

   ```typescript
   import { startAuthFlow } from '@pixpilot/pkce-auth-chrome';

   // Auto-detects production vs development
   await startAuthFlow({
     loginUrl: 'https://auth.example.com/authorize',
     redirectUrl: chrome.runtime.getURL('callback.html'),
     exchangeUrl: 'https://auth.example.com/token',
     allowHttpLocalhost: process.env.NODE_ENV !== 'production',
   });
   ```

## Security Features

- ✅ Secure URL validation (HTTPS, chrome-extension://, localhost in dev)
- ✅ PKCE with SHA-256 challenge
- ✅ CSRF protection with state parameter
- ✅ Authorization code replay prevention
- ✅ State expiration (10 minutes)
- ✅ Token expiration tracking
- ✅ Automatic cleanup of used codes
- ✅ Open redirect protection
- ✅ Credential leak prevention

## License

MIT