Introduction to JSON Web Tokens in Python
JSON Web Token (JWT) is a widely adopted mechanism for stateless authentication and authorization in modern web applications.
Unlike traditional session-based authentication that stores session state on the server, JWT embeds all necessary information in a signed token passed between the client and server.
JWT (JSON Web Token) is a compact, URL-safe token format used for transmitting claims between two parties.
It is widely used in modern authentication and authorization systems, particularly for stateless session handling in web applications and APIs.
A JWT consists of three parts, separated by dots (.):
<Header>.<Payload>.<Signature>
The header part specifies the type of token and the algorithm used for signing:
{
"alg": "HS256",
"typ": "JWT"
}
-
alg: Signing algorithm (commonly HS256: HMAC using SHA-256).
-
typ: Always set to “JWT”. The header is Base64URL-encoded.
The payload carries the actual claims (data) that will be transferred, for example:
{
"username": "askar",
"exp": 1717266000
}
The payload is not encrypted which means anyone can decode and read it.
Sensitive data should not be stored here unless additional encryption is applied.
The signature part ensures the integrity of the token. It is calculated as:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
If any part of the token is modified, the signature becomes invalid.
The secret key must be kept private on the server.
JWT in Python
In Python, the most common library for working with JWTs is PyJWT.
It can be installed using:
pip install PyJWT
PyJWT provides simple methods to create (encode) and verify (decode) JWT tokens.
For example, this code used to encode a token:
import jwt
import datetime
payload = {
'username': 'askar',
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}
token = jwt.encode(payload, 'supersecretkey', algorithm="HS256")
print(token)
# Output:
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFza2FyIiwiZXhwIjoxNzQ5OTEwNzE1fQ.u3iLlPvBOACr7pdKROrHDeX426Y7zOCHa9hGM2e8mfA
The generated token is a compact JWT string that can be sent to the client. The client usually includes this token in the Authorization header of subsequent HTTP requests:
Authorization: Bearer <token>
When receiving the token, the server needs to verify its integrity and decode the payload. This can be done using the decode() function provided by PyJWT:
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFza2FyIiwiZXhwIjoxNzQ5OTEwNzE1fQ.u3iLlPvBOACr7pdKROrHDeX426Y7zOCHa9hGM2e8mfA"
try:
payload = jwt.decode(token, 'supersecretkey', algorithms=["HS256"])
print(payload)
except jwt.ExpiredSignatureError:
print("Token expired")
except jwt.InvalidTokenError:
print("Invalid token")
The decode() function checks both the signature and expiration timestamp. If the token has been tampered with or expired, the corresponding exception will be raised.
It’s important to note that:
The
expfield is automatically validated when decoding the token.
The
secret_keyused for encoding must match the one used for decoding, otherwise the signature verification will fail.
Why JSON Web Tokens matter from an offensive security perspective
When I see a JWT in an assessment I treat it as a self-contained authorization decision the client carries, which is exactly why it is such a high-value target class. A session cookie is an opaque pointer to server-side state I cannot influence; a JWT is the state, signed and handed back to me to inspect and tamper with. If I can break the signing model, I do not bypass one check, I mint identity. A single forged token yields account takeover, privilege escalation to role: admin, and horizontal movement across every user the API trusts, with no further interaction.
What makes JWTs so productive to attack is that the entire trust boundary collapses to one operation: signature verification. Everything else in the token is attacker-controlled plaintext. The payload is Base64URL, not encrypted, so I read the claims for free and learn the role model, the user IDs, the issuer, and the expiry strategy before I touch anything. From there every JWT bug is a variation on one theme, the verifier trusting data inside the token instead of pinning its own.
These are the tells I look for in PyJWT and python-jose code:
jwt.decodewith no algorithm pinned or a permissive list. A missingalgorithms=argument, or one mixing families like["RS256","HS256"], is the root of Forging JWTs with the none Algorithm and JWT Algorithm Confusion (RS256 to HS256).options={"verify_signature": False}orverify=False. Verification disabled for debugging and left on, the entire signature check gone. See Authentication Bypass via Broken JWT Validation.- A short, human-chosen, or copied secret. Values like
secretoryour-256-bit-secretin source fall to an offline wordlist, see Cracking Weak JWT Signing Keys; a secret committed toconfig.pyor.envis straight forgery, see Authentication Bypass via JWT Hardcoded Secret. - Header fields driving key selection. Any code that reads
kid,jku, orjwkfrom the token to locate a key hands me the key, see JWT Header Injection via jku, jwk, and kid. - Authorization from the payload alone. A
roleclaim trusted because the signature passed means recovering the secret is full admin, not just a valid login.
The defender takeaway: a JWT moves the authorization decision to the client, so its only real protection is a verifier that pins its own algorithm and key and never trusts a field the token supplies.
Mitigation
Used correctly a JWT is safe; the attacks in this section all come from how it is verified. Always verify the signature with the algorithm pinned explicitly, such as algorithms=["HS256"] or the specific RS256 public key, never trusting the token’s own alg header and never accepting none. Use a long, random signing secret for HMAC schemes so it cannot be brute-forced and keep it out of source control, validate the standard claims of expiry, issuer, and audience on every request rather than trusting the payload because the signature checked out, and remember the token is readable by the client, so put no secrets in it and keep authorization decisions server-side rather than relying on a self-asserted role claim alone.