On this page

🔗The Authentication Problem in Generated Clients

When you generate API clients from OpenAPI specs, you get type-safe methods and request/response handling. But authentication? That's usually left as an exercise for the developer. Most generators give you a bare Authorization header and call it a day.

The problem is that modern APIs use OAuth2, and OAuth2 is complex. There are multiple flows (Authorization Code, Client Credentials, Password, Implicit), each with different security requirements. There's PKCE for public clients. There's token refresh. There's CSRF protection. There's secure token storage.

Getting all of this right manually is error-prone and time-consuming. Most developers end up with a half-implemented OAuth2 flow that works in development but fails in production, or worse, has security vulnerabilities.

MockForge v0.2.5 solves this by generating complete, production-ready OAuth2 implementations with all the security best practices built in. This post covers the technical implementation details of how it works.

🔗The OAuth2 Flow Landscape

OAuth2 has four standard flows, each designed for different scenarios:

graph TD
    A[OAuth2 Flow Selection] -->|Public Client Browser| B[Authorization Code + PKCE]
    A -->|Server-to-Server| C[Client Credentials]
    A -->|Trusted Client| D[Password]
    A -->|Legacy Browser| E[Implicit]
    
    B -->|Most Secure| F[PKCE Required]
    C -->|No User| G[Direct Token]
    D -->|Trusted Only| H[Username/Password]
    E -->|Deprecated| I[Direct Token]

Authorization Code with PKCE: The gold standard for browser-based applications. Uses PKCE (RFC 7636) to prevent authorization code interception attacks. Required for public clients.

Client Credentials: For server-to-server authentication. No user interaction, direct token exchange. Perfect for API-to-API communication.

Password: For trusted clients only. Username/password flow. Use sparingly, only when you control both client and server.

Implicit: Legacy flow, deprecated. Direct token response without code exchange. Still supported for backward compatibility.

MockForge generates support for all four flows, but defaults to Authorization Code with PKCE for browser applications—the most secure option.

🔗PKCE Implementation: RFC 7636 Compliance

PKCE (Proof Key for Code Exchange) is the security mechanism that makes OAuth2 safe for public clients. The problem it solves: without PKCE, an attacker who intercepts the authorization code can exchange it for an access token. PKCE prevents this by requiring a code verifier that only the client knows.

Here's how MockForge implements PKCE:

/**
 * Generate PKCE code verifier (RFC 7636)
 * Creates a cryptographically random string suitable for PKCE
 */
export function generatePKCECodeVerifier(): string {
  // Generate 43-128 character random string
  // Using Web Crypto API for cryptographically secure randomness
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  
  // Base64URL encode (RFC 4648 Section 5)
  return base64UrlEncode(array);
}

/**
 * Generate PKCE code challenge from verifier (RFC 7636)
 * Uses SHA256 hash for secure PKCE implementation
 */
export async function generatePKCECodeChallenge(
  verifier: string
): Promise<string> {
  if (!crypto?.subtle) {
    throw new Error('Web Crypto API not available for PKCE code challenge generation');
  }

  // SHA256 hash of verifier (RFC 7636 Section 4.2)
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  
  // Base64URL encode the hash
  const hashArray = new Array.from(new Uint8Array(hashBuffer));
  return base64UrlEncode(hashArray);
}

The implementation follows RFC 7636 exactly:

  1. Generate a cryptographically random code verifier (43-128 characters)
  2. Base64URL encode it (URL-safe, no padding)
  3. Generate SHA256 hash of the verifier
  4. Base64URL encode the hash to create the code challenge
  5. Send the challenge in the authorization request
  6. Send the verifier in the token exchange

This ensures that even if an attacker intercepts the authorization code, they can't exchange it without the verifier.

🔗Authorization Code Flow: The Complete Implementation

The Authorization Code flow with PKCE is the most complex. Here's how MockForge generates it:

class OAuth2TokenManager {
  async initiateAuthorizationCodeFlow(config: OAuth2Config): Promise<void> {
    // Generate PKCE values
    const codeVerifier = generatePKCECodeVerifier();
    const codeChallenge = await generatePKCECodeChallenge(codeVerifier);
    
    // Store verifier for later use
    sessionStorage.setItem('pkce_code_verifier', codeVerifier);
    
    // Generate state parameter for CSRF protection
    const state = this.generateState();
    sessionStorage.setItem('oauth2_state', state);
    
    // Build authorization URL
    const params = new URLSearchParams({
      client_id: config.clientId,
      redirect_uri: config.redirectUri,
      response_type: 'code',
      scope: config.scopes?.join(' ') || '',
      code_challenge: codeChallenge,
      code_challenge_method: 'S256', // SHA256
      state: state,
    });
    
    // Redirect to authorization server
    window.location.href = `${config.authorizationUrl}?${params.toString()}`;
  }
  
