Coverage for src / taipanstack / security / jwt.py: 100%
18 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-12 21:18 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-12 21:18 +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"""
9import secrets
10from collections.abc import Iterable
11from typing import TYPE_CHECKING, TypeAlias
13if TYPE_CHECKING:
14 import jwt
16import jwt
17from jwt.exceptions import PyJWTError
19from taipanstack.core.result import safe_from
21__all__ = ["decode_jwt", "encode_jwt"]
23JWTPayload: TypeAlias = dict[str, object]
26@safe_from(PyJWTError, ValueError, TypeError, NotImplementedError)
27def encode_jwt(
28 payload: JWTPayload,
29 secret_key: str,
30 algorithm: str = "HS256",
31) -> str:
32 """Encode a payload into a JWT securely.
34 Explicitly rejects the "none" algorithm to prevent bypass vulnerabilities.
36 Args:
37 payload: Dictionary containing the JWT claims.
38 secret_key: The secret key for signing the token.
39 algorithm: The signing algorithm (default "HS256").
41 Returns:
42 The encoded JWT string.
44 Raises:
45 ValueError: If the "none" algorithm is specified.
46 PyJWTError: If encoding fails.
48 """
49 if secrets.compare_digest(str(algorithm).strip().lower(), "none"):
50 raise ValueError('Algorithm "none" is explicitly disallowed.')
52 return jwt.encode(payload, secret_key, algorithm=algorithm) # nosem
55@safe_from(PyJWTError, ValueError, TypeError, AttributeError, NotImplementedError)
56def decode_jwt(
57 token: str,
58 secret_key: str,
59 algorithms: list[str],
60 audience: str | Iterable[str],
61) -> JWTPayload:
62 """Decode a JWT securely with strict claim validation.
64 Enforces that 'exp' (expiration) and 'aud' (audience) claims are present
65 and validated. Explicitly rejects the "none" algorithm.
67 Args:
68 token: The encoded JWT string.
69 secret_key: The secret key for verifying the signature.
70 algorithms: List of exactly accepted algorithms.
71 audience: The expected audience(s).
73 Returns:
74 The decoded payload dictionary.
76 Raises:
77 ValueError: If the "none" algorithm is present in the `algorithms` list.
78 PyJWTError: If the token is invalid, expired, or has incorrect claims.
80 """
81 if any(
82 secrets.compare_digest(str(alg).strip().lower(), "none") for alg in algorithms
83 ):
84 raise ValueError('Algorithm "none" is explicitly disallowed for decoding.')
86 return jwt.decode(
87 token,
88 secret_key,
89 algorithms=algorithms,
90 audience=audience,
91 options={
92 "require": ["exp", "aud"],
93 "verify_signature": True,
94 "verify_exp": True,
95 "verify_aud": True,
96 },
97 )