The most surprising thing about JWTs is that alg:none is a legitimate, albeit deeply flawed, signing algorithm.

Let’s see it in action. Imagine a simple JWT that represents a user’s identity.

{
  "alg": "none",
  "typ": "JWT"
}
e30K.eyJhbGciOiJub25lIn0K.

The first part is the header, encoded. The second is the payload. The third is the signature, which is empty because the algorithm is none. The server receiving this token would simply trust the payload because there’s no signature to verify.

This lack of verification is the core of several attack vectors.

Attack 1: alg:none Exploitation

When a server is configured to accept alg:none, an attacker can craft a JWT with any payload they desire and set the algorithm to none. They then send this token to the server. Since the server doesn’t expect a signature for alg:none, it blindly trusts the payload.

Diagnosis: Check your JWT validation library’s configuration. Look for settings that control accepted algorithms or explicitly allow none. For example, in python-jose, you might see:

from jose import jwt

# This is BAD configuration
options = {
    "verify_signature": False, # Or algorithm=None is implicitly allowed
}
# ... later in your code
decoded_token = jwt.decode(token, key, algorithms=["none"], options=options)

Fix: Explicitly configure your JWT library to reject the none algorithm. Most libraries have a default list of allowed algorithms. Ensure none is not in that list. For python-jose:

from jose import jwt

# This is GOOD configuration
# Only allow specific, secure algorithms
allowed_algorithms = ["RS256", "HS256"]
decoded_token = jwt.decode(token, key, algorithms=allowed_algorithms)

Why it works: By disallowing alg:none, you force the server to always expect and verify a signature. An attacker sending a none algorithm would then fail the signature verification step, preventing them from injecting malicious claims.

Attack 2: Key Confusion

This attack occurs when a server uses the same secret key for both signing and verification, but also uses algorithms like HS256 (HMAC with SHA-256) and RS256 (RSA signature with SHA-256). An attacker who has obtained the secret key can craft a token signed with HS256 using that secret, but then present it to the server claiming it was signed with RS256. If the server is configured to accept RS256 but uses the same secret key for verification (which is incorrect for RS256), it will incorrectly validate the token.

Diagnosis: Review your JWT signing and verification logic.

  1. Signing: Are you using a symmetric key (like a shared secret for HS256)?
  2. Verification: Are you configured to accept asymmetric algorithms (like RS256)?
  3. Key Usage: If you accept RS256, are you using the public key for verification? If you are using the same secret for both HS256 signing and RS256 verification, you have a key confusion vulnerability.

Fix: Never use the same key for symmetric (HS256) and asymmetric (RS256) algorithms.

  • If you use HS256, the verification key must be the same shared secret used for signing.
  • If you use RS256, the signing key is the private RSA key, and the verification key must be the corresponding public RSA key.

Ensure your verification logic correctly identifies the algorithm and uses the appropriate key type. For example, using PyJWT in Python:

from jwt import encode, decode, PyJWTError

# For HS256
secret_key = "your-super-secret-key"
payload = {"user_id": 123}
token_hs256 = encode(payload, secret_key, algorithm="HS256")
# Verification expects the same secret_key
decoded_hs256 = decode(token_hs256, secret_key, algorithms=["HS256"])

# For RS256 (requires key pair)
# Assume private_key_pem and public_key_pem are loaded
# token_rs256 = encode(payload, private_key_pem, algorithm="RS256")
# Verification expects the PUBLIC key
# decoded_rs256 = decode(token_rs256, public_key_pem, algorithms=["RS256"])

# The VULNERABLE scenario:
# Attacker crafts a token claiming to be RS256 but signed with HS256 secret
# If your decode function accepts RS256 AND uses the HS256 secret for verification:
# THIS IS WRONG: decode(token_signed_with_hs256_secret, secret_key, algorithms=["RS256"])

Why it works: By strictly enforcing the correct key type and algorithm pairing, you prevent an attacker from tricking the verification process into accepting a token signed with a different, potentially compromised, method.

Attack 3: Token Forgery (via Header/Payload Manipulation)

This is the most basic form of forging. If a token is not properly signed or if the signature verification is flawed (e.g., due to alg:none or key confusion), an attacker can simply change claims in the payload. For example, they might change their user ID, role, or permissions.

Diagnosis:

  1. Check Signature: Is the signature being verified at all? Is the correct algorithm being used?
  2. Check Claims: After successful verification, are you re-checking critical claims against expected values or business logic? For example, if a token grants temporary admin access, is there a separate check that the current time is within the allowed window?

Fix: Ensure robust signature verification as described above. Then, always validate critical claims in the payload against your application’s security policies. For example, if a token has an is_admin: true claim, your code should not blindly trust it. It should check if the current logged-in user has the authority to act as an admin, or if the token was issued by a trusted authority for that purpose.

from jwt import decode, PyJWTError

secret = "your-secret"
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaXNBZG1pbiI6ZmFsc2UsImV4cCI6MTY3ODg4NjQwMH0.signed_payload_here" # Example

try:
    payload = decode(token, secret, algorithms=["HS256"])
    user_id = payload.get("userId")
    is_admin = payload.get("isAdmin") # This might be forged!

    # Application-level check:
    if is_admin and not user_can_perform_admin_actions(user_id):
        raise PermissionError("User is not authorized for admin actions.")

    print(f"User {user_id} is authenticated.")

except PyJWTError:
    print("Invalid token.")

Why it works: Signature verification ensures the token hasn’t been tampered with at the cryptographic level. Application-level validation ensures the content of the token is still relevant and permissible within your system’s current context and security rules.

The next error you’ll hit is likely a ValueError or InvalidSignatureError if you’ve successfully closed off the alg:none and key confusion vectors, and you try to use a token with an unsupported algorithm.

Want structured learning?

Take the full Http course →