Skip to content

Work / Chitragupt / Wiki / Decisions

2026-10-05-app-check-is-not-a-rules-check

Decisioncanonicalverified 2026-10-05

DECISION.2026-10-05.APP-CHECK-IS-NOT-A-RULES-CHECK

App Check is not checked in security rules, and nothing may pass only in the emulator

Decision

  1. No security rule reads App Check. hasAppCheck() is deleted from firebase/firestore.rules and firebase/storage.rules, with its 28 uses. Owner, admin and CA checks are unchanged. App Check for Firestore and Storage is the Enforce switch in the Firebase console; for callables it stays enforceAppCheck in _lib/callable.ts.
  2. The app_check_bypass claim is retired — from the rules, verifyEmailOtp, the three seed scripts and the rules tests. The emulator and production now take the same path through every rule.
  3. Every HTTP function says invoker: "public" — in onZodCall for callables, and on each onRequest.
  4. Every collection-group query has its index declared in firebase/firestore.indexes.json.
  5. A deploy is not done until a signed-in production check passes — pnpm --filter website e2e:production, steps in ops/runbooks/production-access.md.

This amends point 3 of 2026-06-28-phase-7a-app-check-enforcement, which planned the rules-side check. Its callable-side enforcement and the mintAppCheckToken provider stand.

Why

On 2026-10-05 the owner could not get past the consent screen on the live site. Four separate faults were stacked, each hiding the next:

Fault What a user saw Since
mintAppCheckToken and 27 callables were private Cloud Run services every call answered 401 their first deploy
the functions service account could not sign tokens (iam.serviceAccounts.signBlob) the token endpoint answered 500 always
rules tested request.get('app', null) != null every signed-in read refused 2026-06-28
fourteen collection-group queries had no index stale reviews never rebuilt; three scheduled jobs failing their first deploy

The rules fault is the instructive one. A rule sees who is signed in and what they ask for. It does not see the App Check token: request has no app. The guard was false for every real caller. It was written on an assumption nobody checked against production, and it survived three months because of the bypass: seeded users and tests carried app_check_bypass: true, a claim that existed only in the emulator, so every local run passed. The comment beside the guard even recorded the symptom — "request.app stays null in the emulator" — and read it as an emulator quirk.

The general lesson, which is why rule 2 is worded as it is: a branch that makes a check pass only in the emulator removes the only evidence that the check works. The other three faults have the same shape — a private service, a missing role and a missing index are all invisible to the emulator, which has no IAM and answers any query.

Each fault now has a guard that fails before a deploy, not after:

Rule Guard
1, 2 scripts/lint-hard-rules.js — hard-rule-app-check-in-rules, hard-rule-app-check-bypass
3 apps/functions/src/__tests__/http-functions-are-public.test.ts
4 apps/functions/src/__tests__/collection-group-indexes.test.ts
5 apps/website/e2e/production.spec.ts

Impact

  • firebase/firestore.rules, firebase/storage.rules, firebase/firestore.indexes.json (14 field overrides, 3 composite indexes).
  • apps/functions/src/_lib/callable.ts, auth/app-check-token.ts, auth/email-otp.ts, index.ts; the three seed scripts.
  • grievances/_intake uploads are signed-in only. The old guard refused everyone there, anonymous or not, so nothing that worked is narrowed; opening it to anonymous callers needs a way to tell a person from a script. tasks.md T037.
  • What production needs outside the code — the two IAM grants, the Enforce switch — is in ops/runbooks/production-access.md and nowhere else.

Sources

  • Production logs, 2026-10-04 18:30 to 19:05 UTC: mintappchecktoken 401 then 500; acceptdpdpconsent with verifications: { auth: VALID, app: MISSING }; fiveminutelydraindirtyreviews FAILED_PRECONDITION.
  • A signed-in walk of the live site as the Play reviewer account, before and after the token endpoint was fixed: reads still refused with a valid token.
  • Firebase, "Enable App Check enforcement": enforcement for Firestore and Storage is a console setting; the page does not mention rules.