Back to Blog
medium SEVERITY9 min read

How OAuth token audience bypass happens in Node.js serverless functions and how to fix it

A critical OAuth authentication vulnerability in a Netlify serverless function allowed any valid Google OAuth token—even those issued to completely different applications—to authenticate successfully. The fix adds proper audience (aud) claim verification using Google's tokeninfo endpoint to ensure only tokens issued specifically for this application are accepted.

O
By Orbis AppSec
Published July 31, 2026Reviewed July 31, 2026

Answer Summary

OAuth token audience bypass (CWE-863) in Node.js occurs when OAuth validation checks token validity but not whether the token was issued for your application. In `parse-schedule.mjs`, the `verifyGoogleUser()` function called Google's userinfo endpoint but never verified the `aud` claim against `GOOGLE_CLIENT_ID`. The fix adds a call to Google's tokeninfo endpoint to validate that `aud` or `azp` matches the expected client ID before accepting the token.

Vulnerability at a Glance

cweCWE-863 (Incorrect Authorization)
fixAdded tokeninfo endpoint call to verify `aud`/`azp` matches `GOOGLE_CLIENT_ID`
riskAttackers with valid Google tokens from other apps can authenticate as legitimate users
languageJavaScript (Node.js)
root causeMissing audience claim validation in `verifyGoogleUser()` at line 113
vulnerabilityOAuth Token Audience Bypass

Introduction

In the web/netlify/functions/parse-schedule.mjs file of a production serverless application, we discovered a high-severity OAuth authentication bypass. The verifyGoogleUser() function at line 113 validated Google OAuth tokens by calling the userinfo endpoint, but it completely skipped a critical security check: verifying that the token was actually issued for this application.

This meant an attacker with any valid Google OAuth token—even one issued to a completely different application like a malicious mobile app or compromised third-party service—could authenticate to this endpoint and access protected functionality. The vulnerable code accepted the token as long as Google confirmed it was valid, without ever checking if it was meant for this application.

The Vulnerability Explained

Let's look at the original vulnerable code in verifyGoogleUser():

async function verifyGoogleUser(authHeader) {
  const token = (authHeader || "").replace(/^Bearer\s+/i, "");
  if (!token) return { ok: false, status: 401, msg: "Missing token." };

  const res = await fetch(GOOGLE_USERINFO, {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!res.ok) {
    return { ok: false, status: 401, msg: "Invalid or expired token." };
  }

  const profile = await res.json();
  const email = (profile.email || "").toLowerCase();

  if (!email || profile.email_verified === false) {
    return { ok: false, status: 401, msg: "Your Google sign-in doesn't have a verified email." };
  }

  // ... additional checks for allowed domains ...
}

The problem is on lines 133-136 (before the fix). The code calls Google's userinfo endpoint with the provided token and accepts it if:
1. Google says the token is valid (res.ok)
2. The profile has a verified email
3. The email domain is in the allowed list

But it never checks the aud (audience) claim.

How the Attack Works

Here's a concrete exploitation scenario for this specific vulnerability:

  1. Attacker obtains a legitimate Google OAuth token for a different application (perhaps a mobile app they control, or from a phishing attack targeting users of another service)

  2. Attacker sends a request to the parse-schedule endpoint:
    bash curl -X POST https://your-app.netlify.app/.netlify/functions/parse-schedule \ -H "Authorization: Bearer eyJhbGc...WRONG_APP_TOKEN" \ -d '{"schedule": "malicious data"}'

  3. The vulnerable code accepts it because:
    - Google's userinfo endpoint confirms the token is valid ✓
    - The token has a verified email ✓
    - The email domain might even be in ALLOWED_HDS if the attacker has a corporate account ✓
    - The token's aud claim points to a completely different application ✗ (but this was never checked!)

  4. Result: The attacker gains authenticated access to the parse-schedule function, potentially uploading malicious schedule data or accessing protected resources.

Real-World Impact

For the parse-schedule.mjs function specifically, this vulnerability could allow attackers to:

  • Bypass domain restrictions: Even with ALLOWED_HDS configured to restrict access to specific corporate domains, an attacker with a token from another app for that same domain could gain access
  • Impersonate legitimate users: The function would accept the attacker's token and associate actions with the email from the token
  • Access protected schedule data: Any functionality gated behind this authentication would be accessible

The severity is particularly high because OAuth tokens are relatively easy to obtain through phishing, compromised applications, or social engineering attacks targeting other services.

The Fix

The fix adds proper audience validation by calling Google's tokeninfo endpoint. Here's the code that was added at line 134:

// Verify the token audience matches this application's client ID to prevent
// tokens issued to other Google apps from being accepted here.
const expectedAud = process.env.GOOGLE_CLIENT_ID;
if (!expectedAud) {
  console.warn("parse-schedule: aud check inactive — GOOGLE_CLIENT_ID unset");
}
if (expectedAud) {
  try {
    const tiRes = await fetch(GOOGLE_TOKENINFO, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: `access_token=${encodeURIComponent(token)}`,
    });
    const tiData = tiRes.ok ? await tiRes.json() : null;
    const aud = tiData?.aud || tiData?.azp || "";
    if (aud !== expectedAud) {
      return { ok: false, status: 401, msg: "Token audience mismatch — sign in again." };
    }
  } catch {
    return { ok: false, status: 502, msg: "Couldn't verify your sign-in." };
  }
}

