Debugging Firebase Token 'aud' Mismatch in a Monorepo
How we traced and fixed Firebase Auth token audience mismatch errors in a Bun+React monorepo with A2A protocol

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 mismatchThe 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:
my-project-id (from UI's Firebase config)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-idOption 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-central1Verification
# Test token verification locally
curl -H "Authorization: Bearer $VALID_TOKEN" \
http://localhost:8080/api/stories
# Should return 200 with data, not 401In 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 onesLessons 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:
.env or config# .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 rootThe 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/_