FastAPI / Editorial solution guide

401, 403, or hidden resources: trace identity and policy

Diagnose missing verification, incorrect resource scope, and leaked test overrides without granting broad access or treating a parsed token as a trusted user.

About 4 min readContent reviewed

Read-only knowledge base · No public comments

Recognize the symptoms

A protected route returns 401 despite a bearer header, one user receives 403, a tenant-scoped lookup returns 404, or tests unexpectedly allow every actor. A header can be syntactically valid while verification fails, and verified identity can still lack permission for the target resource.

Understand the likely causes

  • Common causes include the wrong authentication dependency, extraction without verification, missing or rejected claims, a resource loaded under the wrong tenant, an omitted policy on one route, or a dependency override that was not cleared. CORS and OpenAPI security metadata do not replace server-side verification or object-level authorization.

Map the trusted identity path

Record the declared scheme, extractor, verifier, trusted principal type, router and endpoint dependencies, and safe response classification. Do not copy a real token, password, or claims body into logs or a reproduction. Check which callable actually runs and whether the request reaches it. Parsing a bearer value proves receipt only. A real verifier must apply the chosen algorithm, key, issuer, audience, lifetime, and session policy.

python Complete isolated identity and tenant-policy diagnosis
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
def principal():
    raise HTTPException(status_code=401, detail="No lab identity", headers={"WWW-Authenticate": "Bearer"})
app = FastAPI()
@app.get("/record")
def record(actor: Annotated[dict, Depends(principal)]):
    value = {"tenant": "red", "text": "public lab data"}
    if actor["tenant"] != value["tenant"]: raise HTTPException(status_code=404, detail="Missing")
    return {"text": value["text"]}
with TestClient(app) as client:
    print(client.get("/record").status_code)
    try:
        app.dependency_overrides[principal] = lambda: {"tenant": "red"}
        print(client.get("/record").status_code)
        app.dependency_overrides[principal] = lambda: {"tenant": "blue"}
        print(client.get("/record").status_code)
    finally:
        app.dependency_overrides.clear()
    print(client.get("/record").status_code)
Expected output
401
200
404
401

The sequence is unauthenticated, allowed owner, hidden wrong tenant, and unauthenticated again. Overrides control only trusted synthetic input; the actual resource policy remains active.

Interpret the intended denial contract

Missing or invalid authentication commonly uses 401 and a relevant challenge. A verified principal lacking authority commonly receives 403, while an existence-hiding tenant policy can intentionally return 404. Do not diagnose every 404 by widening a query until it reveals another tenant’s record. Compare trusted actor, operation, target, tenant, and current resource state. A role or scope alone may still be insufficient for ownership, and a client-supplied owner_id must not become trusted identity because its type validated successfully.

Retain the real policy while controlling test inputs

Override the identity verifier or repository only to make a safe scenario deterministic, and keep resource authorization active. The first example allows the synthetic tenant owner, hides the resource from another tenant, and denies again after clearing the override. Replacing the policy itself with always true would remove the behavior the test is meant to protect. Scope overrides to an isolated app or fixture and restore them in finally even when an assertion fails.

python Complete pure permission-matrix check
def allowed(actor, target):
    return bool(actor and actor["tenant"] == target["tenant"] and "read" in actor["permissions"])
target = {"tenant": "red"}
print(allowed({"tenant": "red", "permissions": {"read"}}, target))
print(allowed({"tenant": "blue", "permissions": {"read"}}, target))
print(allowed({"tenant": "red", "permissions": set()}, target))
Expected output
True
False
False

Matching tenant and read permission are both required in this synthetic policy. A matching role-shaped field from an unverified request would not be a trusted actor.

Review adjacent exposure and state changes

A detail route can be protected while list, export, attachment, update, or background flows bypass the same policy. Enumerate those paths and review router-level plus operation-level dependencies. Keep tenant and ownership conditions close to persistence when state can change concurrently. Public response filtering prevents some field leakage but does not authorize the object. Review cache keys and privileged audit records as well, since a correct route check can be undermined by a shared cached response or a verbose log. Avoid resolving a denial by injecting an administrator or disabling the verifier outside the isolated test.

Verify a complete actor and resource matrix

Test absent and invalid verification, valid owner, unrelated user, wrong tenant, privileged actor where permitted, and missing resource. Assert the public status and absence of forbidden data, and confirm original dependencies return after teardown. The examples use only public synthetic principals and records, no real credential, account, database, identity provider, or network listener. Real cryptographic and persistence integration needs separately authorized tests.

Verify the fix

The corrected route should deny absent or invalid identity, allow only the intended actor-resource combinations, and preserve the chosen hidden-resource policy. Verify no unauthorized fields or rows appear and all overrides are cleared. Keep separate evidence for cryptographic verification and real transactional tenant isolation; these local fixtures prove neither automatically.

Keep it from recurring

  • Keep extraction, verification, trusted context, and resource policy visibly separate. Use deny-by-default routes, scoped test overrides, safe audit fields, and a regression matrix for every exposure path. Review dependency and schema changes as security-relevant changes rather than assuming documentation or a valid token secures all operations.

Official references