npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@pixpilot/pkce-auth

v0.10.0

Published

Core PKCE authentication package for OAuth 2.0 flows with TypeScript support

Readme

@pixpilot/pkce-auth

Secure OAuth 2.0 authentication with PKCE (Proof Key for Code Exchange).

⚠️ Security First: Before deploying to production, review the Security Checklist to ensure proper security configuration.

Installation

pnpm add @pixpilot/pkce-auth

Quick Start

Recommended: Class-Based API

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

import type { AuthStorageOperations } from '@pixpilot/pkce-auth';
import { PKCEAuthClient } from '@pixpilot/pkce-auth';

// 1. Implement storage operations (see "Storage Adapters" section below)
const storage: AuthStorageOperations = {
  getAuthState: async (state) => {
    /* retrieve state and verifier */
  },
  updateCodeHash: async (state, hash) => {
    /* bind code to state */
  },
  markCodeAsUsed: async (code) => {
    /* prevent code reuse - MUST be atomic! */
  },
  saveTokens: async (access, refresh) => {
    /* save tokens securely */
  },
  cleanupAuthState: async (state) => {
    /* cleanup state */
  },
};

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

// 3. Start auth flow
const { authUrl } = await auth.startAuthFlow();
window.location.href = authUrl;

// 4. After callback, exchange code for tokens
const tokens = await auth.handleAuthCallback({
  authCode: 'code_from_callback',
  receivedState: 'state_from_callback',
  storage,
});

Alternative: Individual Functions

Use individual functions if you need more control or have different configs per step:

import type { AuthStorageOperations } from '@pixpilot/pkce-auth';
import { handleAuthCallback, startAuthFlow } from '@pixpilot/pkce-auth';

// 1. Implement storage operations (see "Storage Adapters" section below)
const storage: AuthStorageOperations = {
  getAuthState: async (state) => {
    /* retrieve state and verifier */
  },
  updateCodeHash: async (state, hash) => {
    /* bind code to state */
  },
  markCodeAsUsed: async (code) => {
    /* prevent code reuse - MUST be atomic! */
  },
  saveTokens: async (access, refresh) => {
    /* save tokens securely */
  },
  cleanupAuthState: async (state) => {
    /* cleanup state */
  },
};

// 2. Start auth flow
const { authUrl, state } = await startAuthFlow({
  loginUrl: 'https://oauth.example.com/authorize',
  redirectUrl: 'https://yourapp.com/callback',
  exchangeUrl: 'https://oauth.example.com/token',
  clientId: 'your_client_id',
  scope: 'read write',
});

// 3. Redirect to authorization server
window.location.href = authUrl;

// 4. After callback, exchange code for tokens
const tokens = await handleAuthCallback({
  authCode: 'code_from_callback',
  receivedState: 'state_from_callback',
  exchangeUrl: 'https://oauth.example.com/token',
  storage,
});

Low-Level API (Manual Storage)

Use this for basic OAuth flows where you manage storage yourself:

import { exchangeAuthCode, generatePKCE } from '@pixpilot/pkce-auth';

// 1. Generate PKCE challenge
const { codeVerifier, codeChallenge } = await generatePKCE();

// 2. Build authorization URL
const params = new URLSearchParams({
  client_id: 'your_client_id',
  redirect_uri: 'https://yourapp.com/callback',
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
  response_type: 'code',
});
const authUrl = `https://oauth.example.com/authorize?${params}`;

// 3. Store code verifier and redirect
sessionStorage.setItem('code_verifier', codeVerifier);
window.location.href = authUrl;

// 4. After callback, exchange code for tokens
const tokens = await exchangeAuthCode('https://oauth.example.com/token', {
  code: authCode,
  codeVerifier: sessionStorage.getItem('code_verifier'),
  clientId: 'your_client_id',
  redirectUrl: 'https://yourapp.com/callback',
});

Development Mode

For local development with http://localhost, enable development mode:

import { PKCEAuthClient } from '@pixpilot/pkce-auth';

const auth = new PKCEAuthClient({
  loginUrl: 'http://localhost:3000/authorize',
  redirectUrl: 'http://localhost:3000/callback',
  exchangeUrl: 'http://localhost:3000/token',
  clientId: 'dev-client-id',
  allowHttpLocalhost: true, // Allows localhost per RFC 8252
});

// Console will show a development notice:
// ⚠️ HTTP localhost enabled for development (RFC 8252 compliant)

Important:

  • Production: Leave allowHttpLocalhost: false (default) for security
  • Development: Set allowHttpLocalhost: true to enable localhost per RFC 8252
  • Standards: OAuth 2.0 for Native Apps explicitly endorses localhost loopback

