Skip to content

Sprint review: JWTPlugin gets the middleware's checks; object dispatch leaves service.py - #34

Merged
thorwhalen merged 4 commits into
masterfrom
sprint-review-hardening
Oct 13, 2026
Merged

thorwhalen merged 4 commits into
masterfrom
sprint-review-hardening

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Sprint review (2026-10-11 → 13) of #23, #24, #27, #29, #31, #33: adversarial review, design review, coverage first.

What this changes

Auth (closes #25). The sprint hardened mk_jwt_middleware (aiohttp), but mk_app only builds Bottle apps, so JWTPlugin is what actually guards a py2http service, and it still had every flaw #24 fixed:

  • an expired token, a token with a disallowed alg, or one of the wrong key family escaped as a 500 (only DecodeError was caught);
  • the token was echoed back in the 401 body;
  • Authorization minus its first 7 characters was taken as the token, whatever the scheme;
  • an empty secret was accepted (PyJWT ≥2.10 then fails every request);
  • CORS preflight called handler(*args, *kwargs);
  • ignore_methods crashed on /openapi (no method_name).

Both now share a framework-free py2http.auth (jwt_algorithms, bearer_token, decode_jwt, key_matches, jwt_decode_kwargs). ApiKeyAuthPlugin and mk_superadmin_middleware refuse an empty key (it let header-less requests through) and compare in constant time; the superadmin middleware raises its 401 rather than returning it. New decode_kwargs= on JWTPlugin and mk_jwt_middleware passes audience/issuer/leeway to jwt.decode (the seam #25 asked for).

Design. service.py was 1266 lines, half of it the sprint's object-dispatch machinery. It now lives in py2http.obj_dispatch and py2http.sizeof (moved verbatim, AST-checked by the reviewer), and every name stays importable from py2http.service. Small cleanups: LRUObjStore.peek, direct quota validation, a redundant ternary. The tests' three copies of a WSGI _post helper are one call_wsgi.

Caller-visible changes (all refusals of unsafe setups)

  • JWTPlugin(secret="") with verify=True, JWTPlugin(algorithms="HS256") (a bare string; pass a list), and mixed HMAC + public-key algorithms with verify=True now raise at construction, as mk_jwt_middleware already did after JWT middleware: verify signatures, one algorithm family by default, 401 for every bad token #24.
  • ApiKeyAuthPlugin("") / mk_superadmin_middleware("") raise.
  • py2http.bottle_plugins no longer imports jwt / abort at module level; import py2http no longer loads jwt or cryptography.

No dependents in the local package fleet use JWTPlugin, ApiKeyAuthPlugin, mk_superadmin_middleware or the moved private names.

Review

An independent adversarial review (Opus) tried 28 hostile tokens, Bearer-prefix variants, request-token leakage between Bottle requests, import cycles, and the verbatim move. Nothing got through. Its findings (a 500 on non-UTF-8 Authorization headers, the mixed-family check firing without verification, no server-side log for key problems) are fixed in the last commit, with tests.

Tests

Characterisation tests were committed first and pass on the unchanged code. Full suite: 211 passed (was 149). Coverage of touched modules: bottle_plugins 22% → 94%, middleware 87% → 100%, default_configs 68% → 82%, new auth 100%, obj_dispatch 97%, sizeof 95%; total 73% → 78%.

🤖 Generated with Claude Code

thorwhalen and others added 4 commits October 13, 2026 06:28
…t misuse; share the WSGI test helpers

Characterisation tests on the unchanged code, ahead of hardening JWTPlugin
and splitting object dispatch out of service.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…http middleware got

The sprint hardened mk_jwt_middleware, but mk_app only builds Bottle apps, so
JWTPlugin is what guards a py2http service. It still caught only DecodeError
(an expired token, or one with a disallowed alg, escaped as a 500), echoed the
token back in the 401 body, took any header's 8th character onward as the
token, accepted an empty secret, and passed **kwargs as positional args on
CORS preflight.

Both now share py2http.auth: jwt_algorithms (validated at construction),
bearer_token, decode_jwt (client-safe reasons, never the token) and
key_matches (constant time). ApiKeyAuthPlugin and mk_superadmin_middleware
refuse an empty key (which let header-less requests through) and compare in
constant time; the superadmin middleware raises its 401 instead of returning
it. The Bottle plugins keep the wrapped handler's attributes (functools.wraps),
so JWTPlugin(ignore_methods=...) no longer fails on /openapi or when stacked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d sizeof

service.py had grown to 1266 lines, half of it the sprint's object-dispatch
machinery (attribute policies, owned and LRU stores, class and instance
dispatchers) and a deep-sizeof walker. They now live in py2http.obj_dispatch
and py2http.sizeof, and every name stays importable from py2http.service.

Small cleanups on the way: LRUObjStore.peek replaces OwnedObjStore reaching
into another object's _data, quota is validated directly rather than by
building a throwaway store, and the size check drops a redundant ternary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…seam (#25)

From the adversarial review of this branch:
- a non-UTF-8 Authorization header raised (Bottle while decoding it,
  aiohttp in key_matches / jwt.decode) and became a 500; it is now refused;
- the mixed-algorithm-family check only applies when signatures are verified;
- a server-side key problem (InvalidKeyError...) is logged, not just sent to
  the client as an opaque 401.

decode_kwargs= on JWTPlugin and mk_jwt_middleware passes audience, issuer,
leeway... to jwt.decode (without it a token with an aud claim was always
refused); it may not override key, algorithms or options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thorwhalen
thorwhalen merged commit 2b74707 into master Oct 13, 2026
6 checks passed
@thorwhalen
thorwhalen deleted the sprint-review-hardening branch October 13, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JWTPlugin (bottle) lags mk_jwt_middleware; no audience/issuer passthrough in either

1 participant