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

1""" 

2Secure JWT Utility module. 

3 

4Provides explicitly secure wrappers around PyJWT encoding and decoding, 

5enforcing strict validation of algorithms, expiration, and audience claims. 

6All operations return ``Result`` types. 

7""" 

8 

9import secrets 

10from collections.abc import Iterable 

11from typing import TYPE_CHECKING, TypeAlias 

12 

13if TYPE_CHECKING: 

14 import jwt 

15 

16import jwt 

17from jwt.exceptions import PyJWTError 

18 

19from taipanstack.core.result import safe_from 

20 

21__all__ = ["decode_jwt", "encode_jwt"] 

22 

23JWTPayload: TypeAlias = dict[str, object] 

24 

25 

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. 

33 

34 Explicitly rejects the "none" algorithm to prevent bypass vulnerabilities. 

35 

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"). 

40 

41 Returns: 

42 The encoded JWT string. 

43 

44 Raises: 

45 ValueError: If the "none" algorithm is specified. 

46 PyJWTError: If encoding fails. 

47 

48 """ 

49 if secrets.compare_digest(str(algorithm).strip().lower(), "none"): 

50 raise ValueError('Algorithm "none" is explicitly disallowed.') 

51 

52 return jwt.encode(payload, secret_key, algorithm=algorithm) # nosem 

53 

54 

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. 

63 

64 Enforces that 'exp' (expiration) and 'aud' (audience) claims are present 

65 and validated. Explicitly rejects the "none" algorithm. 

66 

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). 

72 

73 Returns: 

74 The decoded payload dictionary. 

75 

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. 

79 

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.') 

85 

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 )