Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
688904d
security(jws): reject unrecognized crit header extensions on verified…
ZhuchkaTriplesix Oct 16, 2026
e97d23f
security(api): validate crit header in unverified decode
ZhuchkaTriplesix Oct 16, 2026
2dc9e2b
test(security): cover crit header rejection on verified and unverifie…
ZhuchkaTriplesix Oct 16, 2026
92e2155
Merge pull request #141 from QueryaHub/issue/132-general-crit-rejection
ZhuchkaTriplesix Oct 16, 2026
8ba7192
perf(claims): enforce strict aud/iss/sub validation in rust
ZhuchkaTriplesix Oct 16, 2026
63e538d
perf(api): handle audience, issuer and subject in decode fast path
ZhuchkaTriplesix Oct 16, 2026
8210250
test(decode): add fast path coverage for aud, iss, and sub
ZhuchkaTriplesix Oct 16, 2026
7eefb1b
Merge pull request #142 from QueryaHub/issue/136-fast-path-audience-i…
ZhuchkaTriplesix Oct 16, 2026
03a18f7
perf(claims): stream deserialize claims to PyDict without intermediat…
ZhuchkaTriplesix Oct 16, 2026
4f0c7c5
test(claims): add regression test for complex claims streaming deseri…
ZhuchkaTriplesix Oct 16, 2026
b1d68f8
Merge pull request #143 from QueryaHub/issue/137-claims-deserializati…
ZhuchkaTriplesix Oct 16, 2026
c94c656
perf(jws): avoid full JSON header parsing in decode when only alg is …
ZhuchkaTriplesix Oct 16, 2026
635e907
test(decode): add header error parity regression tests
ZhuchkaTriplesix Oct 16, 2026
2124540
Merge pull request #144 from QueryaHub/issue/138-zero-copy-header-alg
ZhuchkaTriplesix Oct 16, 2026
dce4b4e
perf(api): avoid redundant payload copy in encode_json and optimize p…
ZhuchkaTriplesix Oct 16, 2026
739ae67
Merge pull request #145 from QueryaHub/issue/139-encode-alloc-and-det…
ZhuchkaTriplesix Oct 17, 2026
4991dd8
perf(keys): eliminate triple JSON round-trip in DecodingKey.from_jwk
ZhuchkaTriplesix Oct 17, 2026
74242f4
Merge pull request #146 from QueryaHub/issue/140-jwk-direct-deseriali…
ZhuchkaTriplesix Oct 17, 2026
ee5bed8
docs: document encode_json, JWK fast-paths, crit enforcement, and upd…
ZhuchkaTriplesix Oct 17, 2026
0b68954
Merge pull request #147 from QueryaHub/docs/update-library-documentation
ZhuchkaTriplesix Oct 17, 2026
9403090
feat(api): export encode_json in public API and PyJWT class
ZhuchkaTriplesix Oct 17, 2026
8eff14e
test(api): add comprehensive unit and typing tests for encode_json
ZhuchkaTriplesix Oct 17, 2026
5ece3ea
Merge pull request #149 from QueryaHub/issue/148-export-encode-json
ZhuchkaTriplesix Oct 17, 2026
83ee257
perf(core): pre-encode static headers and move claim validations to Rust
ZhuchkaTriplesix Oct 17, 2026
cb3746b
test(core): add regression tests for static headers and rust claim va…
ZhuchkaTriplesix Oct 17, 2026
76633f5
docs: document static headers and native claim validation in changelog
ZhuchkaTriplesix Oct 17, 2026
ac8b0e2
Merge pull request #151 from QueryaHub/issue/150-static-headers-rust-…
ZhuchkaTriplesix Oct 17, 2026
967cd6b
chore(release): prepare 0.9.0
ZhuchkaTriplesix Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/RELEASE_NOTES_v0.9.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# OxyJWT 0.9.0

**Beta** performance and security release — zero-copy streaming claims deserialization, pre-encoded static headers, native fast-path claim validation in Rust, zero-copy header algorithm extraction, direct JWK deserialization, RFC 7515/7797 strict `crit` header enforcement, and public top-level `encode_json` export.

## Highlights

### Security

