Skip to content

refactor(auth): migrate JOSE/JWT calls from authlib.jose to joserfc - #908

Open
Mighty303 wants to merge 9 commits into
mainfrom
feat/migrate-jose-to-joserfc
Open

refactor(auth): migrate JOSE/JWT calls from authlib.jose to joserfc#908
Mighty303 wants to merge 9 commits into
mainfrom
feat/migrate-jose-to-joserfc

Conversation

@Mighty303

@Mighty303 Mighty303 commented Aug 18, 2026

Copy link
Copy Markdown

What

Migrate the JOSE/JWT surface from authlib.jose / authlib.oidc.core to joserfc. Stops the AuthlibDeprecationWarning: authlib.jose module is deprecated that authlib 1.7+ prints on CLI startup, and removes the structural blocker to a future authlib 1.7 bump.

Also raises the Authlib floor to >=1.6.12,<2.0: per Safety's vulnerability database, 1.2.01.6.11 are insecure and 1.6.12 is the first secure release. Lockfile changes are out of scope (uv.lock is gitignored in this repo).

Changes

  • safety/utils/tokens.pyget_token_claims now returns the joserfc Token (Token | None); read the payload via .claims. Decode path: KeySet.import_key_set(jwks)jwt.decode(..., algorithms=["RS256","PS256"]) → per-call JWTClaimsRegistry().validate(...). The ["RS256","PS256"] allowlist blocks the HS* alg-confusion path. silent_if_expired semantics unchanged.
  • safety/auth/oauth2.py — drop unused CodeIDToken import; get_claims_for now returns Token | None.
  • safety/auth/main.py — import ExpiredTokenError from joserfc.errors; raise ExpiredTokenError("exp") (joserfc requires the claim positional arg); get_id_token_claims returns decoded.claims, so is_email_verified still receives a dict.
  • safety/config/auth.py, safety/auth/cli.py — read claims via .claims and guard with is None (a Token is always truthy).
  • pyproject.toml — add joserfc>=1.6.8 (fixes GHSA-5jhw-7jv7-qcqq padded-JWT malleability; also the last release supporting Python 3.9, so 3.9 resolves to 1.6.8 and 3.10+ to 1.7.4, both secure). Raise Authlib>=1.2.0>=1.6.12,<2.0 (security floor).

Test plan

  • pytest tests/auth tests/config tests/utils/test_tokens.py — 354 passed, 1 skipped, 1 xfailed
  • Runtime check: real RSA JWKS + signed JWT through get_token_claims — valid decode, expired-silent returns claims, expired-non-silent raises ExpiredTokenError, bad token_type raises ValueError
  • Imports clean under python -W error::DeprecationWarning with authlib 1.7.2 installed (deprecation gone)
  • No residual authlib.jose / authlib.oidc imports in safety/
  • Authlib 1.6.12 reported secure; 1.2.0 / 1.6.11 insecure (Safety vuln DB)
  • CI passes

@Mighty303 Mighty303 self-assigned this Aug 18, 2026
@Mighty303
Mighty303 requested a balanced review from Copilot August 18, 2026 21:00
@coderabbitai

coderabbitai Bot commented Aug 18, 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: Pro Plus

Run ID: 23dd70e8-0c9d-4322-a063-1ef03b6c5dfd

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.

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.

Pull request overview

Migrates JWT validation from deprecated Authlib JOSE APIs to joserfc.

Changes:

  • Replaces JWT decoding and claims validation APIs.
  • Updates token claim return types and expiration errors.
  • Adds joserfc as a dependency.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
safety/utils/tokens.py Migrates JWT decoding and validation.
safety/auth/oauth2.py Updates claim types and imports.
safety/auth/main.py Uses joserfc expiration errors.
pyproject.toml Adds the joserfc dependency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread safety/utils/tokens.py Outdated
Comment thread pyproject.toml Outdated
Comment thread safety/utils/tokens.py Outdated
@Mighty303

Mighty303 commented Aug 18, 2026

Copy link
Copy Markdown
Author

Testing the joserfc migration + algorithm allowlist

This repo ships via PyApp (embeds a standalone Python and installs the wheel + deps), not PyInstaller, so joserfc is installed as a normal package — no frozen-bundle "hidden import" risk for its dynamic JWK/JWS modules.

Re-verified at 540be2d: JWTClaimsRegistry is now built per decode (no module-level reuse), and the floor is joserfc>=1.1.0 — the first release that ships UnsupportedAlgorithmError, which the alg-confusion test asserts.

CI (runs automatically on every push)

  • build-preview builds the binary for every target (linux x86_64 / aarch64 / musl, windows x86_64, macOS arm64 / x86_64) and runs smoke_test_binary.sh on each. Test 3 (safety auth --help, hard-fail) imports auth/cli.pyutils/tokens.pyjoserfc at module load, so a missing or broken joserfc fails the build.
  • Full test matrix (3.9–3.14, all pydantic/typer/click variants) exercises tests/utils/test_tokens.py.

