(No changes yet.)
Performance release. Verified decode is about 2.3× faster and encode about
1.2× faster than 0.6.0 on the HS256 hot path, with no change to any successful
decode or encode result. Measured locally with CPython 3.14 on Linux/x86-64;
absolute numbers vary by machine. The API remains pre-1.0 (Beta). See
Versioning and SECURITY.md on GitHub.
pip install -U oxyjwt- No intentional breaking changes to the public
__all__surface. - One edge-case behaviour change: a present but unparseable claim listed in
options["require"](for example{"exp": "not-a-number"}) now raisesDecodeErrorinstead ofMissingRequiredClaimError, matching PyJWT. - Absent and JSON
nullrequired claims still raiseMissingRequiredClaimError.
| Operation | 0.6.0 | 0.7.0 | Change |
|---|---|---|---|
decode |
6.08 µs | 2.64 µs | 2.30× faster |
decode_complete |
5.90 µs | 2.98 µs | 1.98× faster |
decode (large claims) |
15.18 µs | 9.60 µs | 1.58× faster |
decode (audience + issuer) |
7.12 µs | 5.50 µs | 1.29× faster |
encode |
1.68 µs | 1.44 µs | 1.17× faster |
- Single-pass verified decode. The native decode path now parses the header
once, parses the payload once and verifies the signature once. Previously the
underlying
jsonwebtoken::decodeparsed both segments a second time for its internal validation struct, and the returned header had to be re-serialized before it could be handed to Python. - Fast path for plain
decode/decode_complete. When nothing butalgorithmsis supplied, the options dictionary is no longer built, copied or re-read on either side of the FFI boundary, and Python runs only the claim checks that Rust does not already cover. Calls that passoptions,audience,issuer,subject, a non-zeroleeway,typordetached_payload, or that use aPyJWTinstance with non-default options, behave exactly as before. - Release profile. Wheels are now built with fat LTO, a single codegen unit and stripped symbols.
- RFC 7797 detached-JWS decoding no longer selects its exception type by matching on error message text.
- Header parse failures now report the same message as
get_unverified_headerfor the same token. The exception classes are unchanged.
MissingRequiredClaimErrornow names a deterministic claim. When several claims listed inoptions["require"]were absent, the one reported differed between processes.options["require"]treats a JSONnullas an absent claim, matching PyJWT.encodeno longer copies the payload when nodatetimeclaim needs rewriting, and never mutates the caller's dictionary.
- A claim listed in
options["require"]that is present but unparseable (for example{"exp": "not-a-number"}withrequire=["exp"]) now raisesDecodeErrorinstead ofMissingRequiredClaimError, matching PyJWT. Absent andnullclaims continue to raiseMissingRequiredClaimError.
sort_headersonencodehas never affected the emitted token: claim keys are serialized in sorted order either way. The flag is still accepted for PyJWT compatibility.
Security, JWKS hardening, and performance release: RFC 8725 token-type validation, JWKS refresh throttling, stricter RFC 7797 crit handling, thread-safe JWK materialization, and encode/decode hot-path improvements. The API remains pre-1.0 (Beta). See Versioning and SECURITY.md on GitHub.
pip install -U oxyjwt- No intentional breaking changes to the public
__all__surface. - New optional
typparameter ondecode/decode_completefor explicit token type validation (RFC 8725 §3.11). - New optional
refresh_cooldownonPyJWKClient(default0.0; opt in to rate-limit JWKS refresh). InsecureJWKSUriWarningwhen a JWKS URI uses unencrypted HTTP (RFC 8725 §3.10).- Stricter RFC 7797 validation: unsupported parameters in
critare rejected for detached JWS. PyJWKSetkid index no longer includes keys withuse: enc.
typparameter ondecode/decode_complete— validates the JWT headertypclaim to prevent token type confusion (RFC 8725 §3.11).PyJWKClient.refresh_cooldown— cooldown window between JWKS refreshes to mitigate kid-flooding DoS.- Single-flight coalescing and fine-grained locking in
PyJWKClientfor concurrent JWKS fetches.
- Thread-safe lazy key materialization in
PyJWKandPyJWKSet. PyJWKSetkid index excludesuse: encencryption keys.expexpiration boundary synchronized between Rust and Python validation.- Recursion depth / cycle limit in native
py_to_jsonconversion during encode.
InsecureJWKSUriWarningwhen JWKS URI uses unencrypted HTTP (RFC 8725 §3.10).- JWKS refresh cooldown mitigates repeated refresh under kid-flooding attacks.
- RFC 7797: reject unsupported parameters listed in
critfor detached JWS.
- Avoid
serde_json::ValueDOM tree allocation inencode_json. - Pre-allocate
PyListduringjson_to_boundclaim conversion. - Eliminate intermediate string allocation in JWS signing input parsing.
Performance and hardening release: faster encode/decode hot paths, JWKS concurrency fixes, stricter compact JWT validation, PyJWT-aligned issuer errors, and expanded security regression tests. The API remains pre-1.0 (Beta). See Versioning and SECURITY.md on GitHub.
pip install -U oxyjwt- No intentional breaking changes to the public
__all__surface. - Malformed compact JWTs (wrong segment count) are rejected consistently in Rust before decode.
detached_payloadis capped at 256 KiB (RFC 7797).- When
issuer=is passed, a token withoutissnow raisesInvalidIssuerError(PyJWT parity; previously could slip through on some paths).
issuer=always runs issuer validation in Python; missingissraisesInvalidIssuerErrorinstead of being skipped.PyJWKClientJWKS / signing-key cache is thread-safe under concurrentget_signing_key(lock around cache mutations).
detached_payloadsize capped at 256 KiB before attach (RFC 7797).- Compact JWT segment validation unified in Rust (reject extra/missing segments before
jwtparse). - Expanded
tests/test_security_regression.py— oversized JWT, detached cap,nonealg, concurrent JWKS client, issuer-without-iss.
- Verified decode: skip
get_unverified_headerunless empty payload segment (RFC 7797 detached form). - Unverified decode /
get_unverified_header: single native parse path (no double segment split). encode: use Rustencodedirectly when no customjson_encoder(noencode_jsonround-trip).decode_verified_complete: hold decoding key by reference; combine detach + verify in one Rust path.- Skip redundant Python
aud/iss/subvalidation when Rust already validated on verified decode. - RFC 7797 verified path:
exp/nbf/iatvalidated in Rust for detached tokens. PyJWK/PyJWKSet: lazyDecodingKeymaterialization; large JWKS sets avoid upfront parse of every key.
- Full CI runs on pushes to
dev(same gates as PRs). - PyPI Release workflow runs CI via
workflow_callbefore publishing. - HS256 smoke benchmark: 3 rounds, median ops/s vs PyJWT; gate remains ≥75%.
- Benchmarks: document smoke / extended / full tiers and PEM vs
cachedcompetitor key modes.
Production hardening release: security fixes, performance improvements, expanded PyJWT/JWKS parity, public typing stubs, and stricter CI. The API remains pre-1.0 (Beta). See Versioning and SECURITY.md on GitHub.
pip install -U oxyjwt- No intentional breaking changes to the public
__all__surface; behavior is stricter in several security-sensitive paths (see Security below). - New warnings:
InsecureDecodeWarningwhenverify_signature=False;PyJWKSetSkipWarningwhen JWKS entries are skipped. PyJWKClient.cache_keysdefaults toFalse(PyJWT parity); opt in for per-kidLRU caching.decode/decode_completeacceptissueras a string or iterable.strict_aud,detached_payload(RFC 7797 whenb64: false), and custom string JWT header fields onencode.- Public
.pyistubs andpy.typedfor IDE/mypy users.
tests/test_jwk_rsa_ec.py— RSA and EC JWK/JWKS parity tests with realn/eandcrv/x/yshapes.- RFC 7797 detached payload decode via
detached_payloadwhen the protected header setsb64tofalse. encodeaccepts custom string JWT header parameters beyondalg/typ/cty/kid, plus optional standard string JWS fields (jku,x5u,x5t,x5t#S256,url,nonce).- Public
.pyistubs forapi_jwt,jwk,jwks_client,jwk_exc, andwarnings;py.typedmarker for PEP 561. - Mypy CI job and
tests/typing/smoke checks for the public API surface.
PyJWKClient.get_signing_keyrefetches JWKS once whenkidis missing from the cached set (key rotation).decode/decode_completeacceptissueras a string or iterable (aligned with docs and Rust validation).
- HMAC secret buffers copied from Python are held in
Zeroizing<Vec<u8>>and cleared afterEncodingKey/DecodingKeyconstruction and ephemeralfrom_secretdecode paths. - Compact JWT strings larger than 256 KiB are rejected with
DecodeErrorbefore base64/JSON parsing (all decode and unverified entry points). InsecureDecodeWarningwhenverify_signatureisFalse; additional warnings whensubjectorrequireare used without signature verification.verify_suboption (PyJWT-aligned); Python-sidesubvalidation on the unverified decode path when enabled.PyJWKClientmax_bytes(default 256 KiB) and optionalrequire_httpsfor JWKS fetches.PyJWKClient.get_signing_key_from_jwtoptionalalgorithmsallow-list: rejects disallowed headeralgbefore JWKS lookup.PyJWKrejects JWKs withuse: enc(encryption keys) for signature verification paths.PyJWKSetemitsPyJWKSetSkipWarningwhen unusable JWK entries are skipped (index andkidin message).- Claim validation split documented: Rust validates
exp/nbfon verified decode; Python validatesiatand PyJWT-style audience/issuer/sub rules.verify_sub=Falseno longer validatessubin Rust whensubjectis passed. - Fractional
leewayon verified decode: Python validatesexp/nbfwith float semantics; Rust uses rounded whole seconds whenleewayis an integer. PyJWKSetO(1) lookup bykid;PyJWK.keyparsesDecodingKeylazily on first access.decode_unverified,get_unverified_header, andjws_parse_compactrelease the GIL during native JWS/JWT parsing.PyJWKClientaccepts optionalheadersandssl_context(PyJWT 2.8 subset).PyJWKClientlifespanTTL (default 300s) for cached JWKS whencache_jwk_set=True.strict_auddecode option for exact string audience matching (PyJWT parity).PyJWKClientcache_keysparameter (defaultFalse, PyJWT parity); per-kidLRU is opt-in.tests/test_security_regression.py— dedicated 0.4.0 security regression suite (always run in CI).- Benchmark CI smoke: HS256 OxyJWT vs PyJWT ratio gates tightened to ≥75% (was 25%); decode checked too. Docs describe smoke / extended / full workflows.
- Expanded
PyJWKClientAPI reference; JWKS security notes cross-linked fromSECURITY.mdandsecurity.md.
- Encode path passes
orjsonoutput as bytes into Rust (serde_json::from_slice), removing an extra UTF-8 decode andfrom_strparse. - Verified
decode_completeusesdecode_verified_completein Rust (onejwt_decodeparse) instead ofjws_parse_compactplus a second full decode. - Removed
orjsondumps/loads round-trip when normalizing decode claims and headers (_as_plain_dict).
Documentation, PyJWT parity, and operational polish. PyPI classifiers now mark the project as Beta; the API remains pre-1.0. See Versioning. Security reporting is described in SECURITY.md on GitHub.
pip install -U oxyjwtpulls the new runtime dependency orjson automatically; no code changes required for typicalencode/decodeusage.- New optional
subjectargument ondecode/decode_complete(PyJWT-compatible order:audience,subject,issuer). - No further breaking API changes beyond the 0.2.0 exception hierarchy; update
exceptclauses if you have not already (see Migration from PyJWT).
Added:
subjectparameter onPyJWT.decode/decode_complete(and module helpers), wired through to native verification.- LRU cache for
PyJWKClientsigning keys keyed bykid, bounded bymax_cached_keys. - Expanded optional PyJWT parity tests (
tests/test_parity_pyjwt.py). pytest-covcoverage report (XML + terminal) in CI.SECURITY.md,CONTRIBUTING.md, docs for versioning, benchmarks, and a FastAPI cookbook.- Scheduled benchmark workflow producing a Markdown artifact.
- Runtime dependency on orjson for JSON in the Python API layer (payload serialization for
encode, JWK/JWKS parsing, claim/header normalization). - Pytest benchmark smoke test (
tests/test_benchmark_jwt_libraries.py) that reusesscripts/compare_jwt_libraries.pyfor HS256 throughput vs competitors.
Changed:
- API reference and decoding guides now match
verify_signature/optionsbehavior from 0.2.0. - Exception hierarchy diagrams in docs corrected to match runtime (
InvalidTokenErroraboveDecodeError). - Pytest
pythonpath = ["python"]so local runs resolve the mixed-layout package consistently.
PyJWT compatibility release (API shape, JWK, JWKS client, and documentation). Breaking change: the exception class hierarchy is aligned with PyJWT (InvalidTokenError as the common base for most token and claim errors; DecodeError and InvalidSignatureError nest under it). Code that relied on the previous nesting order may need to update except clauses. See the Migration from PyJWT page for details.
Added:
- Module-level
PyJWT,encode,decode, anddecode_completematching common PyJWT usage, includingjson_encoder,sort_headers, andleeway/timedeltahandling. PyJWKandPyJWKSetbuilt onDecodingKey::from_jwk, plusPyJWKClient(JWKS over HTTP via the standard library).InvalidIssuedAtErrorand claim validation foriatin the Python layer when enabled.encode_jsonandjws_parse_compactin the native module for the Python JWT layer.- Parity and regression tests; optional comparison tests against the
jwtpackage when installed. - Python 3.14 Trove classifier and CI job.
Changed:
decodewithoptions["verify_signature"] = Falseskips JWS verification and no longer requires analgorithmslist for that path (treat unverified tokens as untrusted). Default remains signature verification on.- Native
leewayis expressed in seconds as a float, consistent with PyJWT. - Stubs (
_oxyjwt.pyi) updated for the new symbols and exception layout.
Initial alpha release.
Added:
- PyJWT-like
encodeanddecodeAPI. - Explicit
algorithmsallow-list for decoding. - HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithm support.
- Typed
EncodingKeyandDecodingKeyconstructors. - Unverified inspection helpers.
- OxyJWT exception hierarchy.
- Rust unit tests and Python pytest coverage.
- MkDocs documentation.
Security defaults (0.1.0; see 0.2.0 for unverified decode):
alg="none"is rejected.- Raw
strandbyteskeys are accepted only for HMAC algorithms. - Mixed algorithm families are rejected for one decode call.
- In 0.1.0,
verify_signature=Falsewas rejected indecode(0.2.0 allows an explicit unverified path; still unsafe for trusted claims without a verified signature).