Before and After Comparison

Before (vulnerable):

const profile = await res.json();
const email = (profile.email || "").toLowerCase();

if (!email || profile.email_verified === false) {
  return { ok: false, status: 401, msg: "Your Google sign-in doesn't have a verified email." };
}
// ❌ No audience check - accepts tokens from any Google OAuth app!

const hd = (profile.hd || email.split("@")[1] || "").toLowerCase();

After (secure):

const profile = await res.json();
const email = (profile.email || "").toLowerCase();

if (!email || profile.email_verified === false) {
  return { ok: false, status: 401, msg: "Your Google sign-in doesn't have a verified email." };
}

// ✅ NEW: Verify token was issued for THIS application
const expectedAud = process.env.GOOGLE_CLIENT_ID;
if (expectedAud) {
  const tiRes = await fetch(GOOGLE_TOKENINFO, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: `access_token=${encodeURIComponent(token)}`,
  });
  const tiData = tiRes.ok ? await tiRes.json() : null;
  const aud = tiData?.aud || tiData?.azp || "";
  if (aud !== expectedAud) {
    return { ok: false, status: 401, msg: "Token audience mismatch — sign in again." };
  }
}

const hd = (profile.hd || email.split("@")[1] || "").toLowerCase();

How the Fix Works

The fix introduces several key security improvements:

  1. Calls Google's tokeninfo endpoint (line 19: const GOOGLE_TOKENINFO = "https://www.googleapis.com/oauth2/v3/tokeninfo";) - This endpoint returns detailed token metadata including the aud claim

  2. Retrieves the audience claim (line 147: const aud = tiData?.aud || tiData?.azp || "";) - Checks both aud and azp (authorized party) fields to handle different token types

  3. Validates against the expected client ID (line 148-150) - Compares the token's audience against process.env.GOOGLE_CLIENT_ID, which should be set to this application's OAuth client ID

  4. Rejects mismatched tokens - Returns a 401 error with a clear message if the audience doesn't match

  5. Graceful degradation (line 137-139) - If GOOGLE_CLIENT_ID isn't configured, logs a warning but doesn't break existing deployments

The fix is surgical—it adds exactly one security check at the precise point where it's needed, right after email verification but before any authorization decisions are made based on the token.

Prevention & Best Practices

To prevent OAuth token audience bypass vulnerabilities in your own applications:

1. Always Validate the Audience Claim

For Google OAuth specifically:

// ✅ GOOD: Verify aud matches your client ID
const tokenInfo = await fetch('https://www.googleapis.com/oauth2/v3/tokeninfo', {
  method: 'POST',
  body: `access_token=${token}`
});
const data = await tokenInfo.json();
if (data.aud !== process.env.GOOGLE_CLIENT_ID) {
  throw new Error('Token audience mismatch');
}
// ❌ BAD: Only checking if token is valid
const userInfo = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
  headers: { Authorization: `Bearer ${token}` }
});
if (userInfo.ok) {
  // Token is valid, but for which app?
}

2. Use JWT Libraries with Built-in Validation

For JWT-based OAuth tokens, use libraries that enforce audience validation:

// Using jsonwebtoken library
const jwt = require('jsonwebtoken');

jwt.verify(token, publicKey, {
  audience: process.env.GOOGLE_CLIENT_ID,  // ✅ Enforces aud check
  issuer: 'https://accounts.google.com'
}, (err, decoded) => {
  if (err) throw new Error('Invalid token');
  // Token is valid AND issued for this app
});

3. Follow OAuth 2.0 Security Best Practices

According to RFC 8252 (OAuth 2.0 for Native Apps):
- Always validate the aud claim matches your client ID
- Use the authorization code flow with PKCE for mobile/native apps
- Never accept tokens in URL fragments where they can leak

4. Implement Defense in Depth

Even with audience validation, add additional checks:

// Layer multiple validations
if (!tokenData.aud === expectedClientId) return reject();
if (!tokenData.email_verified) return reject();
if (tokenData.exp < Date.now() / 1000) return reject();
if (!ALLOWED_DOMAINS.includes(tokenData.hd)) return reject();

5. Use Static Analysis Tools

Configure tools like Semgrep to detect missing audience validation:

rules:
  - id: oauth-missing-audience-check
    pattern: |
      fetch($USERINFO, ...)
      ...
    pattern-not: |
      fetch($USERINFO, ...)
      ...
      $AUD === $CLIENT_ID

6. Test with Tokens from Other Applications

Include security tests that attempt authentication with valid OAuth tokens from different applications:

test('rejects valid Google token with wrong audience', async () => {
  const tokenFromDifferentApp = 'eyJhbGc...DIFFERENT_AUD';
  const result = await verifyGoogleUser(`Bearer ${tokenFromDifferentApp}`);
  expect(result.ok).toBe(false);
  expect(result.status).toBe(401);
});

Key Takeaways

  • The verifyGoogleUser() function in parse-schedule.mjs accepted any valid Google OAuth token, regardless of which application it was issued for, creating a critical authentication bypass
  • Calling the userinfo endpoint alone is insufficient—you must also verify the aud claim against your GOOGLE_CLIENT_ID using the tokeninfo endpoint
  • OAuth token audience validation is not optional—it's a fundamental security requirement explicitly mandated by OAuth 2.0 specifications (RFC 6749, Section 10.3)
  • The fix adds just 18 lines of code but closes a critical security gap that could have allowed complete authentication bypass
  • Environment variable checks matter—the fix gracefully handles missing GOOGLE_CLIENT_ID configuration with a warning rather than silently failing open

How Orbis AppSec Detected This

  • Source: HTTP Authorization header containing OAuth bearer token in parse-schedule.mjs serverless function
  • Sink: verifyGoogleUser() function at line 113 that authenticates requests based on Google OAuth tokens
  • Missing control: No validation that the token's aud (audience) claim matches this application's GOOGLE_CLIENT_ID, allowing tokens issued to any Google OAuth application to be accepted
  • CWE: CWE-863 (Incorrect Authorization)
  • Fix: Added call to Google's tokeninfo endpoint to retrieve and validate the aud/azp claim against process.env.GOOGLE_CLIENT_ID before accepting the token

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

OAuth token audience bypass is a subtle but critical vulnerability that stems from incomplete token validation. The fix in parse-schedule.mjs demonstrates that proper OAuth security requires more than just checking if a token is valid—you must also verify it was issued specifically for your application.

By adding the tokeninfo endpoint call and audience validation, this serverless function now properly enforces the OAuth security boundary. The fix is a perfect example of defense in depth: it doesn't replace existing checks like email verification and domain restrictions, but adds a critical missing layer that prevents an entire class of attacks.

When implementing OAuth authentication in your own applications, remember: validate everything—issuer, expiration, email verification, and audience. Each check defends against different attack vectors, and skipping any one of them can create a critical security gap.