  async handleCallback(
    code: string,
    state: string,
    config: OAuth2Config
  ): Promise<TokenResponse> {
    // Verify state parameter (CSRF protection)
    const storedState = sessionStorage.getItem('oauth2_state');
    if (state !== storedState) {
      throw new Error('Invalid state parameter - possible CSRF attack');
    }
    
    // Retrieve PKCE verifier
    const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
    if (!codeVerifier) {
      throw new Error('PKCE code verifier not found');
    }
    
    // Exchange code for token
    const tokenResponse = await fetch(config.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: config.redirectUri,
        client_id: config.clientId,
        code_verifier: codeVerifier, // PKCE verifier
      }),
    });
    
    const tokens = await tokenResponse.json();
    
    // Store tokens securely
    await this.tokenStorage.setAccessToken(
      tokens.access_token,
      tokens.expires_in
    );
    if (tokens.refresh_token) {
      await this.tokenStorage.setRefreshToken(tokens.refresh_token);
    }
    
    // Clean up
    sessionStorage.removeItem('pkce_code_verifier');
    sessionStorage.removeItem('oauth2_state');
    
    return tokens;
  }
}

The implementation handles:

  • PKCE code verifier/challenge generation
  • State parameter for CSRF protection
  • Authorization URL construction
  • Callback handling with state verification
  • Token exchange with PKCE verifier
  • Secure token storage with expiration tracking
  • Automatic cleanup of temporary values

🔗Token Management: Refresh and Expiration

Generated clients handle token refresh automatically. The implementation tracks token expiration and refreshes before expiration:

class OAuth2TokenManager {
  async getAccessToken(config: OAuth2Config): Promise<string> {
    const stored = await this.tokenStorage.getAccessToken();
    
    if (!stored) {
      // No token, need to authenticate
      await this.initiateAuthorizationCodeFlow(config);
      throw new Error('Redirecting to authorization');
    }
    
    // Check if token is expired or expiring soon (within 60 seconds)
    const tokenData = JSON.parse(stored);
    const expiresAt = tokenData.expiresAt * 1000; // Convert to milliseconds
    const now = Date.now();
    
    if (now >= expiresAt - 60000) { // 60 second buffer
      // Token expired or expiring soon, refresh it
      const refreshToken = await this.tokenStorage.getRefreshToken();
      if (refreshToken) {
        try {
          const newTokens = await this.refreshAccessToken(
            refreshToken,
            config
          );
          return newTokens.access_token;
        } catch (error) {
          // Refresh failed, need to re-authenticate
          await this.initiateAuthorizationCodeFlow(config);
          throw error;
        }
      } else {
        // No refresh token, need to re-authenticate
        await this.initiateAuthorizationCodeFlow(config);
        throw new Error('No refresh token available');
      }
    }
    
    return tokenData.token;
  }
  
  async refreshAccessToken(
    refreshToken: string,
    config: OAuth2Config
  ): Promise<TokenResponse> {
    const response = await fetch(config.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: config.clientId,
      }),
    });
    
    if (!response.ok) {
      throw new Error(`Token refresh failed: ${response.statusText}`);
    }
    
    const tokens = await response.json();
    
    // Update stored tokens
    await this.tokenStorage.setAccessToken(
      tokens.access_token,
      tokens.expires_in
    );
    if (tokens.refresh_token) {
      await this.tokenStorage.setRefreshToken(tokens.refresh_token);
    }
    
    return tokens;
  }
}

The token manager proactively refreshes tokens before expiration (60-second buffer), preventing authentication failures mid-request. If refresh fails, it automatically initiates a new authorization flow.

🔗Security Considerations: Built-In Warnings

Generated clients include comprehensive security warnings and best practices:

/**
 * ⚠️ SECURITY WARNING: DO NOT use client secrets in browser-based applications!
 * 
 * Client secrets should only be used in server-side applications.
 * For browser apps, use Authorization Code flow with PKCE (recommended).
 * 
 * ⚠️ XSS Vulnerability Warning:
 * Tokens stored in localStorage are vulnerable to XSS attacks.
 * Consider using httpOnly cookies for sensitive tokens in production.
 * 
 * ✅ CSRF Protection:
 * State parameter validation is enabled by default for OAuth2 flows.
 * 
 * ✅ Token Expiration:
 * Tokens are automatically checked for expiration before use.
 */

These warnings are embedded directly in the generated code, making security considerations visible to developers. The generated README also includes detailed security documentation.

🔗Error Handling: Structured Error Classes

Generated clients include structured error handling for OAuth2 scenarios:

export class ApiError extends Error {
  constructor(
    public status: number,
    public statusText: string,
    public body: any
  ) {
    super(`API Error: ${status} ${statusText}`);
    this.name = 'ApiError';
  }
  
  isClientError(): boolean {
    return this.status >= 400 && this.status < 500;
  }
  
  isServerError(): boolean {
    return this.status >= 500 && this.status < 600;
  }
  
  getErrorDetails(): any {
    return {
      status: this.status,
      statusText: this.statusText,
      body: this.body,
    };
  }
}

