Coverage for src / taipanstack / security / jwt.py: 100%
18 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-23 14:54 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-23 14:54 +0000
1"""
2Secure JWT Utility module.
4Provides explicitly secure wrappers around PyJWT encoding and decoding,
5enforcing strict validation of algorithms, expiration, and audience claims.
6All operations return ``Result`` types.
7"""
9from collections.abc import Iterable
10from typing import TypeAlias
12import jwt
13from jwt.exceptions import PyJWTError
15from taipanstack.core.result import safe_from
17__all__ = ["decode_jwt", "encode_jwt"]
19JWTPayload: TypeAlias = dict[str, object]
22@safe_from(PyJWTError, ValueError, TypeError)
23def encode_jwt(
24 payload: JWTPayload,
25 secret_key: str,
26 algorithm: str = "HS256",
27) -> str:
28 """Encode a payload into a JWT securely.
30 Explicitly rejects the "none" algorithm to prevent bypass vulnerabilities.
32 Args:
33 payload: Dictionary containing the JWT claims.
34 secret_key: The secret key for signing the token.
35 algorithm: The signing algorithm (default "HS256").
37 Returns:
38 The encoded JWT string.
40 Raises:
41 ValueError: If the "none" algorithm is specified.
42 PyJWTError: If encoding fails.
44 """
45 if str(algorithm).strip().lower() == "none":
46 raise ValueError('Algorithm "none" is explicitly disallowed.')
48 return jwt.encode(payload, secret_key, algorithm=algorithm)
51@safe_from(PyJWTError, ValueError, TypeError, AttributeError)
52def decode_jwt(
53 token: str,
54 secret_key: str,
55 algorithms: list[str],
56 audience: str | Iterable[str],
57) -> JWTPayload:
58 """Decode a JWT securely with strict claim validation.
60 Enforces that 'exp' (expiration) and 'aud' (audience) claims are present
61 and validated. Explicitly rejects the "none" algorithm.
63 Args:
64 token: The encoded JWT string.
65 secret_key: The secret key for verifying the signature.
66 algorithms: List of exactly accepted algorithms.
67 audience: The expected audience(s).
69 Returns:
70 The decoded payload dictionary.
72 Raises:
73 ValueError: If the "none" algorithm is present in the `algorithms` list.
74 PyJWTError: If the token is invalid, expired, or has incorrect claims.
76 """
77 if any(str(alg).strip().lower() == "none" for alg in algorithms):
78 raise ValueError('Algorithm "none" is explicitly disallowed for decoding.')
80 # We enforce 'exp' and 'aud' through PyJWT's options parameter
81 options = {
82 "require": ["exp", "aud"],
83 "verify_signature": True,
84 "verify_exp": True,
85 "verify_aud": True,
86 }
88 return jwt.decode(
89 token,
90 secret_key,
91 algorithms=algorithms,
92 audience=audience,
93 options=options, # type: ignore[arg-type]
94 )