References

Frequently Asked Questions

What is OAuth token audience bypass?

OAuth token audience bypass occurs when an application validates that an OAuth token is legitimate (issued by the correct provider) but fails to verify that the token was actually issued for that specific application. This allows attackers to use valid tokens from other applications to gain unauthorized access.

How do you prevent OAuth token audience bypass in Node.js?

Always verify the `aud` (audience) or `azp` (authorized party) claim in OAuth tokens matches your application's client ID. For Google OAuth, call the tokeninfo endpoint and validate the returned `aud` field against `process.env.GOOGLE_CLIENT_ID` before accepting the token.

What CWE is OAuth token audience bypass?

OAuth token audience bypass is classified as CWE-863 (Incorrect Authorization). It represents a failure to properly verify that an authentication credential is valid for the specific resource or application being accessed.

Is checking the token issuer enough to prevent OAuth token audience bypass?

No. Verifying the issuer (e.g., `accounts.google.com`) only confirms the token came from Google, not that it was issued for your application. An attacker can obtain a legitimate Google token for a different app and use it against yours if you don't validate the audience claim.

Can static analysis detect OAuth token audience bypass?

Yes. Advanced static analysis tools that understand OAuth flows can detect when token validation logic checks basic properties (validity, expiration, issuer) but omits audience verification. Pattern-based scanners look for userinfo/tokeninfo endpoint calls without corresponding `aud` claim checks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #167

Related Articles

critical

How session fixation happens in PHP OAuth login handlers and how to fix it

A session fixation vulnerability in `trunk/web/login_weibo.php` allowed attackers to hijack authenticated user sessions by pre-setting a victim's PHP session ID before they logged in via Weibo OAuth. The fix was a single, critical call to `session_regenerate_id(true)` inserted immediately before assigning the authenticated user's ID to the session. Left unpatched, this vulnerability could have given an attacker full access to any account whose Weibo OAuth login they could observe or influence.

critical

How Telegram OAuth hash validation bypass happens in PHP and how to fix it

A critical vulnerability in the Telegram OAuth provider allowed attackers to bypass authentication by submitting fabricated hash values without cryptographic validation. The fix replaces an unsafe string comparison with PHP's constant-time `hash_equals()` function, preventing timing attacks and ensuring all user data parameters are properly validated against the HMAC-SHA256 signature.

medium

Plaintext OAuth Token Storage: A Medium-Severity Vulnerability Fix

A medium-severity vulnerability was discovered in a Docker CLI authentication plugin where OAuth tokens and API keys were stored in plaintext on the local filesystem without any encryption. Despite having PBKDF2 cryptographic capabilities available in the project dependencies, the authentication store was writing sensitive credentials directly to disk, exposing them to potential theft by malicious actors with filesystem access.

high

Plaintext OAuth Token Storage: A Silent Security Risk in Your Application

A medium-severity vulnerability was discovered where OAuth tokens and API keys were stored in plaintext on the local filesystem without encryption. Despite having PBKDF2 cryptographic capabilities available in the application's dependencies, these sensitive credentials were written directly to disk, exposing users to potential credential theft and unauthorized account access.

high

OAuth Tokens Exposed: Why Plaintext Credential Storage Is a Critical Mistake

A medium-severity vulnerability was discovered where OAuth tokens and API keys were being stored in plaintext on the local filesystem without any encryption. Despite having PBKDF2 cryptographic capabilities available in the project dependencies, the authentication module was writing sensitive credentials directly to disk, leaving them vulnerable to unauthorized access. This fix addresses a common but dangerous security oversight that could compromise user accounts and API access.

medium

How supply chain trust policy bypass happens in Node.js pnpm workspaces and how to fix it

A missing `trustPolicy` configuration in `pnpm-workspace.yaml` left a Node.js library vulnerable to supply chain attacks where malicious package updates could downgrade security settings. Combined with insufficient input validation in the todo application component, this created a medium-severity attack surface. The fix adds `trustPolicy: no-downgrade`, `blockExoticSubdeps: true`, and `minimumReleaseAge: 10080` to harden the package manager configuration.