Skip to content

fix: widen verify_broker_token except clause to jwt.PyJWTError - #4

Merged
kratsg merged 1 commit into
mainfrom
fix/jwt-pyjwterror-audit-gap
Sep 9, 2026
Merged

kratsg merged 1 commit into
mainfrom
fix/jwt-pyjwterror-audit-gap

Conversation

@kratsg

@kratsg kratsg commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

verify_broker_token in src/krb5_token_service/identity.py catches
jwt.InvalidTokenError (and ValueError/KeyError) around the JWKS-based
signature verification, but jwt.algorithms.RSAAlgorithm.from_jwk(key_data)
(called just above, to build the public key from a JWKS entry) can raise
jwt.exceptions.InvalidKeyError for a malformed or non-RSA JWKS entry --
e.g. reachable via _select_jwk's single-key fallback path whenever the
JWKS happens to publish exactly one key and that key is malformed/non-RSA.

InvalidKeyError is a jwt.PyJWTError subclass but not a subclass of
InvalidTokenError, and isn't ValueError/KeyError either, so it escaped
both except clauses and propagated out of verify_broker_token entirely
uncaught -- surfacing as a bare unhandled 500 with no audit log line at
all
(the route handler's except HTTPException never sees it). A broker
JWKS key-rotation glitch or misconfiguration (a non-RSA key appearing in
the published JWKS) would silently produce unaudited 500s instead of the
usual audited 401.

Fix

Widen the first except clause from jwt.InvalidTokenError to
jwt.PyJWTError -- the common base class for both InvalidTokenError and
InvalidKeyError -- so any PyJWT-raised verification failure is classified
as a normal, audited 401.

This was found and first fixed in servicex-token-service during code
review while building that service (identity.py is shared, near-verbatim,
across the voms/krb5/condor/servicex-token-service family). This PR ports
the same fix here since krb5-token-service's identity.py has the exact
same bug.

Test plan

  • Added test_malformed_jwks_key_is_401 in tests/test_identity.py:
    builds a JWKS entry missing n/e ({"kid": "malformed-key", "kty": "RSA", "use": "sig"}), mints a token whose kid matches it, and asserts
    verify_broker_token raises HTTPException with status_code == 401.
    Reassigns stub_jwks_fetch.keys to a new list rather than mutating in
    place, since the underlying jwks fixture is session-scoped.
  • Confirmed the new test fails against pre-fix code (raises unhandled
    jwt.exceptions.InvalidKeyError instead of HTTPException).
  • Confirmed the new test passes against the fix.
  • pixi run -e dev check (lint + format + mypy + full test suite):
    148 passed, 1 skipped, no regressions.

Generated with Claude Code

RSAAlgorithm.from_jwk raises jwt.exceptions.InvalidKeyError for a
malformed or non-RSA JWKS entry (e.g. hit via _select_jwk's single-key
fallback when the JWKS happens to contain exactly one key). InvalidKeyError
is a PyJWTError but not an InvalidTokenError, so the previous
except jwt.InvalidTokenError clause let it escape verify_broker_token
uncaught, surfacing as an unhandled 500 with no audit log line instead
of the usual audited 401. A broker JWKS key-rotation glitch or
misconfiguration would silently produce unaudited 500s.

Widen the except clause to jwt.PyJWTError, the common base class for both
InvalidTokenError and InvalidKeyError, so any PyJWT-raised verification
failure is classified as a normal, audited 401.

Found and first fixed in servicex-token-service during code review of its
near-identical identity.py; ported here since krb5-token-service shares
the same pattern.

Assisted-by: Claude (Anthropic)
@kratsg
kratsg merged commit dc98be9 into main Sep 9, 2026
5 checks passed
@kratsg
kratsg deleted the fix/jwt-pyjwterror-audit-gap branch September 9, 2026 07:47
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.

1 participant