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.
Python code
40 linesimport 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
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
- Use the PyJWT library (`pip install PyJWT`) for production-grade encryption, expiration, and standard compliance.
- 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
More from Auth & security at scale
- ACME LetsEncrypt Mock Challenge Server in Python medium
- AES GCM encryption and decryption in Python medium
- Build a Mock OIDC Userinfo Endpoint in Python with Flask easy
- ChaCha20-Poly1305 mock in Python medium
- ECDH key agreement in Python with cryptography medium
- Enforce TLS 1.2 Minimum in Python easy
Keep learning
Related tutorials and quizzes for this topic.