JWT Validation in Python — Structure, Claims, and Expiry Pitfalls
JWTs are everywhere in modern auth — and misunderstood almost as often. Here's what every part of a token means, which claims matter for security, and how to inspect tokens without a secret key.
In this guide
1. What is a JWT?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe way to represent claims between two parties, defined by RFC 7519. JWTs are widely used as access tokens in OAuth 2.0 and OpenID Connect flows, as session tokens in stateless APIs, and for information exchange between microservices.
A JWT looks like three base64url-encoded strings separated by dots. Unlike opaque tokens (random strings your server looks up in a database), JWTs are self-contained — the claims are embedded in the token and can be read by anyone who holds it.
2. JWT anatomy — header, payload, signature
A JWT consists of exactly three parts, each base64url-encoded and joined by dots:
Header
Describes the token type and the signing algorithm used.
{ "alg": "HS256", // Signing algorithm: HS256, RS256, ES256, etc. "typ": "JWT" // Token type (always "JWT") }
Payload
Contains the claims — statements about the subject (user) and metadata about the token.
{ "sub": "user_123", // Subject: who the token is about "name": "Alice", // Custom claim "iat": 1700000000, // Issued at (Unix timestamp) "exp": 1700003600 // Expires at (Unix timestamp) }
Signature
Created by signing base64url(header) + "." + base64url(payload) with the secret (HMAC) or private key (RSA/EC). Verifying the signature requires the secret or public key — the IsValid API parses the structure and checks expiry without verifying the signature (useful for debugging).
3. Standard claims — what they mean
RFC 7519 defines a set of registered claim names. You don't have to use them all, but using the standard names makes your tokens interoperable with existing libraries and frameworks.
| Claim | Full name | Meaning |
|---|---|---|
| iss | Issuer | Who created and signed the token (e.g. "https://auth.example.com") |
| sub | Subject | Who the token is about — typically a user ID |
| aud | Audience | Who the token is intended for — your API should validate this |
| exp | Expiration | Unix timestamp after which the token must not be accepted |
| nbf | Not Before | Unix timestamp before which the token must not be accepted |
| iat | Issued At | Unix timestamp when the token was issued |
| jti | JWT ID | Unique identifier for this token — useful for revocation |
4. Common JWT mistakes in Python
Accepting the "none" algorithm
JWT libraries that accept any algorithm allow an attacker to craft a token with "alg": "none" and no signature, bypassing authentication entirely. Always specify the allowed algorithms explicitly.
import jwt # ❌ NEVER do this — accepts "none" algorithm payload = jwt.decode(token, secret, algorithms=jwt.algorithms.get_default_algorithms()) # ✅ Restrict to your expected algorithm payload = jwt.decode(token, secret, algorithms=["HS256"])
Not checking the audience (aud)
A token issued for your user-facing API should not be accepted by your admin API. Always verify that aud matches your service's expected value.
# ✅ Validate audience payload = jwt.decode( token, public_key, algorithms=["RS256"], audience="https://api.myapp.com", )
Clock skew causing valid tokens to be rejected
Servers in distributed systems can have clocks slightly out of sync. A token issued at iat: now on one server may arrive at a second server where now is a few seconds in the past, causing the token to be rejected as "not yet valid" (nbf check). Add a small clock tolerance.
# ✅ Allow 30 seconds of clock skew payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=30)
Decoding the payload without verification
Python's base64 module makes it easy to decode a JWT payload manually, but this skips signature verification entirely. Use this only for debugging — never trust the claims without verifying the signature first.
import base64 import json # ⚠️ Debugging only — no signature verification! header_b64, payload_b64, signature = token.split(".") # Add padding: base64url requires padding to a multiple of 4 payload_bytes = base64.urlsafe_b64decode(payload_b64 + "==") claims = json.loads(payload_bytes) print(claims) # {'sub': 'user_123', 'iat': 1700000000, 'exp': 1700003600}
5. Parsing and inspecting tokens with the API
The IsValid JWT API parses the structure, decodes the header and payload, and returns the algorithm, issued-at, expires-at, and whether the token is currently expired — all without needing your secret or private key.
This is useful for debugging tokens in development, logging token metadata on the server side, or inspecting third-party tokens from identity providers (e.g. Google, Auth0, Cognito) to check their structure before forwarding them.
Full parameter reference and response schema: JWT API docs →
6. Python code example
Using the requests library — the de facto standard for HTTP in Python. Install it with pip install requests.
# jwt_inspector.py import os import requests API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def inspect_jwt(token: str) -> dict: """Parse and inspect a JWT using the IsValid API. Does NOT verify the cryptographic signature. Args: token: The JWT string (three dot-separated base64url parts). Returns: Parsed token info as a dictionary. Raises: requests.HTTPError: If the API returns a non-2xx status. """ response = requests.get( f"{BASE_URL}/v0/jwt", params={"value": token}, headers={"Authorization": f"Bearer {API_KEY}"}, ) response.raise_for_status() return response.json() # ── Example usage ──────────────────────────────────────────────────────────── token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ" ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" ) result = inspect_jwt(token) if not result["valid"]: print("Malformed JWT — not a valid token structure") else: print(f"Algorithm: {result['algorithm']}") print(f"Subject: {result['payload']['sub']}") print(f"Issued at: {result['issuedAt']}") print(f"Expires at: {result['expiresAt']}") print(f"Expired: {result['expired']}")
In a middleware you might log token metadata without re-decoding. Here's a Flask example:
# middleware.py (Flask) import logging from functools import wraps from flask import request, g logger = logging.getLogger(__name__) def log_token_meta(f): """Decorator that logs JWT metadata for incoming requests.""" @wraps(f) def decorated(*args, **kwargs): auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return f(*args, **kwargs) token = auth_header[7:] try: info = inspect_jwt(token) if info["valid"]: # Log non-sensitive metadata — do NOT log the token itself logger.info( "incoming request token", extra={ "token_sub": info["payload"].get("sub"), "token_alg": info["algorithm"], "token_exp": info["expiresAt"], "token_expired": info["expired"], }, ) except Exception: # Parsing failed — don't block the request, let auth handle it pass return f(*args, **kwargs) return decorated
result["expired"] for quick expiry dashboards or debugging without adding a crypto library to your tooling scripts. For production auth, always use a proper JWT library with signature verification.7. cURL example
Inspect a JWT (use --data-urlencode to avoid encoding issues with the dots and base64 characters):
curl -G -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "value=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" \ "https://api.isvalid.dev/v0/jwt"
Test with a malformed token (not three parts):
curl -G -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "value=notavalidjwt" \ "https://api.isvalid.dev/v0/jwt"
8. Understanding the response
Valid, non-expired token:
{ "valid": true, "algorithm": "HS256", "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "sub": "user_123", "name": "Alice", "iat": 1700000000, "exp": 1700003600 }, "issuedAt": "2023-11-14T22:13:20.000Z", "expiresAt": "2023-11-14T23:13:20.000Z", "expired": false }
Expired token (structure still valid):
{ "valid": true, "algorithm": "HS256", "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "sub": "user_123", "iat": 1600000000, "exp": 1600003600 }, "issuedAt": "2020-09-13T12:26:40.000Z", "expiresAt": "2020-09-13T13:26:40.000Z", "expired": true }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Token has 3 parts, both header and payload decode to valid JSON with the required fields |
| algorithm | string | Signing algorithm from the header (e.g. HS256, RS256, ES256) |
| header | object | Decoded JWT header object |
| payload | object | Decoded JWT payload object with all claims |
| issuedAt | string | null | ISO 8601 timestamp from the iat claim, or null if absent |
| expiresAt | string | null | ISO 8601 timestamp from the exp claim, or null if absent |
| expired | boolean | null | Whether the token has passed its exp timestamp, or null if no exp claim |
9. Edge cases and security notes
Tokens without an expiry claim
If the token has no exp claim, expired is null and expiresAt is null. Non-expiring tokens are a security risk — a stolen token is valid forever. Always issue tokens with an appropriate exp.
if result["expired"] is None: # Token has no expiry — treat with caution import warnings warnings.warn("Token has no exp claim — consider rejecting non-expiring tokens")
Do not send sensitive tokens to third-party services
For tokens that carry sensitive user data in their payload, avoid sending the full token to any external API — including this one. In development and debugging scenarios you can scrub the payload before logging, or use the inspect_jwt helper only with test tokens. In production, decode tokens locally using PyJWT or python-jose.
JWE (encrypted JWTs)
JWE tokens look like 5-part strings (not 3). The payload is encrypted, not just base64-encoded. The API will return valid: false for JWE tokens since they have a different structure from JWS (signed) tokens. If you work with JWE, decrypt it first using your key before inspecting the claims.
Token in query string — never
Passing a JWT as a URL query parameter causes it to appear in server access logs, browser history, and Referer headers. Always pass tokens in the Authorization: Bearer header or in a POST body. The value parameter of the IsValid API is an inspection utility — not an auth pattern to replicate.
Summary
See also
Inspect JWT tokens instantly
Free tier includes 100 API calls per day. No credit card required. Supports all standard JWT signing algorithms.