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

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 

9from collections.abc import Iterable 

10from typing import TypeAlias 

11 

12import jwt 

13from jwt.exceptions import PyJWTError 

14 

15from taipanstack.core.result import safe_from 

16 

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

18 

19JWTPayload: TypeAlias = dict[str, object] 

20 

21 

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. 

29 

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

31 

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

36 

37 Returns: 

38 The encoded JWT string. 

39 

40 Raises: 

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

42 PyJWTError: If encoding fails. 

43 

44 """ 

45 if str(algorithm).strip().lower() == "none": 

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

47 

48 return jwt.encode(payload, secret_key, algorithm=algorithm) 

49 

50 

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. 

59 

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

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

62 

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

68 

69 Returns: 

70 The decoded payload dictionary. 

71 

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. 

75 

76 """ 

77 if any(str(alg).strip().lower() == "none" for alg in algorithms): 

78 raise ValueError('Algorithm "none" is explicitly disallowed for decoding.') 

79 

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 } 

87 

88 return jwt.decode( 

89 token, 

90 secret_key, 

91 algorithms=algorithms, 

92 audience=audience, 

93 options=options, # type: ignore[arg-type] 

94 )