Skip to content

JWT header advertises an algorithm the signature does not use #105

Description

@andrevanzuydam

Summary

Auth accepts an algorithm argument, writes it into the JWT header, and then always signs with HMAC-SHA256. The header advertises an algorithm the signature does not use.

Affects 3.13.89 and every version carrying the current Auth implementation.

Where

tina4_python/auth/__init__.py

# line 202
def __init__(self, secret: str = None, algorithm: str = "HS256", ...):
    self.algorithm = algorithm

# line 240
header = {"alg": self.algorithm, "typ": "JWT"}

# line 316
def _sign(self, message: str) -> str:
    sig = hmac.new(self.secret.encode(), message.encode(), hashlib.sha256).digest()
    return _b64url_encode(sig)

_sign never consults self.algorithm.

Reproduction

from tina4_python.auth import Auth
import base64, json

token = Auth(secret="test-secret", algorithm="RS256").get_token({"user_id": 1})
header = json.loads(base64.urlsafe_b64decode(token.split(".")[0] + "=="))
print(header)   # {'alg': 'RS256', 'typ': 'JWT'}
                # signature is actually HMAC-SHA256

Why it matters

The token is self-consistent inside Tina4, because valid_token also ignores the header and recomputes with _sign. Every internal test passes.

It breaks at the first integration boundary. PyJWT, python-jose, Auth0, an API gateway or a partner service reads alg: RS256, attempts RSA verification against an HMAC signature, and rejects or errors.

RFC 7515 section 4.1.1 requires the alg header to describe the signature actually present.

The failure is silent and only surfaces when something outside Tina4 reads the token, which is the worst place to find it.

Cross-language comparison

Python is the only framework that does this. All four were read in source.

Framework Behaviour with a non-HS256 algorithm
Python Silently signs HMAC-SHA256, header lies
PHP sign() branches HS256/RS256, both implemented (Tina4/Auth.php:494-503)
Ruby Header hardcoded HS256, decode asserts header["alg"] == "HS256" (lib/tina4/auth.rb:116,134)
Node sign() throws Unsupported algorithm: ${algorithm} (packages/core/src/auth.ts:263)

Given the same input, Node throws and Python lies.

Suggested fix

Smallest correct change: reject any algorithm other than HS256 in __init__, matching Node's throw.

Larger change: implement RS256 to reach parity with PHP and Node, which also resolves the TINA4_JWT_ALGORITHM issue filed separately.

Silence is the one option that should be off the table.

Worth noting in defence of the current code

valid_token never reads the alg header when verifying. It recomputes the signature with the server's own key and compares with hmac.compare_digest. That makes Python immune to algorithm-confusion and to the "alg": "none" attack by construction, which is a stronger posture than a naive jwt.decode() call without algorithms=[...].

The defect is in what the token advertises, not in how it is verified.

Found by

Auditing Auth across all four frameworks while grounding a course module on authentication. No fix applied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions