LEARN · DEBUGGING GUIDE

Firebase Security Rules Blocking Requests: Debugging Guide

When Firebase Security Rules block a request, compare the real auth context and request path with every matching allow expression, then reproduce it in the Emulator Suite.

IntermediateAuth7 min read

What this usually means

The most common cause is a mismatch between the auth context or path the client sends and what the rules expect. Firebase Security Rules evaluate `request.auth` and every match statement that covers the requested document. Access is granted if any applicable `allow` condition is true; if none are true, the request is denied. The Rules Playground checks the path and auth values entered manually, so an allowed playground request is not proof that the application sent the same values.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `firebase emulators:start --only firestore` for interactive local debugging, or `firebase emulators:exec --only firestore '<test command>'` for a repeatable rules test run.
  • 2In the Firebase Console Rules Playground, test the same path and auth shape as the failing client request; treat it as a focused rule check, not a substitute for integration tests.
  • 3Check the client-side error object: `error.code` should be `'permission-denied'`; log the full error to see if it includes the rule line that failed
  • 4Verify authentication state with `currentUser` and `getIdTokenResult()`; log only the UID and expected claim names, never the raw ID token.
  • 5Call `getIdTokenResult()` in a safe local or staging session and inspect its UID and claims without pasting a live token into a third-party decoder.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • search`firebase.json` — check if `firestore.rules` or `database.rules` path is correctly set
  • searchFirebase Console > Firestore > Rules — the live rules that are actually enforced (not local files)
  • searchFirebase Console > Authentication > Users — verify the user exists and has expected custom claims
  • searchClient-side network tab — look for failed XHR requests to `firestore.googleapis.com` with status 403
  • searchFirebase Functions logs — if using callable functions, the context.auth may be undefined if the token is missing
  • searchCloud Logging (formerly Stackdriver) — query `protoPayload.methodName="google.firestore.v1.Firestore/Listen"` for denied reads
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningMissing `auth` object in rules when request is unauthenticated (rules expect `request.auth.uid` but user is anonymous)
  • warningExpired ID token not refreshed before making the request (tokens expire after 1 hour by default)
  • warningCustom claims were changed on the server but the client is still using an older ID token; force a token refresh or sign in again.
  • warningAn overlapping broader match grants access because any matching `allow` expression that evaluates to true permits the operation.
  • warningSimulator used with a non-existent UID or wrong project ID, giving false positive results
  • warningSecurity Rules version mismatch (v1 vs v2) causing syntax errors or different default behavior
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAfter changing custom claims, force one ID-token refresh with `await currentUser.getIdToken(true)` or require a fresh sign-in; do not refresh before every request.
  • buildEnumerate every match pattern that covers the path. A broad allow cannot be overridden by a narrower false condition, so remove or tighten unintended broad grants.
  • buildUse `request.auth != null` as a base condition before checking UID, especially for public data
  • buildFor custom claims, force a token refresh or fresh sign-in after the Admin SDK update instead of relying on an arbitrary delay.
  • buildSplit rules into smaller match blocks to avoid complex boolean logic that's hard to debug
  • buildUse the Firebase Emulator Suite locally to test rules with real auth flows before deploying
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAfter applying the fix, run the exact same client operation and confirm the error code changes from 'permission-denied' to success
  • verifiedIn the Rules Playground, reproduce the same path, operation, UID, and claim values observed safely with `getIdTokenResult()`.
  • verifiedDeploy reviewed rules with `firebase deploy --only firestore:rules`, then run the same staging client request with the expected authenticated and unauthenticated contexts.
  • verifiedUse a unit test framework like `@firebase/rules-unit-testing` to simulate authentication and assert allowed/denied
  • verifiedReview audit logs in Cloud Logging for the specific document path to see if rules now evaluate to `true`
  • verifiedHave a second developer review the rule file — fresh eyes catch logical errors faster
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't rely solely on the simulator — it doesn't simulate token expiration or custom claim delays
  • warningNever deploy rules directly from the console without testing via the emulator or a staging project
  • warningRemember that document access calls such as `get()` and `exists()` have per-evaluation limits and can incur billed reads even when the request is rejected.
  • warningDon't hardcode UIDs in rules unless absolutely necessary; use roles via custom claims instead
  • warningDon't forget that Firebase Storage rules have a different syntax (resource vs. request.resource)
  • warningNever use `request.auth.uid == 'admin'` expecting the UID to be 'admin' — that's a common typo for custom claims
( 07 )War story

The Phantom Denial: Firebase Rules Blocking Valid Admin Requests

Backend DeveloperFirebase Firestore, Node.js Admin SDK, React frontend with Firebase Auth

Timeline

  1. 09:15Deploy new Firestore security rules to production
  2. 09:17Support tickets flood in: all users see 'Missing or insufficient permissions' on read
  3. 09:20Quick check: simulator says 'Simulated read allowed' for authenticated user — confusion
  4. 09:25Check client-side logs: token exists, but error persists
  5. 09:30Inspect `getIdTokenResult()`: the expected custom claim `role: admin` is present.
  6. 09:35Rule snippet: `allow read: if request.auth.uid == 'admin';` — oops, comparing UID string literal
  7. 09:40Fix: change to `request.auth.token.role == 'admin'` and redeploy
  8. 09:42Users regain access; postmortem reveals the mistake

This composite incident starts after a Firestore rules deployment: authenticated administrators receive `permission-denied` even though a manually configured Rules Playground request was allowed.

The production rule compares `request.auth.uid` with the literal string `admin`. The real client token carries the role as a custom claim, so the intended condition is `request.auth.token.role == 'admin'`. `getIdTokenResult()` confirms the client-side claim without exposing the raw token.

The corrected rule is covered with `@firebase/rules-unit-testing`, deployed with `firebase deploy --only firestore:rules`, and verified through the staging client. The lesson is to reproduce the real path and auth shape in the Emulator Suite rather than treating a manually entered playground context as end-to-end proof.

Root cause

Security rule incorrectly compared `request.auth.uid` to a string literal 'admin' instead of checking the custom claim `request.auth.token.role`.

The fix

Changed the rule from `allow read: if request.auth.uid == 'admin'` to `allow read: if request.auth.token.role == 'admin'`.

The lesson

Inspect claims with `getIdTokenResult()`, reproduce the real path and auth context with `@firebase/rules-unit-testing`, and use the Rules Playground only as a focused supplement.

( 08 )How Firebase Security Rules Evaluate Requests

Firebase Security Rules are evaluated for every read and write operation. The rules engine receives a `request` object that includes `auth` (the authenticated user), `resource` (the existing document data for reads), and `request.resource` (the new data for writes). The rules are written as boolean expressions — if any `allow` statement evaluates to `true`, the operation is permitted; if none do, it's denied.

Match order and specificity do not create a deny override. If several match statements cover the same request, access is granted when any applicable `allow` condition is true. A broad allow therefore cannot be restricted by adding a narrower false condition; the broad rule itself must be removed or tightened.

( 09 )The Rules Playground Gap

The Firebase Console Rules Playground is useful for checking a specific path, operation, and manually entered auth context. It does not prove that the application is sending that same context or that a client query satisfies the rules for every potential result.

In Firestore rules, custom claims are available under `request.auth.token`. Compare that shape with `getIdTokenResult()` and test it with `@firebase/rules-unit-testing`; do not paste live ID tokens into third-party tools.

( 10 )Token Lifecycle and Custom Claims Propagation

ID tokens issued by Firebase have a one-hour lifespan. The Firebase Auth SDK refreshes tokens, but an existing token does not gain newly assigned custom claims. After changing claims with the Admin SDK, require a fresh sign-in or call `getIdToken(true)` once to force a refresh.

In one incident, a support agent set a user's custom claim to 'premium' but the user still saw permission errors. The fix was to ask the user to refresh the page (which triggers token refresh) or implement a client-side token refresh on the next request. The rule itself was correct, but the token was stale.

( 11 )Debugging with Firebase Emulator Suite

The Firebase Emulator Suite provides a local environment that simulates Firestore, Auth, and Functions. It allows you to test security rules with real authentication flows. You can write integration tests using `@firebase/rules-unit-testing` that assert whether a specific operation is allowed or denied. This catches rule logic errors before deployment.

For example, you can simulate a user with custom claims: `const authedUser = testEnv.authenticatedContext('user123', { role: 'admin' });` then attempt a read and expect success. If the rule fails, the emulator logs the exact rule that blocked the request. This is far more reliable than the console simulator.

Frequently asked questions

Why does the simulator say 'allowed' but my app still gets permission denied?

The Rules Playground evaluates the path, operation, UID, and claims entered manually. Compare those inputs with `getIdTokenResult()`, then reproduce the request in the Emulator Suite; never copy a live ID token into a third-party decoder.

How do I check if my custom claims are reaching the rules?

In your client code, after the user is signed in, call `const token = await firebase.auth().currentUser.getIdTokenResult()` and inspect `token.claims`. This shows exactly what the rules will see as `request.auth.token`. If your custom claim is missing, you may need to force refresh the token or sign out/in.

Can I use `get()` in rules to fetch data from other documents?

Yes, but each `get()` call counts as a read operation and is billed. It also incurs latency. Use it sparingly. For example, `allow read: if get(/databases/$(database)/documents/config/$(request.auth.uid)).data.role == 'admin';`. Be aware that `get()` returns a `Resource` object, so use `.data` to access fields.

What does 'simulated read allowed' mean in the simulator if the request actually fails?

It means the path, operation, and auth context entered in the Rules Playground satisfied an allow expression. Compare those inputs with the real client request, then reproduce the request with the Emulator Suite and `@firebase/rules-unit-testing`.

Why do my rules work for reads but not writes?

Firestore rules distinguish read methods from write methods, and writes commonly validate `request.resource.data`, the proposed document state. Inspect every matching allow expression and the exact changed fields; another match does not override an allow that already evaluates to true.