Decode path (verified at source and in the built wheel)

  • tests/utils/test_tokens.py (5 cases) exercises the real joserfc decode: valid decode, expired with silent_if_expired, expired raising ExpiredTokenError, invalid token_type, and an HS256 alg-confusion forgery rejected with UnsupportedAlgorithmError (the ["RS256","PS256"] allowlist). All 5 pass at the joserfc==1.1.0 floor.
  • Built wheel (Requires-Dist: joserfc>=1.1.0): the same 5 decode cases pass against the installed wheel, loaded from site-packages rather than the source tree.

Live OAuth login (real IdP id_token, through the migrated code)

Captured during the original migration verification (isolated config dir). The change since then only moved JWTClaimsRegistry construction into the call and lowered the floor, so the single-decode result is unchanged. The PKCE callback decoded the real id_token via get_token_claimsjwt.decode(..., algorithms=["RS256","PS256"]), and the identity resolved from the decoded claims:

martinwong@Martins-MacBook-Pro ~ % safety auth login

Redirecting your browser to log in; once authenticated, return here to start using Safety
https://platform.safetycli.com/cli/auth?response_type=code&client_id=AWnwFBMr9DdZbxbDwYxjm4Gb24pFTnMp&...&code_challenge_method=S256&port=59482

You're authenticated
 Account: Martin Wong, martin@safetycli.com

martinwong@Martins-MacBook-Pro ~ %

auth status re-decodes the same token cleanly: Authenticated as martin@safetycli.com.

A real production id_token decodes end-to-end through joserfc with the algorithm allowlist. ✅

@safety-bot

safety-bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🚀 Artifacts — PR #908 by @Mighty303

Security notice: You are viewing pre-release CI artifacts from PR #908 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 #908 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 32321332296 -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 force-pushed the feat/migrate-jose-to-joserfc branch from cca3ee8 to e554b0a Compare August 18, 2026 21:16
Mighty303 added a commit that referenced this pull request Aug 18, 2026
Addresses the Copilot review on PR #908 and the pyright failure surfaced
once ruff went clean:

- tokens.py: restore `# type: ignore` on KeySet.import_key_set. jwks is
  dict[str, Any] but import_key_set wants KeySetSerialization; the original
  authlib decode line carried the same ignore and the migration dropped it.
- pyproject: raise joserfc floor 1.0.0 -> 1.3.0. The module-level
  JWTClaimsRegistry is reused across calls, and joserfc <1.3.0 freezes `now`
  at construction (verified in 1.0.0/1.2.1 source: `now = int(time.time())`),
  so a long-running process would compare every exp against import time and
  accept expired tokens. 1.3.0+ reads the clock per validate() call.
- tests/utils/test_tokens.py: real JWKS+JWT decode coverage (valid, expired
  silent/non-silent, bad token_type). Existing auth/config tests mock
  get_token_claims, so the decode path had no direct coverage.
