ProvenPin Install

RS256 JWTs with PyJWT and cryptography-generated keys: working versions (tested 2026-10-15)

Generate an RSA keypair with cryptography, sign RS256 JWTs with PyJWT, and verify them with a public key while enforcing algorithm, audience, expiry and signature checks, all tested offline.

passed6 passed · 0 failed2026-10-15 13:32 UTC
Status
passed
Last passed
2026-10-15 13:32 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions3
pyjwt@2.15.1cryptography@50.0.2pytest@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 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pyjwt-cryptography-rs256") (install), or see them on GitHub.

requirements.txt
pyjwt==2.15.1
cryptography==50.0.2
pytest==9.1.1
auth.py
from datetime import datetime, timedelta, timezone

import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa


def generate_keypair() -> tuple[bytes, bytes]:
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    private_pem = key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    public_pem = key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return private_pem, public_pem


def issue_token(private_pem: bytes, subject: str, audience: str, ttl: int = 300, kid: str | None = None) -> str:
    now = datetime.now(timezone.utc)
    payload = {"sub": subject, "aud": audience, "iat": now, "exp": now + timedelta(seconds=ttl)}
    headers = {"kid": kid} if kid else None
    # PyJWT 2.x returns str (not bytes) and converts datetime claims to integer timestamps
    return jwt.encode(payload, private_pem, algorithm="RS256", headers=headers)


def verify_token(public_pem: bytes, token: str, audience: str) -> dict:
    # Always pass an explicit algorithm allowlist, never the key's type alone
    return jwt.decode(
        token,
        public_pem,
        algorithms=["RS256"],
        audience=audience,
        options={"require": ["exp", "iat", "sub"]},
    )
tests/test_auth.py
import base64
import json

import jwt
import pytest

from auth import generate_keypair, issue_token, verify_token


@pytest.fixture(scope="module")
def keys():
    return generate_keypair()


def _b64(obj) -> str:
    return base64.urlsafe_b64encode(json.dumps(obj).encode()).rstrip(b"=").decode()


def test_roundtrip_rs256(keys):
    priv, pub = keys
    token = issue_token(priv, "alice", "api", kid="k1")
    assert isinstance(token, str)
    assert jwt.get_unverified_header(token) == {"alg": "RS256", "typ": "JWT", "kid": "k1"}
    assert verify_token(pub, token, "api")["sub"] == "alice"


def test_audience_is_enforced(keys):
    priv, pub = keys
    token = issue_token(priv, "alice", "api")
    # A token carrying "aud" is rejected when the verifier does not expect one
    with pytest.raises(jwt.InvalidAudienceError):
        jwt.decode(token, pub, algorithms=["RS256"])
    with pytest.raises(jwt.InvalidAudienceError):
        verify_token(pub, token, "other")


def test_expired_token_rejected(keys):
    priv, pub = keys
    token = issue_token(priv, "alice", "api", ttl=-5)
    with pytest.raises(jwt.ExpiredSignatureError):
        verify_token(pub, token, "api")


def test_unsigned_none_token_rejected(keys):
    _, pub = keys
    token = f"{_b64({'alg': 'none', 'typ': 'JWT'})}.{_b64({'sub': 'mallory', 'aud': 'api'})}."
    with pytest.raises(jwt.InvalidAlgorithmError):
        verify_token(pub, token, "api")


def test_algorithms_argument_is_required(keys):
    priv, pub = keys
    token = issue_token(priv, "alice", "api")
    # Omitting the allowlist lets the library choose the algorithm, which is unsafe
    with pytest.raises(jwt.DecodeError):
        jwt.decode(token, pub, audience="api")


def test_token_from_foreign_key_rejected(keys):
    _, pub = keys
    other_priv, _ = generate_keypair()
    forged = issue_token(other_priv, "mallory", "api")
    with pytest.raises(jwt.InvalidSignatureError):
        verify_token(pub, forged, "api")
pytest.ini
[pytest]
pythonpath = .

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.