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.
Summary
Authaccepts analgorithmargument, 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
Authimplementation.Where
tina4_python/auth/__init__.py_signnever consultsself.algorithm.Reproduction
Why it matters
The token is self-consistent inside Tina4, because
valid_tokenalso 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
algheader 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.
sign()branches HS256/RS256, both implemented (Tina4/Auth.php:494-503)HS256, decode assertsheader["alg"] == "HS256"(lib/tina4/auth.rb:116,134)sign()throwsUnsupported 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
HS256in__init__, matching Node's throw.Larger change: implement RS256 to reach parity with PHP and Node, which also resolves the
TINA4_JWT_ALGORITHMissue filed separately.Silence is the one option that should be off the table.
Worth noting in defence of the current code
valid_tokennever reads thealgheader when verifying. It recomputes the signature with the server's own key and compares withhmac.compare_digest. That makes Python immune to algorithm-confusion and to the"alg": "none"attack by construction, which is a stronger posture than a naivejwt.decode()call withoutalgorithms=[...].The defect is in what the token advertises, not in how it is verified.
Found by
Auditing
Authacross all four frameworks while grounding a course module on authentication. No fix applied.