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.
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.
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)
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.
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))
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.