How to Encode and Decode JWT with HS256 in Python

Implement JWT encoding and decoding using HMAC-SHA256 (HS256) with Python's standard library, including signature verification.

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

Python code

40 lines
Python 3.9+
import base64
import hashlib
import hmac
import json


def base64url_encode(data: bytes) -> bytes:
    return base64.urlsafe_b64encode(data).rstrip(b"=")


def base64url_decode(data: str) -> bytes:
    padding = "=" * (-len(data) % 4)
    return base64.urlsafe_b64decode(data + padding)


def encode_jwt(payload: dict, secret: str) -> str:
    header = {"alg": "HS256", "typ": "JWT"}
    header_encoded = base64url_encode(json.dumps(header, separators=(",", ":")).encode())
    payload_encoded = base64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    signing_input = header_encoded + b"." + payload_encoded
    signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
    return (signing_input + b"." + base64url_encode(signature)).decode()


def decode_jwt(token: str, secret: str) -> dict:
    header_encoded, payload_encoded, signature_encoded = token.split(".")
    signing_input = (header_encoded + "." + payload_encoded).encode()
    expected_signature = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
    actual_signature = base64url_decode(signature_encoded)
    if not hmac.compare_digest(expected_signature, actual_signature):
        raise ValueError("Invalid signature")
    payload = json.loads(base64url_decode(payload_encoded))
    return payload


if __name__ == "__main__":
    secret = "mysecretkey"
    token = encode_jwt({"user": "alice", "role": "admin"}, secret)
    print("Token:", token)
    print("Decoded:", decode_jwt(token, secret))

Output

stdout
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ.s7fVJBciWx0cvfOyzPBvSJh7ve3Vj3R3m_7MqZz_JGY
Decoded: {'user': 'alice', 'role': 'admin'}

How it works

JWT structure consists of three parts: header, payload, and signature, separated by dots. The header and payload are JSON objects encoded with Base64URL (without padding). The signature is an HMAC-SHA256 hash of the header and payload combined, using a secret key. hmac.compare_digest ensures constant-time comparison to prevent timing attacks. This mock implementation replicates the core behavior of libraries like PyJWT without external dependencies.

Common mistakes

  • Forgetting to remove '=' padding when encoding, causing mismatched signatures.
  • Using regular base64 instead of base64url, which includes '+' and '/' characters invalid in JWTs.
  • Not verifying the signature before trusting the payload, leading to token forgery.
  • Using `==` instead of `hmac.compare_digest` for signature comparison, introducing timing side channels.

Variations

  1. Use the PyJWT library (`pip install PyJWT`) for production-grade encryption, expiration, and standard compliance.
  2. Add an `exp` claim and check it during decoding to enforce token expiration.

Real-world use cases

  • Issuing stateless authentication tokens for a microservice API, verifying them on each request without session storage.
  • Signing short-lived access tokens in a serverless environment to pass user identity between functions.
  • Simulating JWT behavior in tests or educational tools where external dependencies are not allowed.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Auth & security at scale

Related tutorials and quizzes for this topic.