export class RequiredError extends Error {
  constructor(public field: string, message?: string) {
    super(message || `Required parameter '${field}' was null or undefined`);
    this.name = 'RequiredError';
  }
  
  getVerboseMessage(): string {
    return `Missing required field: ${this.field}. Please provide a valid value.`;
  }
}

These error classes make it easy to handle different error scenarios gracefully, with clear distinctions between client errors (4xx) and server errors (5xx).

🔗Request/Response Validation

Generated clients include built-in validation that catches errors before sending requests:

class ApiClient {
  constructor(private config: ClientConfig) {}
  
  async createUser(data: CreateUserRequest): Promise<User> {
    // Validate required fields before sending
    if (this.config.validateRequests) {
      if (!data.email) {
        throw new RequiredError('email', 'Email is required for user creation');
      }
      if (!data.name) {
        throw new RequiredError('name', 'Name is required for user creation');
      }
    }
    
    // Make request
    const response = await fetch(`${this.config.baseURL}/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(await this.getAuthHeaders()),
      },
      body: JSON.stringify(data),
    });
    
    if (!response.ok) {
      throw new ApiError(
        response.status,
        response.statusText,
        await response.json()
      );
    }
    
    // Validate response structure
    if (this.config.validateRequests) {
      const user = await response.json();
      if (!user.id || !user.email) {
        throw new Error('Invalid response structure');
      }
    }
    
    return await response.json();
  }
}

Validation is configurable via the validateRequests flag. When enabled, it catches missing required fields and invalid response structures before they cause issues in production.

🔗Interceptors: Custom Request/Response Transformation

Generated clients support interceptors for custom request/response transformation:

interface Interceptors {
  request?: (config: RequestConfig) => Promise<RequestConfig>;
  response?: (response: Response) => Promise<Response>;
  error?: (error: Error) => Promise<Error>;
}

class ApiClient {
  constructor(
    private config: ClientConfig,
    private interceptors?: Interceptors
  ) {}
  
  async makeRequest(config: RequestConfig): Promise<Response> {
    // Request interceptor
    let requestConfig = config;
    if (this.interceptors?.request) {
      requestConfig = await this.interceptors.request(config);
    }
    
    try {
      const response = await fetch(requestConfig.url, {
        method: requestConfig.method,
        headers: requestConfig.headers,
        body: requestConfig.body,
      });
      
      // Response interceptor
      let processedResponse = response;
      if (this.interceptors?.response) {
        processedResponse = await this.interceptors.response(response);
      }
      
      return processedResponse;
    } catch (error) {
      // Error interceptor
      if (this.interceptors?.error) {
        throw await this.interceptors.error(error as Error);
      }
      throw error;
    }
  }
}

Interceptors enable global request/response transformation, logging, error handling, and custom business logic without modifying individual API calls.

🔗Multiple Authentication Methods

Generated clients support multiple authentication methods with a priority system:

interface AuthConfig {
  type: 'bearer' | 'apiKey' | 'basic' | 'oauth2';
  // OAuth2 takes priority if configured
  oauth2?: OAuth2Config;
  bearer?: string | (() => string | Promise<string>);
  apiKey?: { key: string; value: string | (() => string | Promise<string>) };
  basic?: { username: string; password: string };
}

async function getAuthHeaders(config: AuthConfig): Promise<Record<string, string>> {
  // OAuth2 takes priority
  if (config.oauth2) {
    const token = await oauth2Manager.getAccessToken(config.oauth2);
    return { Authorization: `Bearer ${token}` };
  }
  
  // Bearer token
  if (config.bearer) {
    const token = typeof config.bearer === 'function' 
      ? await config.bearer() 
      : config.bearer;
    return { Authorization: `Bearer ${token}` };
  }
  
  // API key
  if (config.apiKey) {
    const value = typeof config.apiKey.value === 'function'
      ? await config.apiKey.value()
      : config.apiKey.value;
    return { [config.apiKey.key]: value };
  }
  
  // Basic auth
  if (config.basic) {
    const credentials = btoa(`${config.basic.username}:${config.basic.password}`);
    return { Authorization: `Basic ${credentials}` };
  }
  
  return {};
}

This allows flexible authentication configuration while ensuring OAuth2 (the most secure) takes priority when configured.

🔗The Bottom Line

MockForge v0.2.5's client generation transforms OAuth2 from a manual, error-prone process into an automated, secure implementation. The generated clients include:

  • Complete OAuth2 flow support (all four standard flows)
  • RFC 7636 compliant PKCE implementation
  • Automatic token refresh and expiration management
  • CSRF protection via state parameters
  • Structured error handling
  • Request/response validation
  • Security warnings and best practices
  • Interceptors for custom logic

This is production-ready authentication code, generated automatically from your OpenAPI spec. No manual implementation, no security vulnerabilities, no half-finished OAuth2 flows. Just working, secure authentication out of the box.

For teams building API clients that need OAuth2, this feature eliminates weeks of implementation time and months of security debugging. That's the value of automated, production-ready code generation.