PyFu

Introduction to JSON Web Tokens in Python

Python-based Web Application Attacks

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 exp field is automatically validated when decoding the token.

The secret_key used 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:

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.