Sigma Auth
Migration Guides

Sigma Auth v2 introduces production-ready features including PKCE support, comprehensive rate limiting, backend authentication tokens, and enhanced error handling. This guide helps you upgrade from v1 while maintaining backward compatibility.

What's New in v2

🔐 PKCE Support

  • Proof Key for Code Exchange for enhanced security
  • Required for SPAs and mobile applications
  • Backward compatible with existing OAuth flows

⚡ Rate Limiting

  • Comprehensive protection against abuse
  • Smart retry logic with proper HTTP headers
  • Environment-specific limits for development vs production

🛡️ Backend Auth Tokens

  • Direct API authentication for backend services
  • Bitcoin-signed tokens without OAuth flow
  • Server-to-server communication support

🚨 Enhanced Error Handling

  • Detailed error responses with request IDs
  • OAuth 2.0 compliant error formats
  • Development-friendly debugging information

Breaking Changes (Minimal)

1. Error Response Format

Error responses now include additional fields for better debugging:

v1 Format:

{
  "error": "invalid_request",
  "error_description": "Missing client_id"
}

v2 Format:

{
  "error": "invalid_request", 
  "error_description": "Missing required parameter: client_id",
  "error_uri": "https://docs.sigmaidentity.com/docs/oauth/error-handling#invalid_request",
  "request_id": "req_abc123"
}

Migration: Update error handling to work with both formats:

// v2 compatible error handling
function handleAuthError(error) {
  const errorCode = error.error;
  const description = error.error_description;
  const requestId = error.request_id; // New in v2
  
  console.error(`Auth error [${errorCode}]:`, description);
  
  if (requestId) {
    console.log(`Request ID for support: ${requestId}`);
  }
  
  // Your existing error handling logic
  showUserError(description);
}

2. Rate Limit Headers

Response headers now include rate limit information:

// v2: Check rate limit headers
function checkRateLimit(response) {
  const remaining = response.headers.get('X-RateLimit-Remaining');
  const reset = response.headers.get('X-RateLimit-Reset');
  
  if (remaining && parseInt(remaining) < 10) {
    console.warn(`Rate limit low: ${remaining} requests remaining`);
  }
  
  return response;
}

// Apply to all auth requests
fetch('/api/auth/token', options)
  .then(checkRateLimit)
  .then(response => response.json());

New Features Implementation

PKCE significantly improves security with minimal code changes:

Before (v1 OAuth Flow):

function startLogin() {
  const state = generateState();
  sessionStorage.setItem('oauth_state', state);
  
  const params = new URLSearchParams({
    client_id: 'your-app',
    redirect_uri: 'https://yourapp.com/callback', 
    response_type: 'code',
    state: state
  });
  
  window.location.href = `https://auth.sigmaidentity.com/authorize?${params}`;
}

After (v2 with PKCE):

async function startLoginWithPKCE() {
  const state = generateState();
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  
  // Store for later use
  sessionStorage.setItem('oauth_state', state);
  sessionStorage.setItem('pkce_verifier', codeVerifier);
  
  const params = new URLSearchParams({
    client_id: 'your-app',
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code', 
    state: state,
    // PKCE parameters (new)
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });
  
  window.location.href = `https://auth.sigmaidentity.com/authorize?${params}`;
}

// Helper functions for PKCE
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64urlEncode(array);
}

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64urlEncode(digest);
}

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

Update Token Exchange:

// v1 token exchange
async function exchangeToken(code) {
  const response = await fetch('https://auth.sigmaidentity.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      client_id: 'your-app', 
      redirect_uri: 'https://yourapp.com/callback'
    })
  });
  
  return response.json();
}

// v2 token exchange with PKCE
async function exchangeTokenWithPKCE(code) {
  const codeVerifier = sessionStorage.getItem('pkce_verifier');
  
  const response = await fetch('https://auth.sigmaidentity.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      client_id: 'your-app',
      redirect_uri: 'https://yourapp.com/callback',
      // PKCE verification (new)
      code_verifier: codeVerifier
    })
  });
  
  // Clean up stored verifier
  sessionStorage.removeItem('pkce_verifier');
  
  return response.json();
}

2. Rate Limit Handling

Implement proper rate limit handling for a better user experience:

class RateLimitHandler {
  constructor() {
    this.retryQueue = new Map();
  }

  async makeRequest(url, options) {
    try {
      const response = await fetch(url, options);
      
      // Check rate limit headers
      const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '999');
      const reset = parseInt(response.headers.get('X-RateLimit-Reset') || '0');
      
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('X-RateLimit-Retry-After') || '60');
        throw new RateLimitError(retryAfter);
      }
      
      // Warn when rate limit is low
      if (remaining < 10) {
        console.warn(`Rate limit low: ${remaining} requests remaining until ${new Date(reset * 1000)}`);
      }
      
      return response;
    } catch (error) {
      if (error instanceof RateLimitError) {
        return this.handleRateLimit(url, options, error.retryAfter);
      }
      throw error;
    }
  }

  async handleRateLimit(url, options, retryAfter) {
    console.log(`Rate limited. Retrying after ${retryAfter} seconds...`);
    
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return this.makeRequest(url, options);
  }
}