- **Strict rejection of unrecognized `crit` (Critical) header parameters** — Under RFC 7515 §4.1.11 and RFC 7797, any critical header extensions that are unrecognized, malformed, empty, or targeting reserved claims are rejected immediately on both verified and unverified decode paths (#132).

### Performance

- **Pre-encoded static headers and native claim validation** — Standard default headers (`{"typ":"JWT","alg":"..."}`) use pre-encoded immutable base64 slices in `encode` and `encode_json`, eliminating header JSON serialization and base64 allocation. Fast-path `decode` now validates `iat`, unrequested `aud`, and `sub` entirely in Rust, eliminating secondary Python validation passes. HS256 encode benchmark reached **~895,000 ops/s** (#150).
- **Direct JWK deserialization** — Materializing a decoding key from a JWK dictionary now avoids intermediate `serde_json::Value` DOM trees; parsed in a single pass via `orjson.dumps` and streaming `jsonwebtoken::jwk::Jwk` parsing. Speedup: 10× for octet/HMAC keys (0.97 µs vs 9.66 µs) and 21× for RSA keys (1.21 µs vs 25.48 µs) (#140).
- **Zero-copy header algorithm resolution** — Standard compact headers are resolved via static string matching without base64 or JSON decoding; non-standard headers extract `alg` via a streaming serde visitor without allocating map structures (#138).
- **Streaming claims deserialization into `PyDict`** — Deserialization in `decode_complete` directly streams serde JSON tokens into Python `PyDict` without building intermediate Rust DOM trees (#137).
- **In-Rust validation for `aud`, `iss`, and `sub`** — Standard claim checks execute natively during fast-path decoding (#136).
- **Payload borrowing in `encode_json` and GIL optimization** — JWS signing inputs are built directly from borrowed slices; non-cryptographic sub-microsecond helpers run attached to the GIL to eliminate `py.detach` thread context switching overhead (#139).

### Added

- **Top-level `encode_json` export** — `encode_json` is exported directly on `oxyjwt.encode_json` and `PyJWT.encode_json` with complete typing and parity tests (#148).
- `DecodingKey.from_jwk`, `PyJWK`, and `PyJWKSet.from_json` now accept raw UTF-8 `bytes` directly in addition to `str` and `Mapping[str, Any]` (#140).

## Install

```bash
pip install oxyjwt==0.9.0
```

## Upgrade from 0.8.0

```bash
pip install -U oxyjwt
```

- No breaking changes to public APIs.
- Full parity with PyJWT 2.x maintained across all algorithms and options.

See the full [changelog](https://github.com/QueryaHub/OxyJWT/blob/main/CHANGELOG.md#090--2026-10-17).
59 changes: 57 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

(No changes yet.)
## [0.9.0] — 2026-10-17

### Security

- **Strict rejection of unrecognized `crit` (Critical) header parameters.**
Under RFC 7515 §4.1.11 and RFC 7797, any header parameter listed in the `crit`
header array must be understood and processed. OxyJWT now strictly validates
`crit` on both verified and unverified decode paths: unrecognized critical
extensions, empty `crit` lists, malformed structures, or critical references
to standard reserved headers immediately raise `DecodeError` (#132).

### Performance

- **Pre-encoded static base64 headers and native fast-path claim verification.**
In `encode` and `encode_json`, tokens with standard default headers now use
pre-encoded immutable base64 header slices, eliminating runtime `Header` JSON
serialization and base64 allocation. In `decode`, standard claim validations
(`iat`, unrequested `aud` rejection, and `sub` type checks) now execute entirely
within the Rust core, allowing the fast-path decode to return the claims dictionary
directly without redundant Python-side re-checks (#150).
- **Eliminate triple JSON round-trip in `DecodingKey.from_jwk`.**
Materializing a decoding key from a JWK Python dictionary now bypasses intermediate
`serde_json::Value` DOM allocations and re-serialization. The dictionary is
serialized directly in C via `orjson.dumps`, and parsed into `jsonwebtoken::jwk::Jwk`
in a single pass. `DecodingKey.from_jwk` also accepts `&str`, `&[u8]` (`bytes`),
and `bytearray` natively with zero-copy deserialization. Benchmark speedup:
10× faster for octet/HMAC keys (0.97 µs vs 9.66 µs) and 21× faster for RSA keys
(1.21 µs vs 25.48 µs) (#140).
- **Avoid redundant payload copying in `encode_json` and trim `py.detach` overhead.**
In `encode_json`, the JWS signing input is now built directly from the borrowed
payload byte slice without an owned `payload_bytes.to_vec()` heap clone.
Non-cryptographic sub-microsecond paths (`get_unverified_header` and
`decode_unverified`) now run synchronously attached to the GIL, eliminating
repeated `PyEval_SaveThread` / `PyEval_RestoreThread` cycles (#139).
- **Zero-copy header algorithm extraction & static header fast-path.**
Decode paths avoid full JSON DOM parsing when only the algorithm is needed.
Standard compact headers (`{"typ":"JWT","alg":"..."}`, `{"alg":"...","typ":"JWT"}`,
`{"alg":"..."}`) are recognized in a single string match without base64 or JSON
decoding. Non-standard headers extract `alg` via a streaming serde visitor
without allocating a `serde_json::Map<String, Value>` (#138).
- **Streaming claims deserialization into `PyDict`.**
Deserialization of claims in `decode_complete` directly streams serde JSON tokens
into a `PyDict` without allocating an intermediate `serde_json::Value` tree (#137).
- **Fast-path native validation for `aud`, `iss`, and `sub`.**
Extended the in-Rust fast-path decoder to validate audience, issuer, and subject
claims directly in Rust, reducing Python fallback overhead (#136).

### Added

- **Top-level `encode_json` export with full typing and parity coverage.**
`encode_json` is now exported in the public API root (`oxyjwt.encode_json`) and on
`PyJWT.encode_json`, allowing callers who already serialize payloads to JSON
(such as via orjson) to avoid redundant dictionary deserialization (#148).
- `DecodingKey.from_jwk`, `PyJWK`, and `PyJWKSet.from_json` now accept raw UTF-8
`bytes` directly in addition to `str` and `Mapping[str, Any]`.

## [0.8.0] — 2026-09-28

Expand Down Expand Up @@ -425,7 +479,8 @@ Initial alpha release.
- Mixed algorithm families are rejected for one decode call.
- In 0.1.0, `verify_signature=False` was rejected in `decode` (0.2.0 allows an explicit unverified path).

[Unreleased]: https://github.com/QueryaHub/OxyJWT/compare/v0.8.0...HEAD
[Unreleased]: https://github.com/QueryaHub/OxyJWT/compare/v0.9.0...HEAD
[0.9.0]: https://github.com/QueryaHub/OxyJWT/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/QueryaHub/OxyJWT/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/QueryaHub/OxyJWT/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/QueryaHub/OxyJWT/compare/v0.5.0...v0.6.0
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ print(token_data["signature"])

---

### 5. Pre-Serialized High-Throughput Encoding (`encode_json`)

For high-throughput pipelines where payloads are already serialized as JSON bytes or strings:

```python
import orjson
import oxyjwt

payload_bytes = orjson.dumps({"sub": "user_42", "exp": 1893456000})
token = oxyjwt.encode_json(payload_bytes, "secret", algorithm="HS256")
```

---

## 🛡️ Exception Hierarchy

OxyJWT provides a predictable exception tree that mirrors PyJWT for easy catch blocks:
Expand Down
70 changes: 68 additions & 2 deletions docs-site/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ Raises:
- `InvalidAlgorithmError` for unsupported algorithms or `alg` header overrides.
- `InvalidKeyError` for invalid key material.

## `encode_json`

```python
def encode_json(
payload_json: str | bytes,
key: str | bytes | EncodingKey,
algorithm: str = "HS256",
headers: Mapping[str, Any] | None = None,
) -> str: ...
```

High-performance signing variant that accepts an already-serialized JSON payload as `str` or `bytes`. Bypasses Python dictionary serialization and reflection. For HMAC algorithms, the borrowed byte slice is signed directly without heap cloning.

Raises the same exceptions as `encode`.

## `decode`

```python
Expand Down Expand Up @@ -140,10 +155,61 @@ class DecodingKey:
def from_ed_pem(pem: str | bytes) -> DecodingKey: ...

@staticmethod
def from_jwk(jwk: str | Mapping[str, Any]) -> DecodingKey: ...
def from_jwk(jwk: str | bytes | Mapping[str, Any]) -> DecodingKey: ...
```

Use `DecodingKey` for verifying tokens. `from_jwk` accepts a Python dictionary, JSON string, or UTF-8 JSON bytes (with zero-copy single-pass parsing).

## `PyJWK`

```python
class PyJWK:
def __init__(
self,
jwk: Mapping[str, Any] | str | bytes,
algorithm: str | None = None,
) -> None: ...

@staticmethod
def from_dict(obj: Mapping[str, Any], algorithm: str | None = None) -> PyJWK: ...

@staticmethod
def from_json(data: str | bytes, algorithm: str | None = None) -> PyJWK: ...

@property
def key(self) -> DecodingKey: ...

@property
def key_id(self) -> str | None: ...

@property
def key_type(self) -> str | None: ...

@property
def public_key_use(self) -> str | None: ...
```

Represents a single parsed JSON Web Key. The underlying `DecodingKey` is lazily materialized when `.key` is first accessed. Keys with `use: enc` are rejected with `PyJWKError`.

## `PyJWKSet`

```python
class PyJWKSet:
def __init__(self, keys: list[dict[str, Any]]) -> None: ...

@staticmethod
def from_dict(obj: dict[str, Any]) -> PyJWKSet: ...

@staticmethod
def from_json(data: str | bytes) -> PyJWKSet: ...

@property
def keys(self) -> list[PyJWK]: ...

def __getitem__(self, kid: str) -> PyJWK: ...
```

Use `DecodingKey` for verifying tokens.
Represents a set of JWKs (RFC 7517). Supports fast $O(1)$ key lookup by `kid` via `keyset[kid]`. Keys designated for encryption are automatically skipped with `PyJWKSetSkipWarning`.

## `PyJWKClient`

Expand Down
61 changes: 60 additions & 1 deletion docs-site/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,66 @@

## Unreleased

(No changes yet.)
## 0.9.0 — 2026-10-17

Performance and security release: zero-copy streaming claims deserialization,
pre-encoded static headers, native fast-path claim validation in Rust, zero-copy
header algorithm extraction, direct JWK deserialization, RFC 7515/7797 strict crit
header enforcement, and public top-level `encode_json` export.

### Security

- **Strict rejection of unrecognized `crit` (Critical) header parameters.**
Under RFC 7515 §4.1.11 and RFC 7797, any header parameter listed in the `crit`
header array must be understood and processed. OxyJWT now strictly validates
`crit` on both verified and unverified decode paths: unrecognized critical
extensions, empty `crit` lists, malformed structures, or critical references
to standard reserved headers immediately raise `DecodeError` (#132).

### Performance

- **Pre-encoded static base64 headers and native fast-path claim verification.**
In `encode` and `encode_json`, tokens with standard default headers now use
pre-encoded immutable base64 header slices, eliminating runtime `Header` JSON
serialization and base64 allocation. In `decode`, standard claim validations
(`iat`, unrequested `aud` rejection, and `sub` type checks) now execute entirely
within the Rust core, allowing the fast-path decode to return the claims dictionary
directly without redundant Python-side re-checks (#150).
- **Eliminate triple JSON round-trip in `DecodingKey.from_jwk`.**
Materializing a decoding key from a JWK Python dictionary now bypasses intermediate
`serde_json::Value` DOM allocations and re-serialization. The dictionary is
serialized directly in C via `orjson.dumps`, and parsed into `jsonwebtoken::jwk::Jwk`
in a single pass. `DecodingKey.from_jwk` also accepts `&str`, `&[u8]` (`bytes`),
and `bytearray` natively with zero-copy deserialization. Benchmark speedup:
10× faster for octet/HMAC keys (0.97 µs vs 9.66 µs) and 21× faster for RSA keys
(1.21 µs vs 25.48 µs) (#140).
- **Avoid redundant payload copying in `encode_json` and trim `py.detach` overhead.**
In `encode_json`, the JWS signing input is now built directly from the borrowed
payload byte slice without an owned `payload_bytes.to_vec()` heap clone.
Non-cryptographic sub-microsecond paths (`get_unverified_header` and
`decode_unverified`) now run synchronously attached to the GIL, eliminating
repeated `PyEval_SaveThread` / `PyEval_RestoreThread` cycles (#139).
- **Zero-copy header algorithm extraction & static header fast-path.**
Decode paths avoid full JSON DOM parsing when only the algorithm is needed.
Standard compact headers (`{"typ":"JWT","alg":"..."}`, `{"alg":"...","typ":"JWT"}`,
`{"alg":"..."}`) are recognized in a single string match without base64 or JSON
decoding. Non-standard headers extract `alg` via a streaming serde visitor
without allocating a `serde_json::Map<String, Value>` (#138).
- **Streaming claims deserialization into `PyDict`.**
Deserialization of claims in `decode_complete` directly streams serde JSON tokens
into a `PyDict` without allocating an intermediate `serde_json::Value` tree (#137).
- **Fast-path native validation for `aud`, `iss`, and `sub`.**
Extended the in-Rust fast-path decoder to validate audience, issuer, and subject
claims directly in Rust, reducing Python fallback overhead (#136).

### Added

- **Top-level `encode_json` export with full typing and parity coverage.**
`encode_json` is now exported in the public API root (`oxyjwt.encode_json`) and on
`PyJWT.encode_json`, allowing callers who already serialize payloads to JSON
(such as via orjson) to avoid redundant dictionary deserialization (#148).
- `DecodingKey.from_jwk`, `PyJWK`, and `PyJWKSet.from_json` now accept raw UTF-8
`bytes` directly in addition to `str` and `Mapping[str, Any]`.

## 0.8.0 — 2026-09-28

Expand Down
11 changes: 11 additions & 0 deletions docs-site/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ When you pass a raw `str` / `bytes` HMAC secret (or use `EncodingKey.from_secret

OxyJWT rejects compact JWT strings larger than **256 KiB** (same order of magnitude as the default JWKS `max_bytes` cap) with `DecodeError` before base64 or JSON parsing. This applies to verified `decode`, `decode_unverified`, `get_unverified_header`, and `jws_parse_compact`. RFC 7797 **`detached_payload`** bytes passed to verified decode are capped at the same limit before copy or JSON parsing. Legitimate tokens are far smaller; huge inputs are usually denial-of-service attempts.

## Critical Header Parameter Protection (`crit`)

Under RFC 7515 §4.1.11, any extension header specified in the `crit` (Critical) array MUST be understood and processed by the recipient. If an unrecognized header is listed, or if `crit` itself contains invalid types or empty lists, the token must be rejected.

OxyJWT strictly enforces this rule in the native Rust core on **both verified and unverified decode paths**. This prevents attackers from bypassing security checks or injecting arbitrary extension claims into tokens. Only RFC 7797 unencoded payload (`b64`) is accepted if recognized; all unknown critical extensions trigger immediate rejection with `DecodeError`.

## JWK Key Usage Restrictions (`use`)

RFC 7517 distinguishes keys intended for digital signatures (`use: sig`) from keys intended for data encryption (`use: enc`). OxyJWT strictly refuses to materialize `use: enc` keys for signature verification across `PyJWK`, `PyJWKSet`, and `PyJWKClient`. Encrypted-use keys in JWKS sets are safely skipped with a `PyJWKSetSkipWarning`, preventing cross-purpose key misuse attacks.


## Treat Unverified Helpers As Inspection Only

`get_unverified_header` and `decode_unverified` do not verify the signature and do not validate claims.
Expand Down
12 changes: 11 additions & 1 deletion docs-site/docs/usage/decoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ Supported options:

`decode_unverified` and `get_unverified_header` are for inspection and debugging only; they skip cryptographic verification entirely.

## Critical Header Parameters (`crit`)

Under RFC 7515 §4.1.11 and RFC 7797, any header parameter listed in the `crit` header array must be understood and processed by the recipient.

OxyJWT strictly enforces this requirement:
- When a token contains `b64` in `crit`, OxyJWT processes it according to RFC 7797 detached payload rules.
- If a token contains unrecognized critical header extensions, or if `crit` is empty, malformed, or references reserved standard headers, OxyJWT rejects the token with `DecodeError`.
- This protection applies to both verified `decode` and unverified helpers (`decode_unverified` and `decode_complete(options={"verify_signature": False})`) to prevent parser differential security bypasses.

## Unverified Helpers

These helpers do not authenticate a token:
Expand All @@ -154,4 +163,5 @@ header = oxyjwt.get_unverified_header(token)
claims = oxyjwt.decode_unverified(token)
```

Use them for debugging, key lookup, or inspection flows. Never use their output to authorize a request.
Use them for debugging, key lookup, or inspection flows. Never use their output to authorize a request. Both helpers run synchronously attached to the GIL with sub-microsecond latency.

27 changes: 27 additions & 0 deletions docs-site/docs/usage/encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,30 @@ token = oxyjwt.encode(
OxyJWT does not allow `headers["alg"]` to override the `algorithm` argument.

Supported built-in header fields: `typ`, `cty`, `kid`, `jku`, `x5u`, `x5t`, `x5t#S256`, `url`, and `nonce`. Any other **string** name is emitted as a custom JWS header parameter (for example `X-Trace-Id`). Custom values must be strings; complex fields such as `jwk` and `x5c` are not accepted in `headers`.

## High-Throughput `encode_json`

When your application already holds a pre-serialized JSON string or UTF-8 bytes (for example, produced by `orjson.dumps()`, cached payloads, or streaming pipelines), use `oxyjwt.encode_json` to bypass dictionary serialization overhead in Python:

```python
import orjson
import oxyjwt

# Pre-serialized payload bytes
payload_bytes = orjson.dumps({
"sub": "user-123",
"role": "admin",
"exp": 1893456000,
})

# Directly sign without re-serializing
token = oxyjwt.encode_json(
payload_bytes,
"super-secret",
algorithm="HS256",
headers={"kid": "key-1"},
)
```

`encode_json` accepts `payload_json` as either `str` or `bytes`. For HMAC signing, the Rust core borrows the byte slice directly to construct the JWS signing input with zero redundant heap copying.

Loading
Loading