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
« 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.
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"""
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)
20class AuthService:
21 """Service for handling user authentication and token management.
23 This service manages user accounts, authentication flows, and JWT tokens.
24 It stores users in memory and maintains a token blacklist for revocation.
26 Note:
27 For production use, replace in-memory storage with a proper database.
28 """
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
35 def create_user(self, email: str, password: str) -> User:
36 """Create a new user with email and password.
38 Password is hashed using bcrypt before storage.
40 Args:
41 email: User's unique email address
42 password: Plain text password to hash and store
44 Returns:
45 Created User object with ID and timestamp
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")
55 # Check if user already exists
56 if email in self._users:
57 raise ValueError(f"User with email {email} already exists")
59 # Hash password securely
60 hashed_password = hash_password(password)
62 # Create and store user
63 user = User.create(email=email, hashed_password=hashed_password)
64 self._users[email] = user
66 return user
68 def login(self, email: str, password: str) -> Dict[str, Any]:
69 """Login user and issue a JWT token.
71 Authenticates user by email and password, then issues a signed JWT token
72 valid for 1 hour.
74 Args:
75 email: User email address
76 password: User password (plain text)
78 Returns:
79 Dictionary containing:
80 - access_token: Signed JWT token
81 - token_type: "bearer"
82 - expires_in: 3600 (seconds, 1 hour)
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")
91 # Check if user exists
92 if email not in self._users:
93 raise ValueError("User not found")
95 user = self._users[email]
97 # Verify password (timing-safe comparison)
98 if not verify_password(password, user.hashed_password):
99 raise ValueError("Invalid password")
101 # Issue JWT token valid for 1 hour
102 token = create_token(user_id=user.id, email=user.email)
104 return {
105 "access_token": token,
106 "token_type": "bearer",
107 "expires_in": 3600 # 1 hour in seconds
108 }
110 def get_user_from_token(self, token: Optional[str]) -> Dict[str, Any]:
111 """Get user information from a valid JWT token.
113 Validates token signature, expiration, and revocation status.
115 Args:
116 token: JWT token string (typically from "Bearer {token}" header)
118 Returns:
119 Dictionary with user information:
120 - id: User UUID
121 - email: User email address
122 - created_at: ISO8601 timestamp of account creation
124 Raises:
125 ValueError: If token is invalid, expired, or revoked
126 """
127 if not token:
128 raise ValueError("Token required")
130 # Check if token is blacklisted (revoked)
131 if self.is_token_blacklisted(token):
132 raise ValueError("Token has been revoked")
134 # Verify and decode token (may raise ValueError if expired/invalid)
135 payload = verify_jwt_token(token)
137 # Extract claims from token
138 user_id = payload.get("user_id")
139 email = payload.get("email")
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")
145 user = self._users[email]
147 return {
148 "id": user.id,
149 "email": user.email,
150 "created_at": user.created_at
151 }
153 def logout(self, token: str) -> bool:
154 """Logout user by revoking (blacklisting) their token.
156 Once revoked, the token cannot be used for authentication.
158 Args:
159 token: JWT token to revoke
161 Returns:
162 True if logout successful
164 Raises:
165 ValueError: If token is invalid, expired, or already revoked
166 """
167 if not token:
168 raise ValueError("Token required")
170 # Verify token is valid before blacklisting
171 payload = verify_jwt_token(token)
173 # Check if already blacklisted
174 if token in self._blacklist:
175 raise ValueError("Token has been revoked")
177 # Add to blacklist
178 self._blacklist.add(token)
180 return True
182 def is_token_blacklisted(self, token: str) -> bool:
183 """Check if a token is in the revocation blacklist.
185 Args:
186 token: JWT token to check
188 Returns:
189 True if token is revoked, False if still valid
190 """
191 return token in self._blacklist
193 def verify_token(self, token: str) -> Dict[str, Any]:
194 """Verify and decode a JWT token without checking blacklist.
196 Use this only when you need raw token validation.
197 For user authentication, use get_user_from_token() instead,
198 which checks blacklist.
200 Args:
201 token: JWT token to verify
203 Returns:
204 Decoded token payload
206 Raises:
207 ValueError: If token is invalid or expired
208 """
209 return verify_jwt_token(token)