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

52 statements  

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

1"""Authentication service for user management and login/logout operations. 

2 

3This module provides an AuthService class that handles: 

4- User creation and storage 

5- Login with email and password 

6- JWT token issuance and validation 

7- Token revocation and blacklisting 

8""" 

9 

10from typing import Dict, Any, Optional, Set 

11from .models import User 

12from .security import ( 

13 hash_password, 

14 verify_password, 

15 create_token, 

16 verify_token as verify_jwt_token 

17) 

18 

19 

20class AuthService: 

21 """Service for handling user authentication and token management. 

22 

23 This service manages user accounts, authentication flows, and JWT tokens. 

24 It stores users in memory and maintains a token blacklist for revocation. 

25 

26 Note: 

27 For production use, replace in-memory storage with a proper database. 

28 """ 

29 

30 def __init__(self): 

31 """Initialize the authentication service with empty storage.""" 

32 self._users: Dict[str, User] = {} # Email-indexed user storage 

33 self._blacklist: Set[str] = set() # Revoked JWT tokens 

34 

35 def create_user(self, email: str, password: str) -> User: 

36 """Create a new user with email and password. 

37 

38 Password is hashed using bcrypt before storage. 

39 

40 Args: 

41 email: User's unique email address 

42 password: Plain text password to hash and store 

43 

44 Returns: 

45 Created User object with ID and timestamp 

46 

47 Raises: 

48 ValueError: If email already exists or inputs are invalid 

49 """ 

50 if not email or "@" not in email: 

51 raise ValueError("Email must be valid") 

52 if not password or len(password) < 8: 

53 raise ValueError("Password must be at least 8 characters") 

54 

55 # Check if user already exists 

56 if email in self._users: 

57 raise ValueError(f"User with email {email} already exists") 

58 

59 # Hash password securely 

60 hashed_password = hash_password(password) 

61 

62 # Create and store user 

63 user = User.create(email=email, hashed_password=hashed_password) 

64 self._users[email] = user 

65 

66 return user 

67 

68 def login(self, email: str, password: str) -> Dict[str, Any]: 

69 """Login user and issue a JWT token. 

70 

71 Authenticates user by email and password, then issues a signed JWT token 

72 valid for 1 hour. 

73 

74 Args: 

75 email: User email address 

76 password: User password (plain text) 

77 

78 Returns: 

79 Dictionary containing: 

80 - access_token: Signed JWT token 

81 - token_type: "bearer" 

82 - expires_in: 3600 (seconds, 1 hour) 

83 

84 Raises: 

85 ValueError: If email not found ("User not found") or 

86 password incorrect ("Invalid password") 

87 """ 

88 if not email or not password: 

89 raise ValueError("Email and password are required") 

90 

91 # Check if user exists 

92 if email not in self._users: 

93 raise ValueError("User not found") 

94 

95 user = self._users[email] 

96 

97 # Verify password (timing-safe comparison) 

98 if not verify_password(password, user.hashed_password): 

99 raise ValueError("Invalid password") 

100 

101 # Issue JWT token valid for 1 hour 

102 token = create_token(user_id=user.id, email=user.email) 

103 

104 return { 

105 "access_token": token, 

106 "token_type": "bearer", 

107 "expires_in": 3600 # 1 hour in seconds 

108 } 

109 

110 def get_user_from_token(self, token: Optional[str]) -> Dict[str, Any]: 

111 """Get user information from a valid JWT token. 

112 

113 Validates token signature, expiration, and revocation status. 

114 

115 Args: 

116 token: JWT token string (typically from "Bearer {token}" header) 

117 

118 Returns: 

119 Dictionary with user information: 

120 - id: User UUID 

121 - email: User email address 

122 - created_at: ISO8601 timestamp of account creation 

123 

124 Raises: 

125 ValueError: If token is invalid, expired, or revoked 

126 """ 

127 if not token: 

128 raise ValueError("Token required") 

129 

130 # Check if token is blacklisted (revoked) 

131 if self.is_token_blacklisted(token): 

132 raise ValueError("Token has been revoked") 

133 

134 # Verify and decode token (may raise ValueError if expired/invalid) 

135 payload = verify_jwt_token(token) 

136 

137 # Extract claims from token 

138 user_id = payload.get("user_id") 

139 email = payload.get("email") 

140 

141 # Find user (validate token refers to existing user) 

142 if not email or email not in self._users: 

143 raise ValueError("User not found") 

144 

145 user = self._users[email] 

146 

147 return { 

148 "id": user.id, 

149 "email": user.email, 

150 "created_at": user.created_at 

151 } 

152 

153 def logout(self, token: str) -> bool: 

154 """Logout user by revoking (blacklisting) their token. 

155 

156 Once revoked, the token cannot be used for authentication. 

157 

158 Args: 

159 token: JWT token to revoke 

160 

161 Returns: 

162 True if logout successful 

163 

164 Raises: 

165 ValueError: If token is invalid, expired, or already revoked 

166 """ 

167 if not token: 

168 raise ValueError("Token required") 

169 

170 # Verify token is valid before blacklisting 

171 payload = verify_jwt_token(token) 

172 

173 # Check if already blacklisted 

174 if token in self._blacklist: 

175 raise ValueError("Token has been revoked") 

176 

177 # Add to blacklist 

178 self._blacklist.add(token) 

179 

180 return True 

181 

182 def is_token_blacklisted(self, token: str) -> bool: 

183 """Check if a token is in the revocation blacklist. 

184 

185 Args: 

186 token: JWT token to check 

187 

188 Returns: 

189 True if token is revoked, False if still valid 

190 """ 

191 return token in self._blacklist 

192 

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

194 """Verify and decode a JWT token without checking blacklist. 

195 

196 Use this only when you need raw token validation. 

197 For user authentication, use get_user_from_token() instead, 

198 which checks blacklist. 

199 

200 Args: 

201 token: JWT token to verify 

202 

203 Returns: 

204 Decoded token payload 

205 

206 Raises: 

207 ValueError: If token is invalid or expired 

208 """ 

209 return verify_jwt_token(token)