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
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.