Anatomy of a Firebase ID Token
A Firebase Auth ID token is a JSON Web Token signed with RS256. Google holds the private key; the corresponding public keys are published at a well-known URL that rotates every few weeks. The token proves "Firebase Auth issued this session for this uid" and carries all the profile info your backend needs.
Header (typical)
{
"alg": "RS256",
"kid": "a1b2c3d4e5f6...", // matches an entry in Google's JWKS
"typ": "JWT"
}Payload (fully populated)
{
"iss": "https://securetoken.google.com/YOUR-PROJECT-ID",
"aud": "YOUR-PROJECT-ID",
"auth_time": 1727654321,
"user_id": "abcXYZ1234567890",
"sub": "abcXYZ1234567890", // same as user_id / uid
"iat": 1727654400,
"exp": 1727658000, // iat + 3600 (always 1 hour)
"email": "[email protected]",
"email_verified": true,
"name": "Jane Doe",
"picture": "https://lh3.googleusercontent.com/...",
"firebase": {
"identities": {
"google.com": ["1234567890"],
"email": ["[email protected]"]
},
"sign_in_provider": "google.com",
"sign_in_second_factor": "phone", // only if MFA
"tenant": "tenant-abc" // only in multi-tenant
},
"admin": true, // custom claim you set
"premium": "gold" // custom claim you set
}Firebase-Specific Claim Reference
iss— alwayshttps://securetoken.google.com/YOUR-PROJECT-ID. Confirms token is from Firebase Auth.aud— your Firebase project ID. Confirms the token was minted for your project (not stolen from another Firebase app).sub/user_id— the Firebase UID. Same asauth().currentUser.uidon the client.iat— issued-at Unix timestamp. Combined withauth_time, tells you how recently the user actually authenticated.exp— expiration. Always exactlyiat + 3600. Firebase does not issue longer-lived ID tokens.auth_time— Unix timestamp of the user's last sign-in event. Useful for "require recent auth" on sensitive actions.firebase.identities— map of every provider the user has linked (google.com, facebook.com, phone, email, apple.com, github.com, etc.).firebase.sign_in_provider— which provider issued THIS session (may be different from linked identities if the user has multiple providers).firebase.sign_in_second_factor— set tophoneor another MFA method if the sign-in involved second-factor.- Custom claims — any top-level claim you set with
admin.auth().setCustomUserClaims(uid, { admin: true })appears in every future ID token for that user.
Backend Verification — Complete Examples
Node.js — Firebase Admin SDK
import admin from 'firebase-admin';
admin.initializeApp({
credential: admin.credential.cert(require('./service-account.json')),
});
app.use(async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).end();
try {
const decoded = await admin.auth().verifyIdToken(token);
req.uid = decoded.uid;
req.email = decoded.email;
req.admin = decoded.admin === true; // custom claim
next();
} catch (err) {
if (err.code === 'auth/id-token-expired') {
return res.status(401).json({ error: 'expired' });
}
return res.status(401).json({ error: 'invalid token' });
}
});Python — firebase-admin
import firebase_admin
from firebase_admin import auth, credentials
cred = credentials.Certificate('./service-account.json')
firebase_admin.initialize_app(cred)
def verify_firebase_token(id_token: str):
try:
decoded = auth.verify_id_token(id_token)
return decoded # dict with uid, email, custom claims
except auth.ExpiredIdTokenError:
raise HTTPException(401, "token expired")
except auth.InvalidIdTokenError:
raise HTTPException(401, "invalid token")Manual verification without the Admin SDK (e.g. Go, PHP)
// Steps every Firebase verification must do:
// 1. Fetch https://www.googleapis.com/robot/v1/metadata/x509/[email protected]
// (Cache-Control tells you how long to cache — usually 6 hours)
// 2. Read the JWT header, find the "kid" — look up matching x509 cert in step 1
// 3. Extract RSA public key from the x509 cert
// 4. Verify RS256 signature over segments[0] + "." + segments[1]
// 5. Confirm:
// - alg == "RS256"
// - iss == "https://securetoken.google.com/<YOUR_PROJECT_ID>"
// - aud == "<YOUR_PROJECT_ID>"
// - exp > now (with optional small clock skew)
// - iat <= now
// - auth_time <= now
// - sub is a non-empty string
// 6. Only after ALL pass, trust the payload
// Use a proper JWT library (jose, PyJWT, jsonwebtoken) — never verify by hand.Firebase Custom Tokens vs ID Tokens
They're easy to confuse. Custom tokens flow server → client; ID tokens flow client → your backend.
- Custom token: You generate on your server with
admin.auth().createCustomToken(uid, claims), send to the client, client callssignInWithCustomToken(customToken). Signed with RS256 using your service account. Header includesalg: RS256, typ: JWT. - ID token: Firebase Auth issues after any sign-in (custom, email, Google, etc.). Client sends to your backend as a Bearer token. Signed by Google with RS256 using
[email protected].
Check the iss to tell them apart: custom tokens have your service account email as issuer, ID tokens have https://securetoken.google.com/PROJECT.
Common Firebase JWT Issues
- "auth/id-token-expired" — the token is over 1 hour old. Force-refresh on the client:
getIdToken(true). - "auth/id-token-revoked" — user was signed out server-side via
revokeRefreshTokens. Client must re-authenticate. - "auth/argument-error: Firebase ID token has incorrect "aud" claim" — you initialised Admin SDK with the wrong project's service account, or the token is from a different Firebase project.
- Custom claims not appearing — you set them, but the client is holding an ID token issued before you set them. Force
getIdToken(true)or wait for the natural 1-hour refresh. - Cross-clock-skew failures — device clock is off by more than a few minutes; allow a small tolerance in your verification or fix the device time.
Related JWT Tools
- AWS Cognito JWT Decoder — the AWS equivalent
- JWT RS256 Decoder — the algorithm Firebase uses
- JWT Expiration Checker — quickly check if a Firebase token is still valid
- Verify JWT Signature — full signature verification with JWKS
- JWT Refresh Token Example — pattern for non-Firebase auth