Skip to content

fix(auth): require registered claims when validating JWTs - #915

Open
Mighty303 wants to merge 1 commit into
mainfrom
fix/jwt-essential-claims
Open

fix(auth): require registered claims when validating JWTs#915
Mighty303 wants to merge 1 commit into
mainfrom
fix/jwt-essential-claims

Conversation

@Mighty303

@Mighty303 Mighty303 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

Found while reviewing 3.8.1..main ahead of the next release.

#908 (authlib.jose → joserfc) swapped claims_cls=CodeIDToken for a bare jwt.JWTClaimsRegistry(). CodeIDToken required iss/sub/aud/iat/exp to be present; the bare registry declares nothing essential, so it validates only the claims a token happens to carry. With no exp, 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:

Token pair 3.8.1 main today
both well-formed accepted accepted
access_token missing exp rejected rejected — AuthConfig.to_token hand-checks exp
id_token missing exp, access_token fine rejected accepted, and never refreshed
forged signature rejected rejected

Consequence 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 independent exp check 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:

return jwt.JWTClaimsRegistry(
    iss={"essential": True}, sub={"essential": True}, aud={"essential": True},
    iat={"essential": True}, exp={"essential": True},
)

All five rather than just exp: 3.8.1 applied CodeIDToken unconditionally 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 bare except Exception handlers, so MissingClaimError degrades to discard-token-and-reauth, not a traceback. Machine tokens never reach get_token_claims, so enrollment is untouched.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactor
  • Other (please describe):

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 that silent_if_expired forgives expiry only. Without it, widening that except later 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 MissingClaimError before implementing.

End-to-end against a real safety auth login — see the comment below for the method and output.

Verification:

  • Full suite, py3.11 / Authlib 1.7.2 / joserfc 1.7.4: 892 passed, 7 skipped, 1 failed
  • The one failure is tests/integration/test_enroll.py::test_enroll_invalid_key_rejected, which fails identically on pristine main (env-dependent, unrelated)
  • Pinned floor joserfc 1.6.8 / py3.9: complete token accepted, no-exp token raises MissingClaimError
  • ruff check, ruff format --check, pyright clean

Checklist

  • Code is well-documented
  • Changelog is updated (if needed) — auto-generated by commitizen at bump
  • No sensitive information (e.g., keys, credentials) is included in the code
  • All PR feedback is addressed

Additional Notes

Two things from the same review, both out of scope here:

@Mighty303 Mighty303 self-assigned this Sep 4, 2026
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: cc7f6984-d700-459f-ac44-37d15626e38c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Mighty303 Mighty303 added the bug Indicates a problem that needs to be resolved. label Sep 4, 2026
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.
@Mighty303
Mighty303 force-pushed the fix/jwt-essential-claims branch from 283ffee to d78d972 Compare September 4, 2026 17:56
@safety-bot

safety-bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🚀 Artifacts — PR #915 by @Mighty303

Security notice: You are viewing pre-release CI artifacts from PR #915 by @Mighty303. These commands may execute code on your machine. Do NOT run them unless you have reviewed the PR diff and trust the source. The snippets include a confirmation prompt.

Download the wheel file and binaries with gh CLI or from the workflow artifacts.

📦 Install & Run

Pre-requisites

# Install uv if needed
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create and enter artifacts directory
mkdir artifacts && cd artifacts

Quick Test with Python Package

bash -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 follows

uvx safety-*-py3-none-any.whl auth status
uvx safety-*-py3-none-any.whl auth login
uvx safety-*-py3-none-any.whl scan

Note: You need to be logged in to GitHub to access the artifacts.

@Mighty303
Mighty303 requested review from yeisonvargasf and a balanced review from Copilot and removed request for yeisonvargasf September 4, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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, and exp.
  • 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.

@Mighty303

Mighty303 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end test with a real safety auth login

Why not the real IdP

auth.safetycli.com always stamps exp. You cannot obtain a malformed token from it, and if you could, that would be a far bigger problem than this PR. So the setup keeps everything real except which issuer is trusted.

safety/auth/constants.py resolves AUTH_SERVER_URL and SAFETY_PLATFORM_URL through get_config_setting (safety/constants.py:141), which reads the env var first. Pointing those at a local stand-in OIDC provider needs no code change. This is the same override already used to aim the CLI at beta.

What is real vs. stubbed

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Indicates a problem that needs to be resolved.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants