You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
**Beta** production-hardening release — security, performance, PyJWT/JWKS parity, and typing. No intentional breaking changes to the public `__all__` API beyond stricter defaults and new warnings.
- Stricter benchmark CI gates (≥75% vs PyJWT); expanded API/security docs
31
+
32
+
## Install
33
+
34
+
```bash
35
+
pip install oxyjwt==0.4.0
36
+
```
37
+
38
+
## Upgrade from 0.3.0
39
+
40
+
```bash
41
+
pip install -U oxyjwt
42
+
```
43
+
44
+
Review new warnings and stricter JWKS/JWT limits. See the full [changelog](https://github.com/QueryaHub/OxyJWT/blob/main/docs-site/docs/changelog.md#040--2026-05-22).
Copy file name to clipboardExpand all lines: CONTRIBUTING.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,6 +23,14 @@ The Python package lives under `python/`. Pytest is configured to prefer that tr
23
23
-`pytest` (with optional `pytest --cov=oxyjwt`)
24
24
-`mkdocs build --strict -f docs-site/mkdocs.yml` if you change documentation
25
25
26
+
## Branching (0.4.0)
27
+
28
+
Integration branch is **`dev`**. Feature work uses one branch per issue from `dev`, PRs target `dev`.
29
+
30
+
Always sync before branching: `git fetch --all --prune` then `git pull --ff-only origin dev`. Run the full Rust + pytest suite before opening a PR.
31
+
32
+
Use `gh` to set **labels** (`p0`/`p1`/`p2`, `security`, …) and milestone **0.4.0** on issues and PRs. See [docs/GITFLOW.md](docs/GITFLOW.md) and [milestone 0.4.0](https://github.com/QueryaHub/OxyJWT/milestone/1).
33
+
26
34
## Pull requests
27
35
28
36
- Keep changes focused and match existing style (formatting, typing, minimal comments).
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
OxyJWT is a Python JWT/JWS library backed by a Rust core. The public API follows **PyJWT** for `encode`, `decode`, `decode_complete`, JWK/JWKS helpers, and the `PyJWKClient`. When **signature verification** is enabled (the default), you must pass an `algorithms` allow-list, matching common PyJWT usage. Unverified decode is available only when you explicitly set `options["verify_signature"]` to `False` (treat the payload as untrusted).
4
4
5
-
This project is **beta** software on the `0.3.x` line; see the [changelog](docs-site/docs/changelog.md) for **0.2.0** breaking changes (exception hierarchy) and **0.3.0** notes.
5
+
This project is **beta** software on the `0.4.x` line; see the [changelog](docs-site/docs/changelog.md) for **0.2.0** breaking changes (exception hierarchy) and **0.4.0** production-hardening notes.
Copy file name to clipboardExpand all lines: RELEASING.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Releasing OxyJWT
2
2
3
-
This checklist is for maintainers publishing **0.3.x** (and later) to PyPI via the GitHub Actions [Release workflow](.github/workflows/release.yml).
3
+
This checklist is for maintainers publishing **0.4.x** (and later) to PyPI via the GitHub Actions [Release workflow](.github/workflows/release.yml).
4
4
5
5
## Before tagging
6
6
@@ -35,16 +35,16 @@ This checklist is for maintainers publishing **0.3.x** (and later) to PyPI via t
35
35
## Publish
36
36
37
37
1. Commit all release-prep changes on `main`.
38
-
2.Create and push an annotated tag (example for **0.3.0**):
38
+
2.Merge `dev` → `main`, then create and push an annotated tag (example for **0.4.0**):
39
39
40
40
```bash
41
-
git tag -a v0.3.0 -m "Release 0.3.0"
42
-
git push origin v0.3.0
41
+
git tag -a v0.4.0 -m "Release 0.4.0"
42
+
git push origin v0.4.0
43
43
```
44
44
45
45
3. The **Release** workflow builds wheels (Linux x86_64/aarch64, macOS, Windows) + sdist and publishes to PyPI (requires the `pypi` environment and [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)).
46
46
47
-
4. On GitHub, create a **Release** from the tag. Use the **0.3.0** section in `docs-site/docs/changelog.md` as the release notes body.
47
+
4. On GitHub, create a **Release** from the tag. Use [`.github/RELEASE_NOTES_v0.4.0.md`](.github/RELEASE_NOTES_v0.4.0.md) or the **0.4.0** section in `docs-site/docs/changelog.md` as the release notes body.
Copy file name to clipboardExpand all lines: SECURITY.md
+15-4Lines changed: 15 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,11 +4,11 @@
4
4
5
5
| Version line | Support |
6
6
| --- | --- |
7
-
|**0.3.x** (current) | Security fixes and patch releases |
8
-
|**0.2.x**| Best-effort backports only for critical issues |
9
-
|**0.1.x** and older | Unsupported |
7
+
|**0.4.x** (current) | Security fixes and patch releases |
8
+
|**0.3.x**| Best-effort backports only for critical issues |
9
+
|**0.2.x** and older | Unsupported |
10
10
11
-
Security fixes target the latest **0.3.x** release. See the [changelog](docs-site/docs/changelog.md) for release history.
11
+
Security fixes target the latest **0.4.x** release. See the [changelog](docs-site/docs/changelog.md) for release history.
12
12
13
13
## Reporting a vulnerability
14
14
@@ -31,3 +31,14 @@ This policy covers the `oxyjwt` Python package and its Rust extension (`_oxyjwt`
31
31
## JWT usage
32
32
33
33
OxyJWT implements JWT/JWS signing and verification. Correct use still depends on application choices (algorithm allow-lists, audience/issuer checks, secret management). Read the [Security](docs-site/docs/security.md) documentation before deploying to production.
34
+
35
+
### JWKS (`PyJWKClient`)
36
+
37
+
When loading keys from a JWKS URL: use HTTPS (`require_https=True` in production), cap response size (`max_bytes`), pass an explicit `algorithms` list to `get_signing_key_from_jwt` (checked before HTTP), and always verify tokens with `decode` after resolving the key. See [Security — JWKS](docs-site/docs/security.md#jwks-pyjwkclient) and the [API reference](docs-site/docs/api-reference.md#pyjwkclient).
38
+
39
+
### `verify_signature=False`
40
+
41
+
Decoding with `options["verify_signature"] = False` skips cryptographic verification. OxyJWT emits `InsecureDecodeWarning` in that case. Do not use unverified payloads for authorization.
42
+
43
+
- The `subject` parameter is ignored unless `verify_sub` is explicitly `True` (it defaults to `False` when the signature is not verified).
44
+
-`require` only checks claim **presence**, not that the token was signed by your trust anchor.
Copy file name to clipboardExpand all lines: docs-site/docs/api-reference.md
+133-5Lines changed: 133 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,7 +20,7 @@ Parameters:
20
20
-`payload`: JSON-compatible mapping to place in the token claims.
21
21
-`key`: raw HMAC secret or an `EncodingKey`.
22
22
-`algorithm`: signing algorithm. Defaults to `HS256`.
23
-
-`headers`: optional JWT header values such as `kid`and `typ`.
23
+
-`headers`: optional JWT header values such as `kid`, `typ`, and custom string parameters (for example `X-Request-Id`). See [Encoding — Headers](usage/encoding.md#headers).
24
24
25
25
Raises:
26
26
@@ -40,7 +40,7 @@ def decode(
40
40
detached_payload: bytes|None=None,
41
41
audience: str| Iterable[str] |None=None,
42
42
subject: str|None=None,
43
-
issuer: str|None=None,
43
+
issuer: str|Iterable[str] |None=None,
44
44
leeway: float| timedelta =0,
45
45
) -> dict[str, Any]: ...
46
46
```
@@ -56,15 +56,15 @@ Parameters:
56
56
-`algorithms`: required server-side allow-list when signature verification is on.
57
57
-`audience`: expected `aud` value or values.
58
58
-`subject`: expected `sub` value (passed to the native decoder when signature verification is on).
59
-
-`issuer`: expected `iss` value.
59
+
-`issuer`: expected `iss` value, or an iterable of allowed issuers (token `iss` must match one).
60
60
-`leeway`: clock tolerance in seconds or as a `timedelta`.
61
61
-`options`: validation switches (see below). Values from a `PyJWT(..., options=...)` instance are merged with per-call `options`.
62
-
-`detached_payload`: not supported; raises `NotImplementedError` if set.
62
+
-`detached_payload`: required when the protected header sets `b64` to `false` (RFC 7797 detached content). Pass the raw payload bytes used when the JWS was signed.
63
63
64
64
Supported `options` keys (booleans unless noted):
65
65
66
66
-`verify_signature` — verify the JWS signature (default `True`).
-`require_exp` — require an `exp` claim in the token
69
69
-`require` — list of claim names that must be present (may also be passed on the `PyJWT` instance)
70
70
@@ -145,6 +145,134 @@ class DecodingKey:
145
145
146
146
Use `DecodingKey` for verifying tokens.
147
147
148
+
## `PyJWKClient`
149
+
150
+
HTTP client for fetching a [JWKS](https://datatracker.ietf.org/doc/html/rfc7517) document and resolving signing keys by `kid`. Uses the Python standard library (`urllib`) only — no extra HTTP dependencies.
151
+
152
+
Typical flow with an identity provider that publishes rotating RSA/EC keys:
algorithms=["RS256"], # server-side allow-list; checked before HTTP
167
+
)
168
+
claims = oxyjwt.decode(
169
+
token,
170
+
signing_key.key,
171
+
algorithms=["RS256"],
172
+
audience="api",
173
+
issuer="https://auth.example.com",
174
+
)
175
+
```
176
+
177
+
See also the [FastAPI cookbook](cookbooks/fastapi-jwt.md) and [Security — JWKS](security.md#jwks-pyjwkclient).
178
+
179
+
### Two-tier caching
180
+
181
+
| Tier | Option | Default | Behavior |
182
+
|------|--------|---------|----------|
183
+
|**1 — JWK Set**|`cache_jwk_set`, `lifespan`| on, 300s | Caches the parsed JWKS JSON. Refetch when TTL expires or `get_jwk_set(refresh=True)`. |
184
+
|**2 — signing keys**|`cache_keys`, `max_cached_keys`| off, 16 | LRU of `PyJWK` objects by `kid` (no time expiry). Opt-in; matches PyJWT default. |
185
+
186
+
On `get_signing_key`, if `kid` is missing from the current set, the client refetches JWKS **once** and retries (key rotation). A second miss raises `KeyError` (no further HTTP retries in that call).
187
+
188
+
### Constructor
189
+
190
+
```python
191
+
classPyJWKClient:
192
+
def__init__(
193
+
self,
194
+
uri: str,
195
+
*,
196
+
cache_jwk_set: bool=True,
197
+
cache_keys: bool=False,
198
+
max_cached_keys: int=16,
199
+
timeout: float=30.0,
200
+
max_bytes: int=262_144,
201
+
require_https: bool=False,
202
+
headers: Mapping[str, Any] |None=None,
203
+
ssl_context: ssl.SSLContext |None=None,
204
+
lifespan: float=300.0,
205
+
) -> None: ...
206
+
```
207
+
208
+
| Parameter | Description |
209
+
|-----------|-------------|
210
+
|`uri`| JWKS endpoint URL (`http://` or `https://`). |
211
+
|`cache_jwk_set`| Cache the fetched JWKS document (tier 1). |
212
+
|`lifespan`| Seconds before tier-1 cache expires; must be > 0 when `cache_jwk_set=True`. |
|`require_https`| When `True`, reject non-HTTPS `uri` values. |
218
+
|`headers`| Extra request headers merged with `User-Agent` and `Accept: application/json`. |
219
+
|`ssl_context`|`ssl.SSLContext` for HTTPS (default `ssl.create_default_context()`). |
220
+
221
+
Raises `ValueError` for invalid `uri` / `max_bytes`; `TypeError` for invalid `ssl_context`; `PyJWKClientError` when `require_https` blocks the URI or `lifespan` is invalid.
Returns a `PyJWKSet` parsed from the endpoint. When `refresh=False` and tier-1 cache is valid, returns the cached set without HTTP. When `refresh=True`, always fetches a new document and updates the cache (clears tier-2 LRU entries).
230
+
231
+
Raises `PyJWKClientConnectionError` on network/timeout failures; `PyJWKClientError` on oversized body or invalid JSON shape.
232
+
233
+
### `get_signing_key`
234
+
235
+
```python
236
+
defget_signing_key(self, kid: str) -> PyJWK: ...
237
+
```
238
+
239
+
Returns the `PyJWK` for `kid`. Uses tier-1 cache, then tier-2 LRU if enabled. Refetches JWKS once on `KeyError` (rotation). Empty `kid` raises `PyJWKClientError`.
240
+
241
+
### `get_signing_key_from_jwt`
242
+
243
+
```python
244
+
defget_signing_key_from_jwt(
245
+
self,
246
+
jwt: str|bytes,
247
+
algorithms: list[str] |None=None,
248
+
) -> PyJWK: ...
249
+
```
250
+
251
+
Reads `kid` (and optionally `alg`) from the token header via `get_unverified_header`, then calls `get_signing_key`.
252
+
253
+
When `algorithms` is provided:
254
+
255
+
- empty list → `InvalidAlgorithmError`;
256
+
- header `alg` not in the list → `InvalidAlgorithmError`**before** any JWKS HTTP request;
257
+
-`none` → `InvalidAlgorithmError`.
258
+
259
+
Missing or empty `kid` → `PyJWKClientError`.
260
+
261
+
### Security notes
262
+
263
+
- Pass your server-side **`algorithms`** allow-list to `get_signing_key_from_jwt` and again to `decode` — do not trust the header `alg` alone. See [Security](security.md).
264
+
- Prefer **`require_https=True`** in production.
265
+
- Use **`max_bytes`** to limit denial-of-service from huge JWKS responses.
266
+
-`get_unverified_header` inside `get_signing_key_from_jwt` does not verify the signature; only use the returned `PyJWK.key` with verified `decode`. Details: [SECURITY.md](https://github.com/QueryaHub/OxyJWT/blob/main/SECURITY.md) and [Security — unverified helpers](security.md#treat-unverified-helpers-as-inspection-only).
267
+
- Encryption keys (`use: enc`) in the JWKS are rejected when building `PyJWK`; skipped entries emit `PyJWKSetSkipWarning`.
-`PyJWKClientConnectionError` — HTTP/URL errors (subclass of `PyJWKClientError`).
273
+
-`KeyError` — `kid` not found after one refresh attempt.
274
+
-`InvalidAlgorithmError` — disallowed `alg` when `algorithms` is passed to `get_signing_key_from_jwt`.
275
+
148
276
## Exceptions
149
277
150
278
All OxyJWT exceptions inherit from `OxyJWTError`. The layout matches PyJWT: `InvalidTokenError` is the common base for most decode-time errors; `DecodeError` and `InvalidSignatureError` nest under it.
Copy file name to clipboardExpand all lines: docs-site/docs/benchmarks.md
+22-1Lines changed: 22 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,25 @@
2
2
3
3
OxyJWT is optimized for throughput on typical JWT workloads. Numbers depend on CPU, compiler flags, Python version, and the cryptographic backend (see the root [README](https://github.com/QueryaHub/OxyJWT/blob/main/README.md) for the default `aws_lc_rs` backend and the `rust_crypto` alternative on some Linux aarch64 wheels).
4
4
5
+
## Three levels of benchmarking
6
+
7
+
| Level | How to run | Purpose |
8
+
|-------|------------|---------|
9
+
|**Smoke**| Default `pytest` (`tests/test_benchmark_jwt_libraries.py::test_benchmark_hs256_smoke_vs_competitors`) | Fast HS256 regression gate in every CI run; OxyJWT must stay ≥75% of PyJWT encode/decode throughput (was 25% / 4× slack). |
10
+
|**Extended**|`OXYJWT_BENCHMARK=1 pytest -m benchmark`| HS256, RS256, EdDSA with moderate iterations; optional locally or before release. |
11
+
|**Full**| Weekly [Benchmarks workflow](https://github.com/QueryaHub/OxyJWT/blob/main/.github/workflows/benchmarks.yml) or the script below | Higher iteration counts; Markdown artifact for trend spotting, not a hard gate. |
12
+
13
+
## Reference ratios (HS256 smoke parameters)
14
+
15
+
Measured on a typical Linux dev machine with `maturin develop --release`, 50 iterations, 1 round, warmup 8 (same as CI smoke):
CI asserts **≥75%** of PyJWT for both operations so large regressions fail without requiring absolute ops/s parity across runners.
23
+
5
24
## Running comparisons locally
6
25
7
26
The repository includes [`scripts/compare_jwt_libraries.py`](https://github.com/QueryaHub/OxyJWT/blob/main/scripts/compare_jwt_libraries.py), which benchmarks OxyJWT against PyJWT, Authlib, and python-jose where supported.
@@ -22,7 +41,9 @@ Raw JSON/Markdown outputs are gitignored; keep them local or attach them to rele
22
41
23
42
## CI artifacts
24
43
25
-
The [Benchmarks workflow](https://github.com/QueryaHub/OxyJWT/blob/main/.github/workflows/benchmarks.yml) runs on a weekly schedule and on manual dispatch. It uploads a Markdown summary as a workflow artifact (smaller iteration counts than a full local run). Use it to spot large regressions over time, not as absolute performance guarantees.
44
+
The [Benchmarks workflow](https://github.com/QueryaHub/OxyJWT/blob/main/.github/workflows/benchmarks.yml) runs on a weekly schedule and on `workflow_dispatch`. It uploads `benchmark-results/ci-bench.md` (HS256, RS256, EdDSA; 200 iterations × 2 rounds). Use it to spot large regressions over time, not as absolute performance guarantees.
45
+
46
+
Main [CI](https://github.com/QueryaHub/OxyJWT/blob/main/.github/workflows/ci.yml) enforces the HS256 smoke ratios on every push/PR.
0 commit comments