fix(auth): require registered claims when validating JWTs - #915
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The joserfc migration (#908) replaced authlib's CodeIDToken with a bare JWTClaimsRegistry, which carries no essential claims. A signature-valid token missing "exp" was accepted forever: with no expiry claim there is nothing for the expiry check to compare against, so it never fails, and get_auth_info never fires a refresh for it. Restore the iss/sub/aud/iat/exp requirement 3.8.1 enforced via CodeIDToken. 3.8.1 applied it unconditionally to both access_token and id_token, so this is the same contract, not a new one.
283ffee to
d78d972
Compare
🚀 Artifacts — PR #915 by @Mighty303
Download the wheel file and binaries with gh CLI or from the workflow artifacts. 📦 Install & RunPre-requisites# Install uv if needed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create and enter artifacts directory
mkdir artifacts && cd artifactsQuick Test with Python Packagebash -c 'set -euo pipefail; echo; echo "WARNING: You are about to download and execute CI artifacts from PR #915 by @Mighty303. Do NOT proceed unless you have reviewed the PR diff and trust the source."; echo; read -rp "Type I understand to continue: " C; [ "$C" = "I understand" ] || { echo "Aborted."; exit 1; }; gh run download 33903250321 -n dist -R pyupio/safety; uvx safety-*-py3-none-any.whl --version'Run other Safety commands as followsuvx safety-*-py3-none-any.whl auth status
uvx safety-*-py3-none-any.whl auth login
uvx safety-*-py3-none-any.whl scan
|
There was a problem hiding this comment.
🟢 Approval recommended
The required claims are restored with comprehensive regression coverage and no unresolved issues.
Pull request overview
Restores strict JWT validation by requiring registered claims after the joserfc migration.
Changes:
- Requires
iss,sub,aud,iat, andexp. - Adds regression tests for missing claims and expiry handling.
File summaries
| File | Description |
|---|---|
tests/utils/test_tokens.py |
Tests valid, expired, and incomplete JWTs. |
safety/utils/tokens.py |
Enforces required JWT registered claims. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
End-to-end test with a real
|
| Real | Stubbed |
|---|---|
The safety CLI, unmodified |
The IdP's signing key and /oauth/token |
safety auth login, browser launch, localhost callback server |
The platform's /cli/auth bounce |
| PKCE S256 — the stand-in verifies the challenge, it does not rubber-stamp | /userinfo, /cli/api/v1/initialize |
authlib fetch_token, AuthConfig.from_token, real auth.ini on disk |
|
JWKS fetch, get_token_claims, get_auth_info, safety auth status |
Each run uses a throwaway HOME plus an empty SAFETY_SYSTEM_CONFIG_PATH, so constants.py:67 cannot fall back to a real system config and no real credentials are touched.
Results
Every run completed an actual browser login. Login always succeeds and nothing is validated at that point. The only difference is on the next command.
BEFORE — id_token issued without exp:
── auth.ini written by the real login ──
access_token exp present claims: aud, email, exp, iat, iss, sub
id_token exp MISSING claims: aud, email, iat, iss, sub
── safety auth status ──
[2026-09-04 11:17:18]: Safety 3.8.1
Authenticated as demo@example.test <-- accepted, and never refreshed
AFTER — identical token:
── safety auth status ──
[2026-09-04 11:17:29]: Safety 3.8.1
Safety is not authenticated. Please run 'safety auth login' to log in...
CONTROL — this branch, normal tokens:
access_token exp present claims: aud, email, exp, iat, iss, sub
id_token exp present claims: aud, email, exp, iat, iss, sub
── safety auth status ──
Authenticated as demo@example.test <-- normal login unaffected
Description
Found while reviewing
3.8.1..mainahead of the next release.#908 (authlib.jose → joserfc) swapped
claims_cls=CodeIDTokenfor a barejwt.JWTClaimsRegistry().CodeIDTokenrequirediss/sub/aud/iat/expto be present; the bare registry declares nothing essential, so it validates only the claims a token happens to carry. With noexp, the expiry check has nothing to compare against, so it never runs and never fails.This is not a vulnerability, and not a release blocker. Signature verification and the RS256/PS256 allowlist are untouched, so a forged token is still rejected. It takes a real, IdP-signed token that is malformed, in one specific shape:
expAuthConfig.to_tokenhand-checksexpexp, access_token fineConsequence is bounded to that third row. The id_token feeds identity display (
SafetyContext().account, org UUID); the access_token, which actually authorizes API calls, keeps its own independentexpcheck and its authlib-driven refresh.What this is: an unintended regression from a validation contract 3.8.1 shipped. #908 was a library migration, and dropping the required-claims check wasn't part of its intent.
Fix — declare the claims essential:
All five rather than just
exp: 3.8.1 appliedCodeIDTokenunconditionally to both token types, so this restores a contract already running in production instead of asking the IdP for something new.Blast radius: all three callers (
AuthConfig.to_token,get_auth_info,_extract_org_uuid_from_jwt) already sit inside bareexcept Exceptionhandlers, soMissingClaimErrordegrades to discard-token-and-reauth, not a traceback. Machine tokens never reachget_token_claims, so enrollment is untouched.Type of Change
Related Issues
None.
Testing
Tests added or updated
No tests required
test_missing_registered_claim_is_rejected, parametrized over all five claims.test_missing_claim_is_not_silenced_by_silent_if_expired— pins thatsilent_if_expiredforgives expiry only. Without it, widening thatexceptlater would silently reopen this.Added a
_claims()helper and rewired the three existing tests to it, so they no longer fail for the wrong reason.Watched all six new cases fail with
DID NOT RAISE MissingClaimErrorbefore implementing.End-to-end against a real
safety auth login— see the comment below for the method and output.Verification:
tests/integration/test_enroll.py::test_enroll_invalid_key_rejected, which fails identically on pristinemain(env-dependent, unrelated)exptoken raisesMissingClaimErrorruff check,ruff format --check,pyrightcleanChecklist
Additional Notes
Two things from the same review, both out of scope here:
mainis currently red (run 33822577974). It's the flakytest_concurrent_read_and_write_dont_corruptmultiprocessing race, failing on scheduled runs since 2026-08-27, before any of the post-3.8.1 commits. Worth a rerun before tagging.iathalf is real but is not a regression: 3.8.1 rejected future-iattokens identically. That's still Safety keeps logging itself out #850.