Stop counting optional authentication as protection - #63
Merged
Merged
Conversation
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.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Candidate addition #5 — the first of the seven, taken first because it is a security-audit helper that reports success incorrectly.
assert_routes_protectedpassed routes that anyone can call. A route depending onAuth.optional_userlets an anonymous caller through, but it declared the sameHTTPBearerscheme asAuth.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, bothsecurity=[{'HTTPBearer': []}], and the check silent.The fix
Auth.optional_userdeclares its own scheme name, exported aspy_common.security.OPTIONAL_AUTH_SCHEME:Both dependencies still read the same
Authorizationheader 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.{}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_routerstays protected — the router's guard demands a token, and the check asks whether any declared requirement does, not which came last: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.pyspells the scheme name out instead of importing it frompy_common.security. That import would pull PyJWT into a module needing only thehttpextra — the failuremake extras-checkexists to catch, and one this branch's first draft walked straight into before #61 widened the check to coverpy_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 anOptionalBearerscheme, visible as a second entry in Swagger's Authorize dialog.Verification
403 passed (from 399), coverage 92.23%,
make extras-checkgreen on all twelve pairs.Mutation-checked three ways, each turning tests red:
optional_userto the shared bearerNext
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.