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
« 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.
3This module provides cryptographic functions for:
4- Password hashing and verification using bcrypt
5- JWT token creation and validation
6- Token expiration management
7"""
9import jwt
10import bcrypt
11import time
12import os
13from datetime import datetime, timedelta
14from typing import Dict, Any
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
24def hash_password(password: str) -> str:
25 """Hash a password using bcrypt with configurable cost factor.
27 Uses bcrypt with salt to securely hash passwords. Each call produces
28 a different hash due to random salt generation.
30 Args:
31 password: Plain text password to hash
33 Returns:
34 Hashed password string (includes salt)
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")
42 salt = bcrypt.gensalt(rounds=BCRYPT_ROUNDS)
43 return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
46def verify_password(password: str, hashed_password: str) -> bool:
47 """Verify a password against its bcrypt hash.
49 Uses constant-time comparison to prevent timing attacks.
51 Args:
52 password: Plain text password to verify
53 hashed_password: Hashed password to compare against
55 Returns:
56 True if password matches, False otherwise
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")
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
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.
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)
85 Returns:
86 Signed JWT token string
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")
96 if expires_delta is None:
97 expires_delta = timedelta(minutes=TOKEN_EXPIRE_MINUTES)
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())
103 payload = {
104 "user_id": user_id,
105 "email": email,
106 "iat": iat_timestamp,
107 "exp": exp_timestamp
108 }
110 token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
111 return token
114def create_expired_token(user_id: str, email: str) -> str:
115 """Create an expired JWT token (mainly for testing).
117 Args:
118 user_id: User ID to include in token
119 email: User email to include in token
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)
129def verify_token(token: str) -> Dict[str, Any]:
130 """Verify and decode a JWT token with signature validation.
132 Validates:
133 - Token signature (not tampered with)
134 - Token expiration (not expired)
135 - Token format (valid JWT structure)
137 Args:
138 token: JWT token to verify
140 Returns:
141 Decoded token payload as dictionary
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")
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)}")