class RateLimitError extends Error {
  constructor(retryAfter) {
    super(`Rate limited for ${retryAfter} seconds`);
    this.retryAfter = retryAfter;
  }
}

// Usage
const rateLimitHandler = new RateLimitHandler();

// Replace direct fetch calls
const response = await rateLimitHandler.makeRequest('/api/auth/token', {
  method: 'POST', 
  body: formData
});

3. Backend Authentication Tokens

For backend services, use the new direct token endpoint:

import { PrivateKey } from '@bsv/sdk';
import { SignedMessage } from 'bitcoin-auth';

class BackendAuthService {
  constructor(serviceWif) {
    this.privateKey = PrivateKey.fromWif(serviceWif);
    this.cachedToken = null;
    this.tokenExpiry = null;
  }

  async getToken(endpoint) {
    // Return cached token if valid
    if (this.cachedToken && this.tokenExpiry > Date.now() + 60000) {
      return this.cachedToken;
    }

    // Generate signature
    const timestamp = Date.now();
    const message = `${endpoint}:${timestamp}`;
    const signature = SignedMessage.sign(message, this.privateKey);
    
    // Request token
    const response = await fetch('https://auth.sigmaidentity.com/api/auth/token-for-endpoint', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        endpoint,
        signature,
        message, 
        pubkey: this.privateKey.toPublicKey().toString(),
        timestamp
      })
    });

    const { access_token, expires_in } = await response.json();
    
    // Cache token
    this.cachedToken = access_token;
    this.tokenExpiry = Date.now() + (expires_in * 1000) - 60000;
    
    return access_token;
  }

  async authenticatedRequest(url, options = {}) {
    const token = await this.getToken(url);
    
    return fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        ...options.headers
      }
    });
  }
}

// Usage
const backendAuth = new BackendAuthService(process.env.SERVICE_ACCOUNT_WIF);
const response = await backendAuth.authenticatedRequest('https://api.yourservice.com/data');

4. Enhanced Error Handling

Implement comprehensive error handling:

class AuthErrorHandler {
  handleError(error) {
    const errorInfo = {
      code: error.error,
      description: error.error_description,
      requestId: error.request_id,
      timestamp: new Date()
    };

    // Log with request ID for support
    console.error(`Auth Error [${errorInfo.code}]:`, errorInfo.description);
    if (errorInfo.requestId) {
      console.log(`Request ID: ${errorInfo.requestId}`);
    }

    // Handle specific error types
    switch (error.error) {
      case 'rate_limit_exceeded':
        const retryAfter = error.retry_after || 60;
        return this.handleRateLimit(retryAfter);
        
      case 'signature_verification_failed':
        return this.handleSignatureError();
        
      case 'invalid_grant':
        return this.handleExpiredCode();
        
      case 'server_error':
        return this.handleServerError(errorInfo);
        
      default:
        return this.handleGenericError(errorInfo);
    }
  }

  handleRateLimit(retryAfter) {
    return {
      type: 'rate_limit',
      message: `Please wait ${retryAfter} seconds before trying again.`,
      canRetry: true,
      retryAfter
    };
  }

  handleSignatureError() {
    return {
      type: 'signature_error',
      message: 'Bitcoin signature verification failed. Please check your backup file.',
      canRetry: true,
      action: 'reload_backup'
    };
  }

  handleExpiredCode() {
    return {
      type: 'expired_code', 
      message: 'Authentication session expired. Please sign in again.',
      canRetry: true,
      action: 'restart_auth'
    };
  }

  handleServerError(errorInfo) {
    return {
      type: 'server_error',
      message: 'Service temporarily unavailable. Please try again.',
      canRetry: true,
      supportInfo: {
        requestId: errorInfo.requestId,
        timestamp: errorInfo.timestamp
      }
    };
  }

  handleGenericError(errorInfo) {
    return {
      type: 'generic_error',
      message: errorInfo.description || 'An authentication error occurred.',
      canRetry: false,
      supportInfo: {
        requestId: errorInfo.requestId,
        code: errorInfo.code
      }
    };
  }
}

// Usage
const errorHandler = new AuthErrorHandler();

try {
  const result = await authenticateUser();
} catch (error) {
  const handledError = errorHandler.handleError(error);
  showErrorToUser(handledError);
}

Testing Your v2 Integration

1. Feature Detection

Test if v2 features are available:

