Api Key Exposure in Express with Oauth2
Api Key Exposure in Express with Oauth2 — how this specific combination creates or exposes the vulnerability
When an Express service uses OAuth 2.0 for authorization but also relies on API keys for routing, service-to-service calls, or feature gating, accidental exposure of those keys can bypass OAuth 2.0 protections. API keys are typically bearer secrets meant for machine-to-machine identification; if they appear in client-side code, logs, URLs, or error messages, an attacker who discovers the key can impersonate services or call privileged endpoints without user context.
In Express, common exposure paths include logging full request URLs that contain keys as query parameters (e.g., ?api_key=sk_live_xxx), returning configuration or debug payloads that include keys, or constructing OAuth 2.0 redirect URLs that embed keys as fragments or query params. Even when OAuth 2.0 access tokens are used in the Authorization header, a hardcoded or leaked API key in routes can allow unauthorized access to integrations that only check the key rather than validating the token scope and audience. This is especially risky when keys are used alongside OAuth 2.0 client credentials flows where both a client_id and a client_secret are required; if the client secret is exposed, an attacker can obtain access tokens and escalate impact.
OAuth 2.0 introduces additional risk vectors when keys are mishandled during authorization code or client credentials exchanges. For example, if an Express route intended for token exchange logs the full request body, a leaked client_secret can be harvested by automated scanning. Similarly, using API keys to authorize calls to downstream APIs that also accept OAuth 2.0 tokens can create inconsistent enforcement, where a key-only check grants access that should require a valid access token with proper scopes.
Real-world patterns observed in the wild include keys stored in environment variables but accidentally serialized into error stacks, or keys passed via URL fragments that end up in browser history and server logs. Without strict input validation, rate limiting on authentication endpoints, and secure handling of OAuth 2.0 parameters, an Express service can unintentionally present multiple paths to compromise.
Oauth2-Specific Remediation in Express — concrete code fixes
Remediation centers on strict separation of concerns: treat API keys as internal service identifiers and access tokens as user-bound credentials. Never expose secrets to clients, avoid logging sensitive values, and validate OAuth 2.0 tokens properly before allowing access.
1. Avoid exposing keys in URLs or logs
Ensure API keys and client secrets are never sent as query parameters or fragments. Instead, pass OAuth 2.0 parameters in the request body for token exchange and keep keys in server-side environment variables.
// ❌ Avoid: key in query string
// GET /api/resource?api_key=sk_live_xxx
// ✅ Use Authorization header for OAuth 2.0 access tokens
const passport = require('passport');
const BearerStrategy = require('passport-http-bearer').Strategy;
passport.use(new BearerStrategy(
function verifyToken(token, done) {
// Validate token with your OAuth 2.0 introspection or JWKS
validateAccessToken(token).then(user => {
return done(null, user);
}).catch(err => {
return done(null, false);
});
}
));
app.get('/api/me', passport.authenticate('bearer', { session: false }), (req, res) => {
res.json({ user: req.user });
});
2. Secure token exchange endpoint
When implementing the OAuth 2.0 authorization code flow, keep client secrets server-side and avoid logging request details.
const express = require('express');
const qs = require('querystring');
const axios = require('axios');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const CLIENT_ID = process.env.OAUTH_CLIENT_ID;
const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET;
const TOKEN_URL = 'https://auth.example.com/oauth/token';
app.post('/oauth/callback', async (req, res) => {
const { code } = req.body;
try {
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', process.env.OAUTH_REDIRECT_URI);
params.append('client_id', CLIENT_ID);
params.append('client_secret', CLIENT_SECRET);
// Send client secret in body, not URL
const tokenRes = await axios.post(TOKEN_URL, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
// Do not log token response
res.json({ access_token: tokenRes.data.access_token });
} catch (error) {
// Avoid leaking error details that may contain secrets
console.error('Token exchange failed');
res.status(400).json({ error: 'invalid_grant' });
}
});
3. Validate scopes and audience
After token validation, enforce scope and audience checks to prevent misuse if a token is leaked.
function validateScopes(tokenPayload, requiredScopes) {
const tokenScopes = (tokenPayload.scope || '').split(' ');
return requiredScopes.every(s => tokenScopes.includes(s));
}
app.get('/api/admin', async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'unauthorized' });
try {
const payload = await verifyJwt(token);
if (!validateScopes(payload, ['admin:read', 'admin:write'])) {
return res.status(403).json({ error: 'insufficient_scope' });
}
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'invalid_token' });
}
});
4. Rotate keys and monitor exposure
Treat leaked API keys as incidents: rotate keys immediately and review logs for unauthorized use. Combine with rate limiting on authentication-related routes to reduce brute-force risk.