**Important:**

- **Production:** Never set `allowHttpLocalhost: true`
- **Default behavior:** Strict HTTPS-only validation (production-safe)
- **Development:** Explicitly set `allowHttpLocalhost: true` to allow localhost

## Storage Adapters

The high-level API uses platform-agnostic storage adapters. Implement these interfaces for your platform:

### Basic Example (Chrome Extension)

```typescript
import type {
  AuthStateStorageAdapter,
  CodeHashStorageAdapter,
  CodeStorageAdapter,
} from '@pixpilot/pkce-auth';

// For storing used authorization codes
const codeAdapter: CodeStorageAdapter = {
  get: async (key) => {
    const result = await chrome.storage.local.get(key);
    return result[key] ?? {};
  },
  set: async (key, value) => {
    await chrome.storage.local.set({ [key]: value });
  },
};

// For storing auth state (state parameter + code verifier)
const authStateAdapter: AuthStateStorageAdapter = {
  get: async (key) => {
    const result = await chrome.storage.local.get(key);
    return result[key] ?? null;
  },
  set: async (key, value) => {
    await chrome.storage.local.set({ [key]: value });
  },
  remove: async (key) => {
    await chrome.storage.local.remove(key);
  },
};

// For storing code hashes (prevents code substitution)
const codeHashAdapter: CodeHashStorageAdapter = {
  get: async (key) => {
    const result = await chrome.storage.local.get(key);
    return result[key] ?? null;
  },
  set: async (key, value) => {
    await chrome.storage.local.set({ [key]: value });
  },
};

Platform Examples

import AsyncStorage from '@react-native-async-storage/async-storage';

const codeAdapter: CodeStorageAdapter = {
  get: async (key) => {
    const json = await AsyncStorage.getItem(key);
    return json ? JSON.parse(json) : {};
  },
  set: async (key, value) => {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  },
};

const authStateAdapter: AuthStateStorageAdapter = {
  get: async (key) => {
    const json = await AsyncStorage.getItem(key);
    return json ? JSON.parse(json) : null;
  },
  set: async (key, value) => {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  },
  remove: async (key) => {
    await AsyncStorage.removeItem(key);
  },
};
import { readFile, unlink, writeFile } from 'node:fs/promises';

const codeAdapter: CodeStorageAdapter = {
  get: async (key) => {
    try {
      const json = await readFile(`./storage/${key}.json`, 'utf-8');
      return JSON.parse(json);
    } catch {
      return {};
    }
  },
  set: async (key, value) => {
    await writeFile(`./storage/${key}.json`, JSON.stringify(value));
  },
};

const authStateAdapter: AuthStateStorageAdapter = {
  get: async (key) => {
    try {
      const json = await readFile(`./storage/${key}.json`, 'utf-8');
      return JSON.parse(json);
    } catch {
      return null;
    }
  },
  set: async (key, value) => {
    await writeFile(`./storage/${key}.json`, JSON.stringify(value));
  },
  remove: async (key) => {
    try {
      await unlink(`./storage/${key}.json`);
    } catch {
      // File may not exist
    }
  },
};

Helper Functions

Use these to build your AuthStorageOperations:

import {
  getAuthState,
  markCodeAsUsed,
  saveAuthState,
  updateCodeHash,
} from '@pixpilot/pkce-auth';

// Create the storage operations object
const storage: AuthStorageOperations = {
  getAuthState: async (state) => getAuthState(state, authStateAdapter),
  updateCodeHash: async (state, hash) => updateCodeHash(state, hash, codeHashAdapter),
  markCodeAsUsed: async (code) => markCodeAsUsed(code, codeAdapter),
  saveTokens: async (access, refresh) => {
    // Your token storage logic
  },
  cleanupAuthState: async (state) => {
    await authStateAdapter.remove(state);
  },
};

URL Validation

By default, startAuthFlow validates URLs with isHttpsOrLocalhostUrl (HTTPS + localhost HTTP for development).

Built-in Validators

import { isHttpsOrLocalhostUrl, isHttpsUrl, startAuthFlow } from '@pixpilot/pkce-auth';

// Production: HTTPS only
await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize',
  validateLoginUrl: isHttpsUrl,
});

// Development: HTTPS or localhost HTTP
await startAuthFlow({
  loginUrl: 'http://localhost:3000/authorize',
  validateLoginUrl: isHttpsOrLocalhostUrl, // default
});

Custom Protocol Validators

For platform-specific protocols (Chrome extensions, mobile deep links):

import { createCustomProtocolValidator } from '@pixpilot/pkce-auth';

// Chrome extension
const isChromeExtension = createCustomProtocolValidator(
  'chrome-extension:',
  (hostname) => /^[\da-p]{32}$/u.test(hostname), // Validate extension ID
);

// Mobile deep link
const isMobileDeepLink = createCustomProtocolValidator('myapp:', (hostname) =>
  /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/iu.test(hostname),
);

await startAuthFlow({
  redirectUrl: 'chrome-extension://abcd.../callback',
  validateRedirectUrl: isChromeExtension,
});

Security Features:

  • Rejects URLs with embedded credentials (user:pass@host)
  • Prevents open redirect attacks (@ in hostname)
  • Ensures hostname is not empty
  • HTTPS validator rejects IP addresses (phishing protection)

Built-in Validators

isHttpsUrl(url)

Validates HTTPS URLs only. Use for production OAuth flows.

import { isHttpsUrl, startAuthFlow } from '@pixpilot/pkce-auth';

const result = await startAuthFlow({
  loginUrl: 'https://auth.example.com/authorize',
  validateLoginUrl: isHttpsUrl,
});

isHttpsOrLocalhostUrl(url)

Validates HTTPS or localhost HTTP URLs. Use for development.

import { isHttpsOrLocalhostUrl, startAuthFlow } from '@pixpilot/pkce-auth';

const result = await startAuthFlow({
  loginUrl: 'http://localhost:3000/authorize', // ✓ Allowed in dev
  validateLoginUrl: isHttpsOrLocalhostUrl,
});

Custom Protocol Validators

Create validators for platform-specific protocols:

import { createCustomProtocolValidator } from '@pixpilot/pkce-auth';

// Chrome extension validator
const isChromeExtension = createCustomProtocolValidator(
  'chrome-extension:',
  (hostname) => /^[\da-p]{32}$/u.test(hostname), // Validate extension ID
);

// Mobile deep link validator
const isMobileDeepLink = createCustomProtocolValidator('myapp:', (hostname) =>
  /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/iu.test(hostname),
);

// Use in auth flow
await startAuthFlow({
  redirectUrl: 'chrome-extension://abcd.../callback',
  validateRedirectUrl: isChromeExtension,
});

Security Checks

All validators include these security checks:

  • ✓ Rejects URLs with embedded credentials (user:pass@host)
  • ✓ Prevents open redirect attacks (@ in hostname)
  • ✓ Ensures hostname is not empty
  • ✓ HTTPS validator rejects IP addresses (phishing protection)

For Platform Packages

Platform-specific packages (Chrome, React Native, etc.) should compose these validators:

// In @pixpilot/pkce-auth-chrome
import {
  createCustomProtocolValidator,
  isHttpsOrLocalhostUrl,
  isHttpsUrl,
} from '@pixpilot/pkce-auth';

export const isChromeExtensionUrl = createCustomProtocolValidator(
  'chrome-extension:',
  (hostname) => /^[\da-p]{32}$/u.test(hostname),
);

export async function isHttpsOrChromeExtensionOrLocalhostUrl(
  url: string,
): Promise<boolean> {
  const parsed = new URL(url);
  if (parsed.protocol === 'https:') return isHttpsUrl(url);
  if (parsed.protocol === 'chrome-extension:') return await isChromeExtensionUrl(url);
  if (parsed.protocol === 'http:') return isHttpsOrLocalhostUrl(url);
  return false;
}

Security

Critical: Atomic Code Marking

The markCodeAsUsed function must be atomic to prevent authorization code reuse:

❌ Insecure (Race Condition):

// BAD: Check-then-set is NOT atomic
async function markCodeAsUsed(code: string): Promise<boolean> {
  const used = await db.get(`used_${code}`);
  if (used) return false;
  await db.set(`used_${code}`, true); // ← Race window!
  return true;
}

✅ Secure (Atomic Operation):

// SQL with unique constraint
async function markCodeAsUsed(code: string): Promise<boolean> {
  const result = await db.query(
    'INSERT INTO used_codes (code, timestamp) VALUES (?, ?) ON CONFLICT DO NOTHING',
    [code, Date.now()],
  );
  return result.rowsAffected > 0;
}

// Redis SET NX (set if not exists)
async function markCodeAsUsedRedis(code: string): Promise<boolean> {
  const EXPIRATION_SECONDS = 3600;
  const wasSet = await redis.set(`used_${code}`, '1', 'NX', 'EX', EXPIRATION_SECONDS);
  return wasSet === 'OK';
}

Error Handling

All authentication errors throw AuthError with detailed codes:

import { AuthError, handleAuthCallback } from '@pixpilot/pkce-auth';

try {
  await handleAuthCallback(config);
} catch (error) {
  if (error instanceof AuthError) {
    console.error(`Auth failed: ${error.code} (ID: ${error.id})`);

    switch (error.code) {
      case 'INVALID_STATE':
      case 'STATE_MISMATCH':
        // CSRF attack or expired session
        break;
      case 'CODE_REUSED':
        // Replay attack detected
        break;
      case 'CODE_BINDING_FAILED':
        // Code injection attack
        break;
    }
  }
}

Error Codes:

  • INVALID_STATE - State not found or expired
  • STATE_MISMATCH - State mismatch (CSRF protection)
  • CODE_REUSED - Code already used (replay attack)
  • CODE_BINDING_FAILED - Code hash mismatch (injection attack)
  • INVALID_VERIFIER - Custom verifier validation failed
  • INVALID_EXCHANGE_URL - Exchange URL failed validation

Best Practices

  • HTTPS Only: Use isHttpsUrl validator in production
  • Secure Storage: Store state and verifier in encrypted storage
  • State Expiration: Auth states automatically expire after 10 minutes
  • Input Validation: All inputs validated for max length (prevents DoS)
  • Constant-Time Comparison: Prevents timing attacks
  • Atomic Operations: Code marking uses triple verification with retry logic

API Reference

Core Functions

generatePKCE()

Generates PKCE code verifier and challenge.

Returns: { codeVerifier: string, codeChallenge: string }

exchangeAuthCode(exchangeUrl, request)

Exchanges authorization code for access tokens.

Parameters:

  • exchangeUrl - OAuth token endpoint URL
  • request.code - Authorization code from callback
  • request.codeVerifier - Code verifier from generatePKCE()
  • request.clientId - (optional) OAuth client ID
  • request.redirectUrl - (optional) Redirect URI

Returns: { access_token: string, refresh_token?: string | null, user?: object }

startAuthFlow(config)

Starts complete OAuth flow with PKCE.

Parameters:

  • config.loginUrl - Authorization server URL
  • config.redirectUrl - Your callback URL
  • config.exchangeUrl - Token exchange endpoint
  • config.clientId - OAuth client ID
  • config.scope - (optional) OAuth scopes
  • config.validateLoginUrl - (optional) Login URL validator function
  • config.validateRedirectUrl - (optional) Redirect URI validator

Returns: { authUrl: string, state: string, codeVerifier: string }

handleAuthCallback(config)

Handles OAuth callback and exchanges code for tokens.

Parameters:

  • config.authCode - Code from callback URL
  • config.receivedState - State from callback URL
  • config.exchangeUrl - Token exchange endpoint
  • config.storage - Storage operations implementation

Returns: { access_token: string, refresh_token?: string | null }

Storage Helpers

markCodeAsUsed(code, storage, storageKey?)

Atomically marks authorization code as used (prevents replay attacks).

Returns: Promise<boolean> - true if marked successfully

getAuthState(state, storage)

Retrieves auth state with expiration checking.

Returns: Promise<StoredAuthState | null>

saveAuthState(state, codeVerifier, storage)

Saves auth state with timestamp.

Returns: Promise<void>

updateCodeHash(state, codeHash, storage)

Atomically binds code hash to state (prevents code substitution).

Returns: Promise<string> - The bound hash

Validation

verifyPKCEChallenge(codeVerifier, codeChallenge)

Verifies PKCE challenge matches verifier (for OAuth servers).

Returns: Promise<boolean>

Constants

import { MAX_LENGTHS } from '@pixpilot/pkce-auth';

// Maximum allowed lengths for OAuth parameters
MAX_LENGTHS.STATE; // 256
MAX_LENGTHS.AUTH_CODE; // 512

Security Checklist for Implementers

Before deploying to production, ensure you've implemented these critical security measures:

Required Security Measures

  • [ ] Redirect URI Validation: Implement strict whitelist validation for redirect URIs

    import { startAuthFlow } from '@pixpilot/pkce-auth';
    
    const ALLOWED_REDIRECT_URIS = [
      'https://myapp.com/callback',
      'https://myapp.com/auth/callback',
    ];
    
    await startAuthFlow({
      loginUrl: 'https://auth.example.com/authorize',
      redirectUrl: 'https://myapp.com/callback',
      exchangeUrl: 'https://auth.example.com/token',
      validateRedirectUrl: (uri) => ALLOWED_REDIRECT_URIS.includes(uri),
    });
  • [ ] Token Storage: Use platform-specific encrypted storage

    • Web: Web Crypto API with IndexedDB encryption
    • Chrome Extensions: chrome.storage.local (automatically encrypted)
    • React Native: react-native-keychain or Expo SecureStore
    • iOS: iOS Keychain
    • Android: Android Keystore with EncryptedSharedPreferences
    • Node.js: Encrypted file storage or secure key management service
  • [ ] Rate Limiting: Implement rate limiting at the application level

    // Example: Limit auth attempts per IP/user
    // - startAuthFlow: Max 10 requests per minute
    // - handleAuthCallback: Max 5 requests per minute
    // - exchangeAuthCode: Max 3 requests per minute
  • [ ] HTTPS Enforcement: Strict HTTPS in production, development mode for localhost

    import { startAuthFlow } from '@pixpilot/pkce-auth';
    
    // Production: Default strict HTTPS validation (no allowHttpLocalhost flag)
    await startAuthFlow({
      loginUrl: 'https://auth.example.com/authorize',
      redirectUrl: 'https://myapp.com/callback',
      exchangeUrl: 'https://auth.example.com/token',
      // allowHttpLocalhost: false (default) - HTTPS only
    });
    
    // Development: Enable localhost support (RFC 8252 compliant)
    await startAuthFlow({
      loginUrl: 'http://localhost:3000/authorize',
      redirectUrl: 'https://myapp.com/callback',
      exchangeUrl: 'https://auth.example.com/token',
      allowHttpLocalhost: true, // RFC 8252 OAuth 2.0 for Native Apps
    });
  • [ ] Storage Backend: Ensure your storage adapter supports concurrent access if needed

    • For multi-instance deployments, use shared storage (Redis, database)
    • For single-instance apps, local storage is sufficient
    • Test concurrent callback handling scenarios

Additional Security Recommendations

  • [ ] URL Validation: Validate OAuth provider URLs against a whitelist

    const ALLOWED_AUTH_DOMAINS = ['auth.example.com'];
    
    (url) => {
      const parsed = new URL(url);
      return parsed.protocol === 'https:' && ALLOWED_AUTH_DOMAINS.includes(parsed.hostname);
    };
  • [ ] State Entropy: Use the library's built-in 256-bit random state (no action needed)

  • [ ] Code Verifier Security: Let the pkce-challenge library generate verifiers (no action needed)

  • [ ] Error Logging: Implement secure error logging that doesn't expose sensitive data

    // ✅ Good: Log error codes/IDs
    console.error(`Auth failed: ${error.code} (${error.id})`);
    
    // ❌ Bad: Don't log state, codes, or tokens
    console.error(`Failed with state: ${state}`); // DON'T DO THIS
  • [ ] Timeout Handling: Implement proper timeout handling for auth flows

    • Auth state expires after 10 minutes (built-in)
    • Used codes expire after 1 hour (built-in)
    • Implement UI timeouts for pending auth flows
  • [ ] Content Security Policy: Configure CSP headers for web applications

    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; connect-src https://auth.example.com"
    />
  • [ ] Insecure Localhost: Ensure allowHttpLocalhost is NOT enabled in production

    // ❌ Never do this in production
    startAuthFlow({
      // ...
      allowHttpLocalhost: true, // DANGEROUS IN PRODUCTION
    });
  • [ ] HTTP Localhost: Ensure allowHttpLocalhost is appropriate for your environment

    // ⚠️ Review localhost settings for production vs development
    startAuthFlow({
      // ...
      allowHttpLocalhost: true, // OK for development (RFC 8252)
    });
    
    // ✅ Production typically uses HTTPS endpoints
    startAuthFlow({
      // ...
      // allowHttpLocalhost defaults to false
    });

Testing Checklist

Before production deployment, test these scenarios:

  • [ ] Authorization code reuse is prevented
  • [ ] CSRF attacks are blocked (invalid state parameter)
  • [ ] Code injection attacks fail (code binding verification)
  • [ ] Expired states are rejected (10 minute expiration)
  • [ ] HTTP localhost is configured appropriately (allowHttpLocalhost setting matches environment)
  • [ ] Localhost HTTP URLs work in development, secure HTTPS in production
  • [ ] Expired codes are cleaned up (1 hour expiration)
  • [ ] Concurrent auth flows don't interfere with each other
  • [ ] Invalid redirect URIs are rejected
  • [ ] Non-HTTPS URLs are rejected in production mode

License

MIT