Skip to content

Stop counting optional authentication as protection - #63

Merged
EdwardPham1615 merged 1 commit into
mainfrom
fix/optional-auth-audit
Sep 27, 2026
Merged

EdwardPham1615 merged 1 commit into
mainfrom
fix/optional-auth-audit

Conversation

@EdwardPham1615

Copy link
Copy Markdown
Owner

Candidate addition #5 — the first of the seven, taken first because it is a security-audit helper that reports success incorrectly.

assert_routes_protected passed routes that anyone can call. A route depending on Auth.optional_user lets an anonymous caller through, but it declared the same HTTPBearer scheme as Auth.current_user — so the generated document was byte-identical and the audit could not tell an open endpoint from a guarded one. Reproduced before changing anything: two routes, one of each, both security=[{'HTTPBearer': []}], and the check silent.

The fix

Auth.optional_user declares its own scheme name, exported as py_common.security.OPTIONAL_AUTH_SCHEME:

/public/card   security=[{'OptionalBearer': []}]
/api/v1/me     security=[{'HTTPBearer':     []}]

Both dependencies still read the same Authorization header and admit exactly the same callers. The scheme is the only place the document can distinguish must be authenticated from may be.

Two recorded alternatives, tried and dropped

The recorded proposal offered either marking the route in OpenAPI or teaching the checker to read {} as public. I tried both:

  • openapi_extra={"security": [{}, ...]} appends rather than replaces — it produced [{'HTTPBearer': []}, {}, {'HTTPBearer': []}]. Readable by the checker, but messy in the spec and the burden lands at every call site.
  • Teaching the checker to read {} carries the same call-site cost.

A distinct scheme name needs nothing from the consumer and makes the document more informative.

The combination that is still protected

Optional auth on a protected_router stays protected — the router's guard demands a token, and the check asks whether any declared requirement does, not which came last:

/api/v1/card   security=[{'HTTPBearer': []}, {'OptionalBearer': []}]   → protected

My first test app got this wrong: it put the "public" route on a protected router, and the assertion failed because the route genuinely was protected. The distinction is now asserted directly, since it is the part most likely to be broken by a later simplification.

One thing about the layering

testing/routes.py spells the scheme name out instead of importing it from py_common.security. That import would pull PyJWT into a module needing only the http extra — the failure make extras-check exists to catch, and one this branch's first draft walked straight into before #61 widened the check to cover py_common.testing.routes. A test pins the two copies equal.

Consumer impact

This can newly fail a consumer's test suite, which is the point: optional-auth routes now have to appear in public_paths/public_prefixes. The OpenAPI document also gains an OptionalBearer scheme, visible as a second entry in Swagger's Authorize dialog.

Verification

403 passed (from 399), coverage 92.23%, make extras-check green on all twelve pairs.

Mutation-checked three ways, each turning tests red:

mutation result
revert optional_user to the shared bearer 2 tests fail
remove the checker's optional-scheme branch 1 test fails
desynchronise the two copies of the constant 2 tests fail

Next

Six candidates remain, in the planned order: #3 (sqlalchemy ceiling + a scheduled fresh-resolve job), #6 (adopt uvicorn's loggers — not uvicorn.access, which would duplicate the access log), #2, #4, #1, #7. Then release 0.3.0.

assert_routes_protected reported success on routes anyone could call. A
route depending on Auth.optional_user declared the same HTTPBearer scheme
as Auth.current_user, so the generated document was byte-identical and the
audit could not tell an open endpoint from a guarded one -- the single
thing that helper exists not to do. Reproduced before changing anything:
two routes, one of each, both `security=[{'HTTPBearer': []}]`, and the
check silent.

Auth.optional_user now declares its own scheme name. Both dependencies
still read the same Authorization header and admit exactly the same
callers; the scheme is the only place the document can distinguish "must
be authenticated" from "may be". The checker treats a route whose only
requirement is that scheme as open, so it has to be listed like any other
public route.

Two alternatives from the recorded proposal were tried and dropped.
Marking the route with openapi_extra={"security": [{}, ...]} appends
rather than replaces -- it produced [{HTTPBearer}, {}, {HTTPBearer}] -- and
would put the burden at every call site. Teaching the checker to read {}
as public has the same call-site cost. A distinct scheme name needs
nothing from the consumer and makes the document more informative.

Optional auth on a protected router stays protected: the router's guard
demands a token, and the check asks whether any declared requirement does,
not which came last. That combination is now asserted, because it is the
part most likely to be broken by a later "simplification".

testing/routes.py spells the scheme name out rather than importing it from
py_common.security, which would pull PyJWT into a module that needs only
the http extra -- the failure make extras-check exists to catch, and one
this branch's first draft walked straight into. A test pins the two copies
equal.

Mutation-checked three ways: reverting optional_user to the shared bearer,
removing the checker's branch, and desynchronising the two constants each
turn tests red.
@EdwardPham1615 EdwardPham1615 self-assigned this Sep 27, 2026
@EdwardPham1615 EdwardPham1615 added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file labels Sep 27, 2026
@EdwardPham1615
EdwardPham1615 merged commit 3fceb32 into main Sep 27, 2026
3 checks passed
@EdwardPham1615
EdwardPham1615 deleted the fix/optional-auth-audit branch September 27, 2026 13:58
@EdwardPham1615 EdwardPham1615 mentioned this pull request Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant