Chapter 14 · IAM, Admin/Server SDKs, Service Accounts, Authentication, and Trust Boundaries
Custom Claims, Tenant / Role Data, Token Refresh, Revocation, and Avoiding Authorization Only in UI
Use custom claims as bounded-staleness access assertions, observe token refresh, model revocation, and keep rapidly changing authorization out of stale UI-only state.
1. Claims are cached authorization assertions, not a live permissions table
Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.
AtlasMart wants support agents to see a support console and call
privileged backend actions. A custom claim such as
role: "support" is useful because it is
cryptographically carried in the Firebase ID token and can be
read by Security Rules or a verified backend token. But changing
the user record does not mutate already-issued ID tokens. Role
changes therefore have a propagation lifecycle, and revocation
has separate semantics.
AtlasMart continues the same mandatory environment used in
Chapters 01–13: project ID
demo-atlasmart-firestore, Standard edition /
Native mode / (default) database, Firestore
emulator 127.0.0.1:8080, Authentication emulator
127.0.0.1:9099, Emulator UI
127.0.0.1:4000, Firebase CLI
15.30.0, Firebase JavaScript SDK
12.19.0, Firebase Admin Node.js SDK
14.4.0 carrying
@google-cloud/firestore 9.1.0,
@firebase/rules-unit-testing 5.0.2, and Node.js
22+. Mandatory work remains local/no-cost. Standard Native
Core is the canonical lab; Enterprise Native Core/Pipeline and
MongoDB compatibility differences are called out explicitly.
A Firebase Authentication ID token represents an end user. A
Google Cloud access token or attached service account
represents a workload. Firestore mobile/web SDK calls are
authorized by Security Rules using request.auth;
Admin/server libraries bypass those Rules and are authorized
with IAM. If a backend receives a user ID token, verifying
that token establishes user identity only—the backend must
still make its own application authorization decision before
using its more powerful workload identity. App Check, when
enabled, is an additional app/device attestation signal and
does not replace either user authorization or IAM.
Firebase CLI 15.30.0 was released 9 September 2026. Firebase
Admin Node.js 14.4.0 was released 10 September 2026, requires
Node.js 22+, and uses
@google-cloud/firestore 9.1.0. The Auth emulator
issues unsigned ID tokens for local testing; Admin SDK accepts
those only when FIREBASE_AUTH_EMULATOR_HOST is
explicitly configured. Never carry that emulator environment
variable into production.
Learning outcomes
Use custom claims only for compact access-control assertions rather than mutable profile data.
Explain when changed claims become visible to clients and how forced token refresh differs from backend revocation checks.
Distinguish short-lived ID tokens from long-lived refresh tokens and explain the effect of refresh-token revocation.
Design AtlasMart authorization so critical rapidly changing state does not depend solely on stale claims.
Test claim-sensitive behavior locally without printing or storing raw tokens.
2. What custom claims are good for
| Candidate data | Claim? | Reason |
|---|---|---|
| role = support | Usually yes | Compact authorization attribute used across requests |
| tenantId = t-atlas | Often, if stable and policy-controlled | Useful for tenant-scoped Rules/backends; keep lifecycle explicit |
| displayName | No | Profile/UI data changes independently and does not belong in every token |
| currentCartTotal | No | Rapidly changing business data; stale token would be wrong |
| fraudHold | Usually not as sole source | Critical mutable state often needs authoritative server lookup or revocation strategy |
Firebase custom claims are limited to 1000 bytes and must remain JSON-serializable. More importantly, their intended purpose is access control—not general user metadata.
3. Observe claim propagation in the Auth emulator
process.env.FIREBASE_AUTH_EMULATOR_HOST = "127.0.0.1:9099";process.env.GCLOUD_PROJECT = "demo-atlasmart-firestore";import { initializeApp } from "firebase-admin/app";import { getAuth } from "firebase-admin/auth";initializeApp({ projectId: "demo-atlasmart-firestore" });const auth = getAuth();const uid = "u-support-1";await auth.updateUser(uid, { displayName: "Atlas Support" }).catch(() => {});await auth.setCustomUserClaims(uid, { tenantId: "t-atlas", role: "support"});console.log("Claims updated. Existing client ID tokens remain unchanged until refreshed/reissued.");
import { getAuth } from "firebase/auth";const auth = getAuth();const before = await auth.currentUser.getIdTokenResult(false);console.log({ phase: "before-force-refresh", role: before.claims.role ?? null, tenantId: before.claims.tenantId ?? null, issuedAtTime: before.issuedAtTime, expirationTime: before.expirationTime});const after = await auth.currentUser.getIdTokenResult(true);console.log({ phase: "after-force-refresh", role: after.claims.role ?? null, tenantId: after.claims.tenantId ?? null, issuedAtTime: after.issuedAtTime, expirationTime: after.expirationTime});// Do not print the raw JWT itself.
The important observation is not the exact token text. Record
only decoded fields such as uid, role,
tenantId, issue time and expiry. Existing tokens
may continue to contain the old role until a new token is issued
through sign-in, normal refresh after expiration, or an explicit
force refresh.
Firebase ID tokens are short-lived and last about one hour. Refresh tokens are long-lived and can obtain new ID tokens until revoked or invalidated by account events. Therefore “I removed the role from the user record” and “every active request immediately loses the role” are not the same event.
4. Backend verification is identity input, not the final decision
const decoded = await adminAuth.verifyIdToken(idToken);const actor = { uid: decoded.uid, tenantId: decoded.tenantId ?? null, role: decoded.role ?? "customer", authTime: decoded.auth_time};// Authorization still follows.if (actor.tenantId !== order.tenantId) throw forbidden("CROSS_TENANT");if (!['support', 'admin'].includes(actor.role)) throw forbidden("ROLE_REQUIRED");
verifyIdToken() validates the token’s
signature/shape/expiry and returns decoded claims, but the
ordinary verification method does not by itself check
refresh-token revocation. When immediate session invalidation
matters in production, use the documented revocation mechanism
and an appropriate verification strategy rather than assuming
claim removal invalidates an already-issued JWT.
5. Simulate revocation locally, understand production revocation separately
// Production Firebase supports revokeRefreshTokens(uid) and verifyIdToken(token, true).// The mandatory local lab instead makes the freshness requirement deterministic.const revokedAfterByUid = new Map();export function revokeLocally(uid, nowSeconds) { revokedAfterByUid.set(uid, nowSeconds);}export function requireFreshSession(actor) { const revokedAfter = revokedAfterByUid.get(actor.uid) ?? 0; if ((actor.authTime ?? 0) <= revokedAfter) { throw Object.assign(new Error("SESSION_REAUTH_REQUIRED"), { status: 401 }); }}
// Run only in a real controlled Firebase project when immediate revocation is required.await adminAuth.revokeRefreshTokens(uid);try { const decoded = await adminAuth.verifyIdToken(idToken, true); // checkRevoked = true // continue authorization} catch (error) { if (error.code === "auth/id-token-revoked") { // Ask the client to reauthenticate/sign out. } throw error;}
The production revoked-token check requires consulting Firebase Authentication state and adds work compared with normal stateless JWT verification. Apply it according to risk rather than sprinkling it blindly into every endpoint. The mandatory lab uses a deterministic in-memory revocation threshold so learners can test freshness-sensitive authorization without pretending the emulator reproduces every production session mechanism.
6. Wrong approach: put rapidly changing authorization only in claims
Imagine AtlasMart stores refundLimitCents=50000 in
a claim and compliance lowers it to 5000 during an incident.
Existing tokens may retain the old value until refresh. A safer
pattern keeps coarse stable roles in claims and loads highly
dynamic/high-risk policy from an authoritative backend store
when the action is sensitive enough to justify it.
| Policy signal | Typical freshness need | Suggested source |
|---|---|---|
| UI support badge | Minutes may be acceptable | Custom claim after refresh |
| Tenant membership | Depends on revocation sensitivity | Claim plus server-side membership check for privileged actions if needed |
| Refund amount ceiling during incident | Immediate/high risk | Authoritative backend policy/document |
| Account disabled/revoked | Immediate when enforced | Auth account state + revocation-aware backend check where required |
| Document ownership | Per resource | Authoritative Firestore document + Rules/application authorization |
7. Do not authorize only in the UI
// BAD: UI decides access, backend trusts it.if (tokenResult.claims.role === "support") showRefundButton();// attacker can still call POST /refund directly// GOOD: UI is convenience only; backend repeats authoritative decision.const actor = await verifiedActor(adminAuth, req.headers.authorization);authorizeRefund(actor, order);await performRefund(order);
The UI can use claims to shape navigation, but hidden controls are not a security boundary. Security Rules and backend authorization remain authoritative.
8. Claim test matrix
| Case | Token state | Expected outcome |
|---|---|---|
| Customer token | role=customer | Direct own reads okay; refund API denied |
| Role changed in Admin, old token reused | old role still encoded | Behavior follows old token until refresh unless additional authoritative check blocks it |
| Forced refresh after claim change | new support role encoded | Support UI/API can observe updated claim |
| Locally revoked-after threshold newer than auth_time | stale session | Backend simulation requires reauthentication |
| Production refresh tokens revoked + revocation-aware verify | revoked session | Backend rejects token and asks for reauthentication |
Production judgment and bridge to Lesson 4
Claims trade lookup cost for bounded staleness. Use them for compact, trusted authorization assertions whose lifecycle you understand. For critical state, combine them with authoritative server checks and explicit session-revocation policy. Lesson 4 turns to the other side of identity—the backend credential itself—and shows how ADC, attached service accounts, impersonation and Workload Identity Federation avoid distributing long-lived private keys.
Knowledge check
- When do changed custom claims appear in a user’s token?
- How long do Firebase ID tokens normally last?
- Does verifyIdToken(token) automatically check that refresh tokens were revoked?
- Why should fast-changing refund policy not live only in claims?
- Can a UI role check replace backend authorization?
Review the answers
1. When a new ID token is issued—for example after sign-in/reauthentication, normal refresh, or a forced refresh.
2. About one hour.
3. No; revocation-aware verification is a separate documented option.
4. Already-issued tokens can remain stale; high-risk mutable policy may require an authoritative backend lookup.
5. No. It only changes presentation; the API must enforce the same or stronger policy.
Summary
Custom claims are signed, compact and useful—but not instantaneous. AtlasMart now distinguishes claim issuance, token refresh, session revocation and application policy freshness instead of treating “role changed” as an immediate global fact.
Authoritative references
- Secure data in Cloud Firestore
- Firestore IAM
- Security Rules conditions and server-library bypass
- Add the Firebase Admin SDK to your server
- Verify Firebase ID tokens
- Custom claims and access control
- Manage user sessions and token revocation
- Connect to the Authentication emulator
- Connect to the Firestore emulator
- How Application Default Credentials works
- Best practices for service accounts
- Workload Identity Federation
- Firestore IAM roles and permissions
- Authenticate and connect with Firestore MongoDB compatibility
- Enterprise Native server client libraries
- Firebase Admin Node.js SDK release notes
- Firebase CLI release notes