ProvenPin Install

Authlib jose JWT encode/decode with custom claims: working versions (tested 2026-10-15)

Issue and verify HS256 JWTs with Authlib jose, enforcing required and custom claims, then test round trips, expiry, leeway, tampering and algorithm confusion offline under pytest.

passed6 passed · 0 failed2026-10-15 21:56 UTC
Status
passed
Last passed
2026-10-15 21:56 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
authlib@1.9.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38813530570

The code

Exactly the files that passed. Your agent can pull them with get_recipe("authlib-jose-jwt-claims") (install), or see them on GitHub.

requirements.txt
authlib==1.9.1
pytest==9.1.1
jwt_utils.py
import time

from authlib.jose import jwt

SECRET = b"dev-only-secret-rotate-me-before-prod"

CLAIMS_OPTIONS = {
    "sub": {
        "essential": True,
        "validate": lambda claims, value: isinstance(value, str) and value.startswith("user:"),
    },
    "roles": {
        "essential": True,
        "validate": lambda claims, value: isinstance(value, list)
        and all(isinstance(role, str) for role in value),
    },
    "aud": {"essential": True, "values": ["provenpin-api"]},
    "iat": {"essential": True},
    "exp": {"essential": True},
}


def issue_token(sub, roles, ttl=300, now=None, **extra):
    issued = int(time.time() if now is None else now)
    payload = {
        "sub": sub,
        "roles": roles,
        "aud": "provenpin-api",
        "iat": issued,
        "exp": issued + ttl,
    }
    payload.update(extra)
    # jwt.encode returns bytes, not str
    return jwt.encode({"alg": "HS256", "typ": "JWT"}, payload, SECRET).decode("ascii")


def read_token(token, now=None, leeway=0):
    claims = jwt.decode(token, SECRET, claims_options=CLAIMS_OPTIONS)
    # decode only verifies the signature, validate() applies the claims rules
    claims.validate(now=now, leeway=leeway)
    return dict(claims)
tests/test_jwt.py
import pytest
from authlib.jose import jwt
from authlib.jose.errors import (
    BadSignatureError,
    ExpiredTokenError,
    InvalidClaimError,
    MissingClaimError,
    UnsupportedAlgorithmError,
)

from jwt_utils import SECRET, issue_token, read_token

NOW = 1_700_000_000


def test_round_trip_keeps_custom_claims():
    token = issue_token("user:42", ["editor"], now=NOW)
    assert isinstance(token, str)
    claims = read_token(token, now=NOW)
    assert claims["sub"] == "user:42"
    assert claims["roles"] == ["editor"]


def test_decode_does_not_validate_exp():
    expired = issue_token("user:1", ["viewer"], ttl=-10, now=NOW)
    claims = jwt.decode(expired, SECRET)  # signature only, no error yet
    assert claims["exp"] < NOW
    with pytest.raises(ExpiredTokenError):
        read_token(expired, now=NOW)


def test_leeway_tolerates_clock_skew():
    token = issue_token("user:1", ["viewer"], ttl=5, now=NOW)
    with pytest.raises(ExpiredTokenError):
        read_token(token, now=NOW + 30)
    assert read_token(token, now=NOW + 30, leeway=60)["sub"] == "user:1"


def test_required_and_custom_claim_rules():
    bare = jwt.encode(
        {"alg": "HS256"},
        {"sub": "user:1", "iat": NOW, "exp": NOW + 60},
        SECRET,
    ).decode("ascii")
    with pytest.raises(MissingClaimError):
        read_token(bare, now=NOW)
    with pytest.raises(InvalidClaimError):
        read_token(issue_token("user:1", [123], now=NOW), now=NOW)


def test_wrong_audience_is_rejected():
    token = issue_token("user:1", ["viewer"], now=NOW, aud="someone-else")
    with pytest.raises(InvalidClaimError):
        read_token(token, now=NOW)


def test_tampering_and_none_alg_are_rejected():
    token = issue_token("user:1", ["viewer"], now=NOW)
    forged = jwt.encode(
        {"alg": "HS256"}, dict(jwt.decode(token, SECRET)), b"attacker-key"
    ).decode("ascii")
    with pytest.raises(BadSignatureError):
        read_token(forged, now=NOW)
    with pytest.raises(UnsupportedAlgorithmError):
        jwt.encode({"alg": "none"}, {"sub": "user:1"}, "")
pytest.ini
[pytest]
pythonpath = .
filterwarnings =
    ignore::authlib.deprecate.AuthlibDeprecationWarning

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.