Back to blog
May 3, 2026

Debugging Firebase Token 'aud' Mismatch in a Monorepo

FirebaseAuthDebuggingMonorepo

How we traced and fixed Firebase Auth token audience mismatch errors in a Bun+React monorepo with A2A protocol

Debugging Firebase Token 'aud' Mismatch in a Monorepo

The Symptom

Everything worked perfectly in local development. The user signs in via Firebase Auth in the React UI, the token gets sent to the Express proxy, forwarded to the Agent service, verified by Firebase Admin — and everyone's happy.

Then we deployed to production.

FirebaseAuthError: auth/argument-error
Invalid token: audience mismatch

The UI could sign in users fine. But every API call to the Agent service returned 401. The same code, same Firebase project, different behavior.

The Architecture

First, let me show you the setup — a monorepo with two services:

User (Browser) → UI (React + Vite + Firebase Auth)
                   ↓ /api/* (Express proxy)
                 Agent (Bun + Firebase Admin SDK)

The UI uses firebase/auth to sign in users and get ID tokens. The Agent uses firebase-admin to verify those tokens via admin.auth().verifyIdToken().

The Investigation

Step 1: Check the Token

First, I decoded the JWT (without verification) to see what's inside:

# Get token from browser DevTools → Application → Local Storage → firebase:authUser:...
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .

The decoded token showed:

{
  "iss": "https://securetoken.google.com/my-project-id",
  "aud": "my-project-id",
  "sub": "xyz789...",
  "iat": 1715000000,
  "exp": 1715003600
}

Step 2: Check Firebase Admin Initialization

In the Agent service, the Firebase Admin SDK was initialized like this:

// agent/src/config/firebase.ts
import admin from "firebase-admin";

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  projectId: process.env.FIREBASE_PROJECT_ID,
});

Wait — applicationDefault() uses the service account from the environment. Let me check what project that service account belongs to:

# Decode the service account key
cat service-account-key.json | jq -r '.project_id'
# Output: my-other-project-id  ← DIFFERENT PROJECT!

Found it. The service account key was from my-other-project-id, but the tokens were issued by my-project-id.

The Root Cause

In Firebase, the aud (audience) claim in the JWT must match the project ID of the service account verifying the token. When you call verifyIdToken(), Firebase Admin checks:

1. Is the token signed by Google?

2. Does aud match the project ID of the verifying service account?

In our case:

  • Token `aud`: my-project-id (from UI's Firebase config)
  • Service account project: my-other-project-id (from Agent's service-account-key.json)
  • Mismatch → auth/argument-error.

    The Fix

    Fix 1: Align Project IDs

    Option A: Use the same Firebase project for both UI and Agent.

    # In agent/.env
    FIREBASE_PROJECT_ID=my-project-id  # Match the UI's project
    
    # And ensure the service account key is from the SAME project
    cat agent/service-account-key.json | jq -r '.project_id'
    # Should output: my-project-id

    Option B: Explicitly set the project ID in Admin SDK initialization:

    // agent/src/config/firebase.ts
    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.FIREBASE_PROJECT_ID!,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
        privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, "\n"),
      }),
    });

    Fix 2: Verify Token in Code

    // agent/src/middleware/auth.ts
    export function verifyFirebaseToken(
      req: AuthRequest,
      res: Response,
      next: NextFunction,
    ) {
      const authHeader = req.headers.authorization;
    
      if (!authHeader || !authHeader.startsWith("Bearer ")) {
        return res.status(401).json({ error: "Unauthorized: Missing token" });
      }
    
      const idToken = authHeader.split("Bearer ")[1];
    
      admin
        .auth()
        .verifyIdToken(idToken)
        .then((decodedToken) => {
          // Log token claims for debugging
          console.log("Token verified:", {
            uid: decodedToken.uid,
            aud: decodedToken.aud,
            iss: decodedToken.iss,
          });
          req.userId = decodedToken.uid;
          next();
        })
        .catch((error) => {
          console.error("Token verification failed:", {
            code: error.code,
            message: error.message,
            aud: error?.errorInfo?.message, // Sometimes contains the expected aud
          });
          return res.status(401).json({ error: "Unauthorized: Invalid token" });
        });
    }

    Fix 3: Update Cloud Run Environment Variables

    gcloud run services update fable-agent \
      --set-env-vars="FIREBASE_PROJECT_ID=my-project-id" \
      --region=us-central1

    Verification

    # Test token verification locally
    curl -H "Authorization: Bearer $VALID_TOKEN" \
      http://localhost:8080/api/stories
    
    # Should return 200 with data, not 401

    In production:

    # After deployment
    curl -H "Authorization: Bearer $PROD_TOKEN" \
      https://fable-agent-abc123-uc.a.run.app/api/stories/public
    
    # Should work for public endpoints, 401 for protected ones

    Lessons Learned

    1. Always Decode and Inspect the Token

    When debugging auth issues, the first step is always: decode the JWT and look at the claims.

    function debugToken(token: string) {
      const [header, payload, signature] = token.split('.');
      console.log("Header:", JSON.parse(atob(header)));
      console.log("Payload:", JSON.parse(atob(payload)));
      console.log("Signature length:", signature.length);
    }

    2. Service Account Project Must Match Token Issuer

    The aud claim in the Firebase ID token is the project ID that issued the token. The service account verifying it must belong to the same project.

    | Component | Check |

    | --------------------- | ------------------------------------------ |

    | UI Firebase config | projectId in firebase/config.ts |

    | Agent service account | project_id in service-account-key.json |

    | Agent env vars | FIREBASE_PROJECT_ID must match above |

    3. Log Token Verification Errors in Detail

    Don't just catch and return 401. Log the actual error:

    .catch((error) => {
      console.error("Auth error details:", {
        code: error.code,           // e.g., "auth/argument-error"
        message: error.message,      // e.g., "Invalid token: audience mismatch"
        stack: error.stack,
      });
    });

    4. Test Auth Flow End-to-End in Production

    Local development often uses emulators or different configs. Always test:

    1. Sign in on the deployed UI

    2. Get the token from DevTools

    3. Call the deployed Agent API directly

    4. Verify the token is forwarded correctly

    5. Use a Consistent Firebase Project Across Monorepo

    If you have a monorepo with multiple services accessing the same Firebase project:

  • Store the project ID in the root .env or config
  • Reference it from all services
  • Add a CI check that verifies all services use the same project
  • # .env (root)
    FIREBASE_PROJECT_ID=my-consistent-project-id
    
    # agent/.env
    FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID}  # Inherit from root
    
    # ui/.env
    VITE_FIREBASE_PROJECT_ID=${FIREBASE_PROJECT_ID}  # Inherit from root

    The Takeaway

    Firebase Auth token issues are rarely about the code — they're about configuration mismatches between services. When you see auth/argument-error, don't just retry. Decode the token, check the aud claim, and verify your service account belongs to the same project.

    Five minutes of investigation saves five hours of debugging.


    _Check more open source repos at: github.com/mnkrana/_