Coverage for src/moai_adk/auth/security.py: 0.00%

49 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-19 06:02 +0900

1"""Security utilities for password hashing and JWT token management. 

2 

3This module provides cryptographic functions for: 

4- Password hashing and verification using bcrypt 

5- JWT token creation and validation 

6- Token expiration management 

7""" 

8 

9import jwt 

10import bcrypt 

11import time 

12import os 

13from datetime import datetime, timedelta 

14from typing import Dict, Any 

15 

16# Security constants 

17# TODO: Load from environment variable in production 

18SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production") 

19ALGORITHM = "HS256" 

20TOKEN_EXPIRE_MINUTES = 60 

21BCRYPT_ROUNDS = 12 # Cost factor for bcrypt hashing 

22 

23 

24def hash_password(password: str) -> str: 

25 """Hash a password using bcrypt with configurable cost factor. 

26 

27 Uses bcrypt with salt to securely hash passwords. Each call produces 

28 a different hash due to random salt generation. 

29 

30 Args: 

31 password: Plain text password to hash 

32 

33 Returns: 

34 Hashed password string (includes salt) 

35 

36 Raises: 

37 ValueError: If password is empty or invalid 

38 """ 

39 if not password or not isinstance(password, str): 

40 raise ValueError("Password must be a non-empty string") 

41 

42 salt = bcrypt.gensalt(rounds=BCRYPT_ROUNDS) 

43 return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") 

44 

45 

46def verify_password(password: str, hashed_password: str) -> bool: 

47 """Verify a password against its bcrypt hash. 

48 

49 Uses constant-time comparison to prevent timing attacks. 

50 

51 Args: 

52 password: Plain text password to verify 

53 hashed_password: Hashed password to compare against 

54 

55 Returns: 

56 True if password matches, False otherwise 

57 

58 Raises: 

59 ValueError: If inputs are invalid 

60 """ 

61 if not password or not isinstance(password, str): 

62 raise ValueError("Password must be a non-empty string") 

63 if not hashed_password or not isinstance(hashed_password, str): 

64 raise ValueError("Hashed password must be a non-empty string") 

65 

66 try: 

67 return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8")) 

68 except (ValueError, TypeError): 

69 # Invalid hash format 

70 return False 

71 

72 

73def create_token( 

74 user_id: str, 

75 email: str, 

76 expires_delta: timedelta = None 

77) -> str: 

78 """Create a signed JWT token with user information. 

79 

80 Args: 

81 user_id: User ID to include in token 

82 email: User email to include in token 

83 expires_delta: Token expiration time delta (default: 1 hour) 

84 

85 Returns: 

86 Signed JWT token string 

87 

88 Raises: 

89 ValueError: If inputs are invalid 

90 """ 

91 if not user_id or not isinstance(user_id, str): 

92 raise ValueError("user_id must be a non-empty string") 

93 if not email or not isinstance(email, str): 

94 raise ValueError("email must be a non-empty string") 

95 

96 if expires_delta is None: 

97 expires_delta = timedelta(minutes=TOKEN_EXPIRE_MINUTES) 

98 

99 # Use time.time() for consistency with JWT library 

100 iat_timestamp = int(time.time()) 

101 exp_timestamp = iat_timestamp + int(expires_delta.total_seconds()) 

102 

103 payload = { 

104 "user_id": user_id, 

105 "email": email, 

106 "iat": iat_timestamp, 

107 "exp": exp_timestamp 

108 } 

109 

110 token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) 

111 return token 

112 

113 

114def create_expired_token(user_id: str, email: str) -> str: 

115 """Create an expired JWT token (mainly for testing). 

116 

117 Args: 

118 user_id: User ID to include in token 

119 email: User email to include in token 

120 

121 Returns: 

122 Expired JWT token string 

123 """ 

124 # Create a token that expired 1 hour ago 

125 expires_delta = timedelta(minutes=-60) 

126 return create_token(user_id, email, expires_delta) 

127 

128 

129def verify_token(token: str) -> Dict[str, Any]: 

130 """Verify and decode a JWT token with signature validation. 

131 

132 Validates: 

133 - Token signature (not tampered with) 

134 - Token expiration (not expired) 

135 - Token format (valid JWT structure) 

136 

137 Args: 

138 token: JWT token to verify 

139 

140 Returns: 

141 Decoded token payload as dictionary 

142 

143 Raises: 

144 ValueError: If token is invalid or expired 

145 - "Token expired" - if token's exp claim is in the past 

146 - "Invalid token" - for other JWT validation failures 

147 """ 

148 if not token or not isinstance(token, str): 

149 raise ValueError("Token must be a non-empty string") 

150 

151 try: 

152 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 

153 return payload 

154 except jwt.ExpiredSignatureError: 

155 raise ValueError("Token expired") 

156 except jwt.InvalidTokenError as e: 

157 raise ValueError(f"Invalid token: {str(e)}")