@Mighty303 Mighty303 added the dependencies Pull requests that update a dependency file label Aug 18, 2026
Replace authlib.jose / authlib.oidc.core usage with joserfc so the
deprecation warning authlib 1.7+ emits ("authlib.jose module is
deprecated, please use joserfc instead") no longer fires, and the repo
is unblocked from a future authlib 1.7 bump.

- utils/tokens.py: KeySet.import_key_set + jwt.decode + a reused
  JWTClaimsRegistry().validate; return type narrows to
  Optional[Dict[str, Any]] (all callers use dict-style access).
- auth/main.py: import ExpiredTokenError from joserfc.errors; the manual
  raise becomes raise ExpiredTokenError("exp") (joserfc requires the
  claim positional arg).
- auth/oauth2.py: drop the unused CodeIDToken import; retype the return.
- pyproject.toml: add joserfc>=1.0.0. Authlib>=1.2.0 is unchanged (the
  version bump belongs to a separate CVE PR).
The lint job lints changed files whole, and these three carried pre-existing
ruff violations that surfaced once the migration touched them. Clear them
without changing behavior:

- add `from __future__ import annotations` (clears FA100 and makes the modern
  annotation forms runtime-safe on 3.9)
- modernize annotations (UP006/UP035/UP045): Dict/Tuple -> dict/tuple,
  Optional/Union -> X | None; sort imports (I001); collapse a nested if (SIM102)
- `# noqa: BLE001` on two pre-existing intentional catch-alls in get_auth_info
  (narrowing them would change auth-refresh behavior)

No runtime behavior change. tests/auth + tests/config: 350 passed.
Addresses the Copilot review on PR #908 and the pyright failure surfaced
once ruff went clean:

- tokens.py: restore `# type: ignore` on KeySet.import_key_set. jwks is
  dict[str, Any] but import_key_set wants KeySetSerialization; the original
  authlib decode line carried the same ignore and the migration dropped it.
- pyproject: raise joserfc floor 1.0.0 -> 1.3.0. The module-level
  JWTClaimsRegistry is reused across calls, and joserfc <1.3.0 freezes `now`
  at construction (verified in 1.0.0/1.2.1 source: `now = int(time.time())`),
  so a long-running process would compare every exp against import time and
  accept expired tokens. 1.3.0+ reads the clock per validate() call.
- tests/utils/test_tokens.py: real JWKS+JWT decode coverage (valid, expired
  silent/non-silent, bad token_type). Existing auth/config tests mock
  get_token_claims, so the decode path had no direct coverage.
@Mighty303
Mighty303 force-pushed the feat/migrate-jose-to-joserfc branch from 683848f to 1b65140 Compare August 18, 2026 22:33
get_token_claims called jwt.decode without an algorithms allowlist, so
joserfc's default accepted any recommended algorithm including HS256. An
attacker could HMAC-sign a token with the public JWKS key and pass
verification (alg-confusion). Pin decoding to the asymmetric algorithms
the IdP issues (RS256/PS256) and reject the rest.

Adds test_alg_confusion_hs256_is_rejected: it forges an HS256 token whose
HMAC secret is the RSA public key and asserts UnsupportedAlgorithmError.
Drop the module-level JWTClaimsRegistry singleton and build a fresh
instance per decode. A reused registry froze `now` at construction on
joserfc <1.3.0 (a long-running process would accept expired tokens),
which was the only reason for the 1.3.0 floor. Per-call construction
reads the clock each time, so lower the floor to joserfc>=1.1.0 — the
version that introduces UnsupportedAlgorithmError, which the
alg-confusion test asserts. Below 1.1.0 that test cannot even import.
@Mighty303
Mighty303 force-pushed the feat/migrate-jose-to-joserfc branch from 132e6a6 to 540be2d Compare August 19, 2026 17:19
Authlib 1.2.0 through 1.6.11 are flagged insecure by Safety's
vulnerability database; 1.6.12 is the first secure release. Raise the
lower bound to >=1.6.12 and cap below the next major (<2.0) to avoid an
unvetted Authlib 2.0.
get_token_claims now returns the joserfc Token (payload via .claims)
instead of the bare claims dict. Callers read fields through .claims and
guard with `is None` (a Token is always truthy, so `if not x` would be
wrong). Test mocks wrap their claims dict in a Token via a small helper.
The Token return required editing cli.py, config/auth.py, and
test_machine_credential.py, which pulls them into the PR's changed-file
lint scope. Their pre-existing FA100/UP006/UP035/I001/SIM117/DTZ005 debt
is out of scope for this migration: modernizing it would change Typer's
runtime annotation resolution (cli.py) and break the runtime cast()
calls on Python 3.9 (config/auth.py). Suppress those rules per file with
a rationale and defer modernization to its own PR.

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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread pyproject.toml Outdated
joserfc <1.6.8 accepts padded JWT encodings (GHSA-5jhw-7jv7-qcqq),
making tokens malleable. 1.6.8 is the fix and the last release that
still supports Python 3.9 (the 1.7 line dropped 3.9). With >=1.6.8,
Python 3.9 resolves to 1.6.8 and 3.10+ resolves to 1.7.4, both secure.
@Mighty303

Copy link
Copy Markdown
Author

Why joserfc>=1.6.8

1.6.8 is the lowest joserfc release with no known vulnerabilities and the last release that still supports Python 3.9. A higher floor would drop 3.9; a lower one would permit a known CVE.

Version status

joserfc Vuln-free? Python 3.9? What installs here
1.1.0 – 1.6.7 excluded by the floor
1.6.8 resolved on Python 3.9
1.7.1 – 1.7.4 ❌ (3.10+) resolved on Python 3.10+ (1.7.4)

Vuln status is from Safety's database; 3.9 support was confirmed by installing each version under Python 3.9 (1.7.x is unsatisfiable there).

The advisories that set the boundary

Every release ≤ 1.6.7 is affected by at least one open advisory; the last fix landed in 1.6.8:

Advisory Severity Affected Fixed in
CVE-2026-49852 — HS256/384/512 verify accepts an empty HMAC key High ≤ 1.6.7 1.6.8
CVE-2026-48990b64=false payload-size-limit bypass Medium 1.3.4 – 1.6.6 1.6.7
CVE-2026-27932 — PBES2 p2c unbounded iteration DoS High < 1.6.3 1.6.3
CVE-2025-65015 — large-payload logging DoS Critical 1.3.3 – 1.3.4, 1.4.0 – 1.4.1 1.3.5 / 1.4.2

CVE-2026-49852 is the one that pins the floor: its affected range is everything ≤ 1.6.7, so 1.6.8 is the minimal version clear of all four. We don't accept the HS* family (_ALLOWED_ALGORITHMS = ["RS256", "PS256"]), so that specific issue isn't reachable through our decode path, but flooring at the patched release keeps the dependency scan clean.

Why not a higher floor (e.g. >=1.7.2)

joserfc dropped Python 3.9 in the 1.7 line, so joserfc>=1.7.2 is unsatisfiable on 3.9 and would make Safety uninstallable on a supported interpreter. >=1.6.8 keeps 3.9 working (resolves to 1.6.8) while 3.10+ still gets the latest secure release (1.7.4).

Note: the advisory ID in the earlier review comment (GHSA-5jhw-7jv7-qcqq) does not resolve on GitHub; the real fix-at-1.6.8 advisory is GHSA-gg9x-qcx2-xmrh.

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

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants