CRITICAL api key exposureoauth2

Api Key Exposure with Oauth2

How Api Key Exposure Manifests in Oauth2

API key exposure in OAuth2 implementations often occurs through misconfigured client secrets, improper token handling, and flawed redirect URI validation. The most common scenario involves developers hardcoding client secrets in frontend applications where they become trivially extractable by anyone inspecting the client-side code.

// INSECURE: Client secret exposed in frontend code
const config = {
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret', // NEVER do this
  redirectUri: 'https://yourapp.com/callback'
};

This pattern is particularly dangerous because OAuth2 client secrets should never be treated as secrets in public clients. The client secret exists to authenticate confidential clients (like servers) that can maintain secret storage. When exposed in single-page applications or mobile apps, attackers can immediately impersonate your application.

Another critical manifestation occurs in authorization code flow implementations where PKCE (Proof Key for Code Exchange) is omitted. Without PKCE, the authorization code can be intercepted and exchanged for tokens using the exposed client secret:

// Vulnerable authorization code exchange
async function exchangeCodeForToken(code) {
  const response = await fetch('https://oauth2.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ' + btoa(clientId + ':' + clientSecret) // Exposes secret
    },
    body: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}`
  });
  return response.json();
}

Redirect URI manipulation represents another OAuth2-specific exposure vector. If your OAuth2 provider doesn't validate redirect URIs strictly, attackers can register malicious URIs that capture authorization codes:

// Vulnerable redirect URI handling
app.get('/auth/callback', (req, res) => {
  const { code, state } = req.query;
  
  // DANGER: No validation of redirect_uri parameter
  const redirectUri = req.query.redirect_uri || config.redirectUri;
  
  // Attacker-controlled redirect_uri receives the token
  const tokenResponse = await exchangeCodeForToken(code, redirectUri);
  res.redirect(`${redirect_uri}?access_token=${tokenResponse.access_token}`);
});

State parameter manipulation during the OAuth2 dance enables CSRF attacks where an attacker's session receives tokens intended for the victim. This occurs when applications fail to validate the state parameter or use predictable values:

// Vulnerable state handling
app.get('/login', (req, res) => {
  const state = 'static-value'; // Predictable state
  const authUrl = `https://oauth2.example.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&state=${state}`;
  res.redirect(authUrl);
});

Oauth2-Specific Detection

Detecting API key exposure in OAuth2 implementations requires examining both the authorization flow and token exchange mechanisms. Start by analyzing the authorization request parameters for exposed secrets or predictable state values.

# Check for exposed client secrets in authorization URLs
curl -I "https://oauth2.example.com/authorize?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=..."

Effective detection must examine the complete OAuth2 flow, including authorization code exchange endpoints where client secrets are most vulnerable. Look for endpoints accepting client secrets via Basic authentication headers or form parameters.

# Test token endpoint for secret exposure
curl -X POST "https://oauth2.example.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"

middleBrick's OAuth2-specific scanning identifies these vulnerabilities through automated testing of the complete authorization flow. The scanner examines redirect URI handling, state parameter validation, and token exchange mechanisms without requiring credentials or setup.

Detection Target middleBrick Check Risk Level
Client secret in frontend code Static analysis of JavaScript files Critical
Missing PKCE in public clients Authorization flow analysis High
Redirect URI manipulation Parameter tampering tests High
State parameter predictability State value analysis Medium
Token endpoint exposure Authentication bypass attempts Critical

The scanner also validates compliance with OAuth2 security best practices, including proper PKCE implementation for public clients and secure state parameter generation using cryptographically random values.

Oauth2-Specific Remediation

Remediating OAuth2 API key exposure requires implementing proper client authentication patterns and secure flow implementations. For public clients (web and mobile applications), eliminate client secrets entirely and enforce PKCE for all authorization code exchanges.

// SECURE: PKCE implementation for public clients
import crypto from 'crypto';

function generatePKCEChallenge() {
  const codeVerifier = crypto.randomBytes(32).toString('base64url')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(///g, '_');
  
  const codeChallenge = crypto.createHash('sha256')
    .update(codeVerifier)
    .digest('base64url')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(///g, '_');
  
  return { codeVerifier, codeChallenge };
}

async function initiateOAuth2() {
  const { codeVerifier, codeChallenge } = generatePKCEChallenge();
  
  // Store codeVerifier securely for later use
  sessionStorage.setItem('codeVerifier', codeVerifier);
  
  const authUrl = `https://oauth2.example.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=read&code_challenge=${codeChallenge}&code_challenge_method=S256`;
  
  window.location.href = authUrl;
}

async function exchangeCodeForToken(code) {
  const codeVerifier = sessionStorage.getItem('codeVerifier');
  
  const tokenResponse = await fetch('https://oauth2.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&code_verifier=${codeVerifier}`
  });
  
  return tokenResponse.json();
}

For confidential clients (server-side applications), implement secure client secret storage using environment variables or secret management services rather than embedding secrets in code.

// SECURE: Confidential client with proper secret handling
require('dotenv').config();

const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;

async function clientCredentialsFlow() {
  const tokenResponse = await fetch('https://oauth2.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
    },
    body: 'grant_type=client_credentials&scope=read'
  });
  
  return tokenResponse.json();
}

// Example using AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

async function getOAuthCredentials() {
  const data = await secretsManager.getSecretValue({ SecretId: 'oauth2-credentials' }).promise();
  const credentials = JSON.parse(data.SecretString);
  return credentials;
}

async function secureClientCredentialsFlow() {
  const { clientId, clientSecret } = await getOAuthCredentials();
  
  const tokenResponse = await fetch('https://oauth2.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
    },
    body: 'grant_type=client_credentials&scope=read'
  });
  
  return tokenResponse.json();
}

Implement robust redirect URI validation to prevent open redirect attacks that could capture authorization codes. Maintain a whitelist of allowed redirect URIs and validate against it.

// SECURE: Redirect URI validation
const allowedRedirectUris = [
  'https://yourapp.com/callback',
  'https://yourapp.com/another-callback'
];

function validateRedirectUri(redirectUri) {
  if (!redirectUri) return false;
  
  const uri = new URL(redirectUri);
  const allowed = allowedRedirectUris.find(pattern => {
    try {
      const allowedUrl = new URL(pattern);
      return allowedUrl.origin === uri.origin && allowedUrl.pathname === uri.pathname;
    } catch (e) {
      return false;
    }
  });
  
  return !!allowed;
}

// Use in OAuth2 flow
app.get('/auth/callback', (req, res) => {
  const { code, state, redirect_uri } = req.query;
  
  if (!validateRedirectUri(redirect_uri)) {
    return res.status(400).json({ error: 'Invalid redirect URI' });
  }
  
  // Continue with token exchange
});

Generate cryptographically secure state parameters to prevent CSRF attacks during the OAuth2 authorization flow.

// SECURE: State parameter generation
import crypto from 'crypto';

function generateState() {
  return crypto.randomBytes(16).toString('hex');
}

function createStateStore() {
  const store = new Map();
  
  return {
    create: (state) => {
      const value = crypto.randomBytes(16).toString('hex');
      store.set(state, value);
      return value;
    },
    validate: (state, value) => {
      return store.get(state) === value;
    },
    cleanup: (state) => {
      store.delete(state);
    }
  };
}

const stateStore = createStateStore();

app.get('/login', (req, res) => {
  const state = generateState();
  const stateValue = stateStore.create(state);
  
  const authUrl = `https://oauth2.example.com/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=read&state=${state}&state_value=${stateValue}`;
  
  res.redirect(authUrl);
});

app.get('/auth/callback', (req, res) => {
  const { code, state, state_value } = req.query;
  
  if (!stateStore.validate(state, state_value)) {
    return res.status(400).json({ error: 'Invalid state parameter' });
  }
  
  stateStore.cleanup(state);
  
  // Continue with token exchange
});

Frequently Asked Questions

Why can't I use client secrets in single-page applications?

Client secrets cannot be securely stored in single-page applications because the code runs in the browser where anyone can view and extract the secret using browser developer tools. OAuth2 client secrets are designed for confidential clients that can maintain secret storage (like servers). Public clients should use PKCE instead, which provides equivalent security without requiring secrets.

What's the difference between authorization code flow with and without PKCE?

Authorization code flow without PKCE sends the authorization code to the token endpoint where it can be exchanged using only the client ID and secret. With PKCE, a code verifier generated by the client must also be presented, preventing attackers who intercept the authorization code from obtaining tokens. PKCE is mandatory for public clients and recommended for all OAuth2 implementations.