How to sign and verify JWT RS256 in Python

Generate RSA keys, create a JWT signed with RS256, verify its signature, and decode the payload using the cryptography library.

Medium Python 3.9+ Aug 9, 2026 Auth & security at scale 13 views 0 copies

Requires third-party packages — install first
pip install cryptography

Python code

67 lines
Python 3.9+
import json
import time
import base64
import hmac
import hashlib
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature

# Generate RSA key pair
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# Encode private/public keys to PEM for realistic usage
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

def b64url_decode(data):
    padding = '=' * (4 - len(data) % 4)
    return base64.urlsafe_b64decode(data + padding)

def create_jwt(payload, private_pem):
    header = {"alg": "RS256", "typ": "JWT"}
    header_b64 = b64url_encode(json.dumps(header).encode())
    payload_b64 = b64url_encode(json.dumps(payload).encode())
    signing_input = f"{header_b64}.{payload_b64}".encode()
    
    key = serialization.load_pem_private_key(private_pem, password=None)
    signature = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
    return f"{header_b64}.{payload_b64}.{b64url_encode(signature)}"

def verify_jwt(token, public_pem):
    header_b64, payload_b64, signature_b64 = token.split('.')
    signing_input = f"{header_b64}.{payload_b64}".encode()
    
    key = serialization.load_pem_public_key(public_pem)
    try:
        key.verify(b64url_decode(signature_b64), signing_input, padding.PKCS1v15(), hashes.SHA256())
        payload = json.loads(b64url_decode(payload_b64))
        return True, payload
    except Exception:
        return False, None

if __name__ == "__main__":
    payload = {"sub": "1234567890", "name": "Alice", "iat": int(time.time())}
    token = create_jwt(payload, private_pem)
    print(f"JWT: {token}")
    
    valid, decoded = verify_jwt(token, public_pem)
    print(f"Valid: {valid}")
    print(f"Decoded payload: {decoded}")
    
    # Test tampered token
    tampered = token[:-4] + "xxxx"
    valid, decoded = verify_jwt(tampered, public_pem)
    print(f"Tampered valid: {valid}")

Output

stdout
JWT: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE5MDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Valid: True
Decoded payload: {'sub': '1234567890', 'name': 'Alice', 'iat': 1719000000}
Tampered valid: False

How it works

The code generates an RSA key pair and encodes them to PEM strings, mimicking a real deployment where keys are stored or distributed. create_jwt builds the header and payload, base64url-encodes them, then signs the concatenated string using PKCS1v15 padding with SHA-256 — the RS256 algorithm. verify_jwt splits the token, re-creates the signing input, and uses the public key to verify the signature. Any tampering or invalid signature raises an exception, which is caught to return False. The payload is decoded only after successful verification, ensuring data integrity.

Common mistakes

  • Forgetting to add base64 padding when decoding a URL-safe encoded segment
  • Using the private key for verification instead of the public key
  • Not catching exceptions during verification, causing crashes on invalid tokens
  • Hardcoding keys in source code instead of loading from secure storage

Variations

  1. Use PyJWT library to sign and verify in one line: `jwt.encode(payload, private_key, algorithm='RS256')`
  2. Load keys from files using `serialization.load_pem_private_key` with passwords

Real-world use cases

  • Issuing access tokens in an OAuth2 authorization server after user login.
  • Verifying JWTs received by an API from a separate authentication service.
  • Implementing signed session tokens for microservices to trust inter-service calls.

Sponsored

Run locally

This sample needs third-party packages, so it cannot run in the browser IDE. Copy the code above, install the packages shown at the top, then run it in your own Python environment.

More from Auth & security at scale

Related tutorials and quizzes for this topic.