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.

Intermediate → Advanced150–180 minutesClaims · refresh · revocationFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0 · Rules tests 5.0.2Last reviewed: September 2026

1. Claims are cached authorization assertions, not a live permissions table

Execution and safety note

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.

Chapter 14 reproducibility baseline · reviewed 16 September 2026

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.

Trust boundary that must not be blurred

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.

Pinned versions and current release check

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

01

Use custom claims only for compact access-control assertions rather than mutable profile data.

02

Explain when changed claims become visible to clients and how forced token refresh differs from backend revocation checks.

03

Distinguish short-lived ID tokens from long-lived refresh tokens and explain the effect of refresh-token revocation.

04

Design AtlasMart authorization so critical rapidly changing state does not depend solely on stale claims.

05

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

claims-local.mjs · set claims 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.");
web client · observe claim propagation safely
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.

Token lifetime

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

verify and normalize claims
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

local revocation simulation · deterministic application gate
// 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 });  }}
production-only revocation pattern
// 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 vs good boundary
// 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

  1. When do changed custom claims appear in a user’s token?
  2. How long do Firebase ID tokens normally last?
  3. Does verifyIdToken(token) automatically check that refresh tokens were revoked?
  4. Why should fast-changing refund policy not live only in claims?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.