Chapter 14 · IAM, Admin/Server SDKs, Service Accounts, Authentication, and Trust Boundaries

Firebase Authentication vs Google Cloud IAM vs Firestore Security Rules: Who Authorizes Which Request Path

Separate Firebase Authentication, Security Rules and Google Cloud IAM by request path, then prove client-rule denial versus privileged server behavior in the AtlasMart emulator.

Intermediate → Advanced155–185 minutesAuthN · Rules · IAM · trust pathsFirebase JS 12.19.0 · Admin 14.4.0 · CLI 15.30.0 · Rules tests 5.0.2Last reviewed: September 2026

1. Why AtlasMart needs three different authorization mental models

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 has three operations that look similar from the UI but cross different trust boundaries. A customer reads orders/o-9001 directly from a browser. A support agent presses “Refund,” which calls an AtlasMart backend. A nightly worker repairs stale projections. All three eventually touch Firestore, but they do not arrive with the same principal or pass through the same policy engine. Treating them as “authenticated Firestore calls” hides the most important security distinction in the system.

Authentication establishes who a principal is. Authorization decides what that principal may do. Firebase Authentication issues end-user identity tokens. Cloud Firestore Security Rules authorize mobile/web client requests. Google Cloud IAM authorizes privileged server workloads. A backend that accepts an end-user token has two identities in play simultaneously: the caller’s user identity and the backend’s own workload identity.

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

Classify each AtlasMart request as client-direct, backend-mediated, or workload-only before choosing an authorization mechanism.

02

Explain what Firebase Authentication, Security Rules, IAM, ADC, service accounts and application authorization each prove—and what they do not prove.

03

Demonstrate that a client write denied by Security Rules can still be performed through a trusted Admin/server SDK path.

04

Distinguish authentication failures, Security Rules denials, IAM denials and application-level authorization failures in logs and tests.

05

Apply the same trust-boundary reasoning to Standard Native, Enterprise Native Core/Pipeline and MongoDB compatibility without conflating their access surfaces.

2. The principal map: user identity is not workload identity

Request path End-user identity Workload identity Primary authorization Can Rules protect this path?
Browser/mobile → Firestore Firebase Auth ID token None exposed to app Security Rules (+ App Check if enabled) Yes; this is the intended Rules path
Browser/mobile → AtlasMart API → Firestore Firebase Auth ID token verified by API API service account / ADC Application authorization, then IAM for Firestore Rules do not constrain the API Firestore call
Background worker → Firestore Usually no end-user token Worker service account / workload identity IAM + worker business policy No
Admin script → Firestore Operator identity may obtain ADC User ADC or impersonated service account IAM + operational procedure No
MongoDB-compatible driver → Enterprise DB Not the Firebase mobile/web Rules path SCRAM user or supported Google/OIDC workload identity MongoDB-compatibility authentication + IAM/DB permissions Do not assume Native client Rules model

Firebase Auth answers “which signed-in user is this?” for client-oriented flows. IAM answers “which Google Cloud principal is this workload operating as?” for privileged server flows. Security Rules can use request.auth.uid and token claims, but an Admin/server Firestore call is not converted into a Rules request just because the backend originally received a Firebase ID token.

Mechanism consequence

If support user u-support-1 calls the AtlasMart API, the API first verifies the user token and decides whether that user may refund the order. The subsequent Firestore write is performed as the API workload identity. IAM can permit that workload to write; only the application authorization layer knows that this particular support user is allowed to refund this particular tenant/order.

3. Build the local trust-boundary lab