async function detectV2Features() {
  try {
    // Test PKCE support
    const pkceResponse = await fetch('https://auth.sigmaidentity.com/authorize?code_challenge_method=S256&code_challenge=test');
    const supportsPKCE = !pkceResponse.url.includes('unsupported_response_type');
    
    // Test rate limit headers
    const rateLimitResponse = await fetch('https://auth.sigmaidentity.com/health');
    const supportsRateLimit = rateLimitResponse.headers.has('X-RateLimit-Limit');
    
    // Test backend tokens
    const backendTokenResponse = await fetch('https://auth.sigmaidentity.com/api/auth/token-for-endpoint', {
      method: 'POST'
    });
    const supportsBackendTokens = backendTokenResponse.status !== 404;
    
    return {
      pkce: supportsPKCE,
      rateLimit: supportsRateLimit,
      backendTokens: supportsBackendTokens
    };
  } catch (error) {
    console.warn('Feature detection failed:', error);
    return { pkce: false, rateLimit: false, backendTokens: false };
  }
}

// Use feature detection
const features = await detectV2Features();
if (features.pkce) {
  // Use PKCE flow
} else {
  // Fallback to basic OAuth
}

2. Compatibility Testing

Test both v1 and v2 flows:

describe('Sigma Auth v2 Compatibility', () => {
  it('should work with v1 OAuth flow', async () => {
    const code = await performBasicOAuthFlow();
    expect(code).toBeDefined();
  });

  it('should work with v2 PKCE flow', async () => {
    const code = await performPKCEOAuthFlow();
    expect(code).toBeDefined();
  });

  it('should handle rate limits gracefully', async () => {
    // Make multiple requests to trigger rate limit
    const responses = await Promise.all([
      ...Array(15).fill().map(() => fetch('/api/auth/token'))
    ]);
    
    const rateLimited = responses.some(r => r.status === 429);
    expect(rateLimited).toBe(true);
  });

  it('should include error request IDs', async () => {
    try {
      await fetch('/api/auth/token', { method: 'POST' }); // Invalid request
    } catch (error) {
      expect(error.request_id).toBeDefined();
    }
  });
});

Rollback Plan

If you encounter issues with v2, you can temporarily rollback:

1. Environment Variables

# Disable v2 features temporarily
DISABLE_PKCE=true
DISABLE_RATE_LIMITING=true
DISABLE_ENHANCED_ERRORS=true

2. Client-Side Fallback

// Graceful degradation to v1 behavior
function createAuthUrl(usePKCE = true) {
  const baseParams = {
    client_id: 'your-app',
    redirect_uri: 'https://yourapp.com/callback',
    response_type: 'code',
    state: generateState()
  };

  if (usePKCE) {
    try {
      const codeVerifier = generateCodeVerifier();
      const codeChallenge = await generateCodeChallenge(codeVerifier);
      
      sessionStorage.setItem('pkce_verifier', codeVerifier);
      
      return new URLSearchParams({
        ...baseParams,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256'
      });
    } catch (error) {
      console.warn('PKCE failed, falling back to basic OAuth:', error);
      // Fall through to basic OAuth
    }
  }

  return new URLSearchParams(baseParams);
}

Timeline and Support

Rollout Schedule

  • January 2024: v2 available in development
  • February 2024: v2 available in staging
  • March 2024: v2 promoted to production
  • June 2024: v1 features marked deprecated
  • December 2024: v1 support ends

Getting Help

Deprecation Notices

  • v1 OAuth flows: Continue to work but are deprecated
  • Basic error responses: Will include v2 fields by default
  • No rate limit handling: Applications may experience 429 errors

Best Practices for v2

1. Always Use PKCE

// Good: Always include PKCE
const authUrl = await createPKCEAuthUrl();

// Avoid: Plain OAuth (less secure)
const authUrl = createBasicAuthUrl();

2. Handle Rate Limits Gracefully

// Good: Respect rate limits
if (response.status === 429) {
  const retryAfter = response.headers.get('X-RateLimit-Retry-After');
  await sleep(retryAfter * 1000);
}

// Avoid: Ignoring rate limits
// This will lead to more errors and poor UX

3. Use Request IDs for Support

// Good: Log request IDs for debugging
if (error.request_id) {
  console.log(`Support request ID: ${error.request_id}`);
  errorReporting.setContext('request_id', error.request_id);
}

// Good: Include in user-facing error messages
showError(`Authentication failed. Reference: ${error.request_id}`);

4. Implement Proper Error Recovery

// Good: Smart error recovery
switch (error.error) {
  case 'invalid_grant':
    // Restart auth flow
    window.location.href = createAuthUrl();
    break;
  case 'rate_limit_exceeded':
    // Wait and retry
    await sleep(error.retry_after * 1000);
    return retryAuthentication();
}

Sigma Auth v2 provides significant security and reliability improvements while maintaining backward compatibility. The upgrade path is straightforward, and the new features will make your authentication more robust and user-friendly.