package.json
{  "name": "atlasmart-firestore-ch14",  "private": true,  "type": "module",  "engines": { "node": ">=22" },  "dependencies": {    "firebase": "12.19.0",    "firebase-admin": "14.4.0"  },  "devDependencies": {    "firebase-tools": "15.30.0",    "@firebase/rules-unit-testing": "5.0.2"  }}
firebase.json
{  "firestore": {    "rules": "firestore.rules",    "indexes": "firestore.indexes.json"  },  "emulators": {    "firestore": { "port": 8080 },    "auth": { "port": 9099 },    "ui": { "enabled": true, "port": 4000 }  }}
local setup
mkdir atlasmart-firestore-ch14 && cd atlasmart-firestore-ch14npm init -ynpm install firebase@12.19.0 firebase-admin@14.4.0npm install --save-dev firebase-tools@15.30.0 @firebase/rules-unit-testing@5.0.2# Save firebase.json, firestore.rules and snippets from this chapter.npx firebase-tools@15.30.0 emulators:start   --project demo-atlasmart-firestore   --only firestore,auth
firestore.rules · client ownership boundary
rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    function signedIn() { return request.auth != null; }    function tenant() { return signedIn() ? request.auth.token.tenantId : null; }    match /profiles/{uid} {      allow get: if signedIn() && request.auth.uid == uid;      allow list: if false;      allow update: if signedIn()        && request.auth.uid == uid        && request.resource.data.uid == resource.data.uid        && request.resource.data.tenantId == resource.data.tenantId        && request.resource.data.diff(resource.data).affectedKeys()             .hasOnly(["displayName", "locale"]);      allow create, delete: if false;    }    match /orders/{orderId} {      allow get: if signedIn()        && resource.data.tenantId == tenant()        && resource.data.customerId == request.auth.uid;      allow list: if signedIn()        && resource.data.tenantId == tenant()        && resource.data.customerId == request.auth.uid        && request.query.limit <= 25;      allow create: if signedIn()        && request.resource.data.tenantId == tenant()        && request.resource.data.customerId == request.auth.uid        && request.resource.data.status == "DRAFT";      allow update, delete: if false;    }    match /refundRequests/{requestId} {      // Clients can create a request; only trusted backend code changes status.      allow create: if signedIn()        && request.resource.data.requestedBy == request.auth.uid        && request.resource.data.status == "REQUESTED";      allow get: if signedIn()        && resource.data.requestedBy == request.auth.uid;      allow list, update, delete: if false;    }    match /{document=**} {      allow read, write: if false;    }  }}

These Rules deliberately deny client-side updates of orders.status. That turns the refund operation into a privileged server action. The rule is not a “backend rule”; it is specifically the mobile/web policy boundary.

client-path.test.mjs · Security Rules are evaluated
import fs from "node:fs";import test, { before, after, beforeEach } from "node:test";import {  initializeTestEnvironment,  assertSucceeds,  assertFails} from "@firebase/rules-unit-testing";import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";let env;before(async () => {  env = await initializeTestEnvironment({    projectId: "demo-atlasmart-firestore",    firestore: {      host: "127.0.0.1", port: 8080,      rules: fs.readFileSync("firestore.rules", "utf8")    }  });});beforeEach(async () => {  await env.clearFirestore();  await env.withSecurityRulesDisabled(async ctx => {    const db = ctx.firestore();    await setDoc(doc(db, "profiles/u-1001"), {      uid: "u-1001", tenantId: "t-atlas", displayName: "Ava", locale: "en"    });    await setDoc(doc(db, "orders/o-9001"), {      tenantId: "t-atlas", customerId: "u-1001", status: "PAID", totalCents: 12990    });  });});after(async () => env.cleanup());test("client path is constrained by Rules", async () => {  const ava = env.authenticatedContext("u-1001", { tenantId: "t-atlas", role: "customer" }).firestore();  const other = env.authenticatedContext("u-2002", { tenantId: "t-atlas", role: "customer" }).firestore();  await assertSucceeds(getDoc(doc(ava, "orders/o-9001")));  await assertFails(getDoc(doc(other, "orders/o-9001")));  await assertFails(updateDoc(doc(ava, "orders/o-9001"), { status: "REFUNDED" }));});
backend/admin.js · emulator-safe initialization
// Never ship this module to a browser bundle.process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";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";import { getFirestore } from "firebase-admin/firestore";initializeApp({ projectId: "demo-atlasmart-firestore" });export const adminAuth = getAuth();export const adminDb = getFirestore();
backend-path.test.mjs · Admin path bypasses Rules locally
import test from "node:test";import assert from "node:assert/strict";import "./backend/admin.js";import { adminDb } from "./backend/admin.js";test("Admin SDK can perform a write forbidden to client Rules", async () => {  const ref = adminDb.doc("orders/o-9001");  await ref.set({    tenantId: "t-atlas",    customerId: "u-1001",    status: "PAID",    totalCents: 12990  });  await ref.update({ status: "REFUNDED" });  const snap = await ref.get();  assert.equal(snap.data().status, "REFUNDED");});// What this proves: the privileged SDK path is not governed by firestore.rules.// What it does NOT prove: production IAM is correct; the emulator does not enforce IAM.

Expected evidence: the owning client can read its own order, another client cannot, and the owning client still cannot mutate status. The Admin path can mutate status despite that client prohibition. This is the local proof that server libraries bypass Security Rules.

4. Authentication failure, rule denial, application denial and IAM denial are different failures

Failure Where it occurs Typical evidence Repair direction
No/invalid Firebase ID token Client or backend identity verification Unauthenticated / token verification error Fix sign-in/token transport; do not weaken authorization
Security Rules denial Mobile/web Firestore path PERMISSION_DENIED with emulator rule trace Fix query/write shape or Rules if policy is wrong
Application authorization denial AtlasMart API after token verification 403 with structured reason such as ROLE_REQUIRED Fix role/tenant/resource policy; IAM may be perfectly healthy
IAM denial Privileged server call in production PERMISSION_DENIED attributed to workload principal Grant the minimum required permission/role to the correct principal
App Check rejection Supported production Firebase service enforcement Invalid/missing attestation telemetry Fix app attestation/integration; do not use App Check as user authorization

A single “403” dashboard metric is therefore inadequate. AtlasMart should preserve the layer and reason in structured logs so an incident responder can distinguish a malicious user, a rule regression, a missing IAM binding and a broken App Check rollout.

5. Wrong approach: “The user is authenticated, therefore the request is authorized”

A UI might hide the refund button unless role === 'support'. An attacker can still call the endpoint directly. Likewise, a backend might verify an ID token and immediately perform an Admin SDK update. Both approaches authenticate the caller but fail to authorize the requested business action.

backend/authorize.js · identity then application authorization
export async function verifiedActor(adminAuth, authorizationHeader) {  const prefix = "Bearer ";  if (!authorizationHeader?.startsWith(prefix)) {    throw Object.assign(new Error("UNAUTHENTICATED"), { status: 401 });  }  const idToken = authorizationHeader.slice(prefix.length);  const decoded = await adminAuth.verifyIdToken(idToken);  return {    uid: decoded.uid,    tenantId: decoded.tenantId,    role: decoded.role,    authTime: decoded.auth_time  };}export function authorizeRefund(actor, order) {  if (actor.tenantId !== order.tenantId) {    throw Object.assign(new Error("CROSS_TENANT"), { status: 403 });  }  if (!["support", "admin"].includes(actor.role)) {    throw Object.assign(new Error("ROLE_REQUIRED"), { status: 403 });  }  if (order.status !== "PAID") {    throw Object.assign(new Error("ORDER_NOT_REFUNDABLE"), { status: 409 });  }}
backend/audit.js · log decisions, never credentials
export function auditAuthorization({ correlationId, actor, action, resource, outcome, reason }) {  console.log(JSON.stringify({    event: "authorization_decision",    correlationId,    actorUid: actor?.uid ?? null,    actorTenantId: actor?.tenantId ?? null,    actorRole: actor?.role ?? null,    action,    resource,    outcome,    reason,    // Deliberately omitted: ID token, refresh token, Authorization header,    // service-account private keys, ADC credential material.    at: new Date().toISOString()  }));}

The repair is explicit: verify token → normalize actor → load the minimum authoritative resource → evaluate tenant/role/invariant policy → perform privileged write → record a non-secret authorization decision. Do not log raw bearer tokens to make debugging easier; that turns observability into credential leakage.

6. Edition and mode boundaries

Database surface Client-direct authorization Privileged/server authorization Chapter 14 consequence
Standard Native Core Firebase Auth + Security Rules ADC/IAM Canonical lab
Enterprise Native Core Firebase Auth + Security Rules ADC/IAM Same identity split; re-check feature/SDK availability
Enterprise Native Pipeline Security Rules still govern supported client Pipeline operations; rule-query proof differs ADC/IAM for server libraries Do not assume Core request.query semantics
Enterprise MongoDB compatibility Separate MongoDB-compatible connection/authentication model IAM, supported workload OIDC/service accounts, or database user credentials depending on connection path Do not paste Native Security Rules assumptions onto a MongoDB driver

The trust principle survives the product variation: never let a privileged workload credential become an end-user credential, and never assume an end-user token restricts the backend’s Firestore identity automatically.

Verification checklist and cleanup

  • Client owner read succeeds; cross-user read fails.
  • Client order-status update fails under Rules.
  • Admin SDK emulator update succeeds, proving Rules bypass only—not production IAM correctness.
  • Application authorization returns a separate 403 reason before privileged write for wrong tenant/role.
  • No log line contains a raw JWT, refresh token, service-account key, Authorization header or ADC file.
  • Stop emulators to discard state, or restart with a clean data directory before the next exercise.

Production judgment and bridge to Lesson 2

Choose the authorization layer from the request path, not from the collection name. Direct client access is attractive when Rules can express the policy safely. Privileged or cross-resource actions often belong behind a backend, but moving them there increases blast radius because that backend is now an IAM principal with broader database capability. Lesson 2 focuses on containing that blast radius with least-privilege service identities plus mandatory application-layer authorization.

Knowledge check

  1. Does verifying a Firebase ID token make an Admin SDK Firestore write obey Security Rules?
  2. What does the emulator Admin-bypass test prove?
  3. Why is UI button visibility not authorization?
  4. Which identity does IAM evaluate for a backend Firestore call?
  5. Is MongoDB compatibility simply Native mode with different query syntax?
Review the answers

1. No. Token verification identifies the user; the Admin/server Firestore call uses the backend workload identity and bypasses Rules.

2. That the server SDK path is not constrained by the loaded Firestore Rules. It does not prove production IAM is correct.

3. Attackers can call APIs directly; authorization must be enforced at the policy boundary that performs the action.

4. The workload principal represented by ADC/service account or equivalent credentials.

5. No. It has a distinct driver/authentication surface and must not inherit Native client Rules assumptions automatically.

Summary

AtlasMart now has an explicit principal map. End users authenticate with Firebase Auth, direct client requests meet Security Rules, privileged workloads meet IAM, and backend business authorization bridges the two identities without confusing them.

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.