REFACTOR API gateway fix 8

This commit is contained in:
Urtzi Alfaro
2025-07-26 23:29:57 +02:00
parent 1291d05183
commit 97ae58fb06
8 changed files with 997 additions and 375 deletions

View File

@@ -30,20 +30,6 @@ redis_client = redis.from_url(settings.REDIS_URL)
class SecurityManager:
"""Security utilities for authentication - FIXED VERSION"""
@staticmethod
def hash_password(password: str) -> str:
"""Hash password using passlib bcrypt - FIXED"""
return pwd_context.hash(password)
@staticmethod
def verify_password(password: str, hashed_password: str) -> bool:
"""Verify password against hash using passlib - FIXED"""
try:
return pwd_context.verify(password, hashed_password)
except Exception as e:
logger.error(f"Password verification error: {e}")
return False
@staticmethod
def validate_password(password: str) -> bool:
"""Validate password strength"""
@@ -65,48 +51,59 @@ class SecurityManager:
return True
@staticmethod
def create_access_token(user_data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token with PROPER validation"""
def hash_password(password: str) -> str:
"""Hash password using passlib bcrypt - FIXED"""
return pwd_context.hash(password)
@staticmethod
def verify_password(password: str, hashed_password: str) -> bool:
"""Verify password against hash using passlib - FIXED"""
try:
return pwd_context.verify(password, hashed_password)
except Exception as e:
logger.error(f"Password verification error: {e}")
return False
@staticmethod
def create_access_token(user_data: Dict[str, Any]) -> str:
"""
Create JWT ACCESS token with proper payload structure
✅ FIXED: Only creates access tokens
"""
# ✅ FIX 1: Validate required fields BEFORE token creation
required_fields = ["user_id", "email"]
missing_fields = [field for field in required_fields if field not in user_data]
# Validate required fields for access token
if "user_id" not in user_data:
raise ValueError("user_id required for access token creation")
if missing_fields:
error_msg = f"Missing required fields for token creation: {missing_fields}"
logger.error(f"Token creation failed: {error_msg}")
raise ValueError(error_msg)
# ✅ FIX 2: Validate that required fields are not None/empty
if not user_data.get("user_id"):
raise ValueError("user_id cannot be empty")
if not user_data.get("email"):
raise ValueError("email cannot be empty")
if "email" not in user_data:
raise ValueError("email required for access token creation")
try:
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
# ✅ FIX 3: Build payload with SAFE access to user_data
# ✅ FIX 1: ACCESS TOKEN payload structure
payload = {
"sub": user_data["user_id"],
"user_id": user_data["user_id"],
"email": user_data["email"], # ✅ Guaranteed to exist now
"type": "access",
"full_name": user_data.get("full_name", ""), # Safe access with default
"is_verified": user_data.get("is_verified", False), # Safe access with default
"is_active": user_data.get("is_active", True), # Safe access with default
"email": user_data["email"],
"type": "access", # ✅ EXPLICITLY set as access token
"exp": expire,
"iat": datetime.now(timezone.utc),
"iss": "bakery-auth" # Token issuer
"iss": "bakery-auth"
}
# Add optional fields for access tokens
if "full_name" in user_data:
payload["full_name"] = user_data["full_name"]
if "is_verified" in user_data:
payload["is_verified"] = user_data["is_verified"]
if "is_active" in user_data:
payload["is_active"] = user_data["is_active"]
logger.debug(f"Creating access token with payload keys: {list(payload.keys())}")
# ✅ FIX 4: Use jwt_handler with proper error handling
token = jwt_handler.create_access_token(payload)
# ✅ FIX 2: Use JWT handler to create access token
token = jwt_handler.create_access_token_from_payload(payload)
logger.debug(f"Access token created successfully for user {user_data['email']}")
return token
@@ -116,13 +113,14 @@ class SecurityManager:
@staticmethod
def create_refresh_token(user_data: Dict[str, Any]) -> str:
"""Create JWT refresh token with FLEXIBLE validation"""
"""
Create JWT REFRESH token with minimal payload structure
✅ FIXED: Only creates refresh tokens, different from access tokens
"""
# ✅ FIX 1: Validate only essential fields for refresh token
# Validate required fields for refresh token
if "user_id" not in user_data:
error_msg = "user_id required for refresh token creation"
logger.error(f"Refresh token creation failed: {error_msg}")
raise ValueError(error_msg)
raise ValueError("user_id required for refresh token creation")
if not user_data.get("user_id"):
raise ValueError("user_id cannot be empty")
@@ -130,24 +128,31 @@ class SecurityManager:
try:
expire = datetime.now(timezone.utc) + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS)
# ✅ FIX 2: Minimal payload for refresh token (email is optional)
# ✅ FIX 3: REFRESH TOKEN payload structure (minimal, different from access)
payload = {
"sub": user_data["user_id"],
"user_id": user_data["user_id"],
"type": "refresh",
"type": "refresh", # ✅ EXPLICITLY set as refresh token
"exp": expire,
"iat": datetime.now(timezone.utc),
"iss": "bakery-auth"
}
# ✅ FIX 3: Include email only if available (no longer required)
# Add unique JTI for refresh tokens to prevent duplicates
if "jti" in user_data:
payload["jti"] = user_data["jti"]
else:
import uuid
payload["jti"] = str(uuid.uuid4())
# Include email only if available (optional for refresh tokens)
if "email" in user_data and user_data["email"]:
payload["email"] = user_data["email"]
logger.debug(f"Creating refresh token with payload keys: {list(payload.keys())}")
# Use the same JWT handler method (it handles both access and refresh)
token = jwt_handler.create_access_token(payload)
# ✅ FIX 4: Use JWT handler to create REFRESH token (not access token!)
token = jwt_handler.create_refresh_token_from_payload(payload)
logger.debug(f"Refresh token created successfully for user {user_data['user_id']}")
return token
@@ -167,6 +172,55 @@ class SecurityManager:
logger.warning(f"Token verification failed: {e}")
return None
@staticmethod
def decode_token(token: str) -> Dict[str, Any]:
"""Decode JWT token without verification (for refresh token handling)"""
try:
payload = jwt_handler.decode_token_no_verify(token)
return payload
except Exception as e:
logger.error(f"Token decoding failed: {e}")
raise ValueError("Invalid token format")
@staticmethod
def generate_secure_hash(data: str) -> str:
"""Generate secure hash for token storage"""
return hashlib.sha256(data.encode()).hexdigest()
@staticmethod
async def track_login_attempt(email: str, ip_address: str, success: bool) -> None:
"""Track login attempts for security monitoring"""
try:
# This would use Redis for production
# For now, just log the attempt
logger.info(f"Login attempt tracked: email={email}, ip={ip_address}, success={success}")
except Exception as e:
logger.warning(f"Failed to track login attempt: {e}")
@staticmethod
def is_token_expired(token: str) -> bool:
"""Check if token is expired"""
try:
payload = SecurityManager.decode_token(token)
exp_timestamp = payload.get("exp")
if exp_timestamp:
exp_datetime = datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
return datetime.now(timezone.utc) > exp_datetime
return True
except Exception:
return True
@staticmethod
def verify_token(token: str) -> Optional[Dict[str, Any]]:
"""Verify JWT token with enhanced error handling"""
try:
payload = jwt_handler.verify_token(token)
if payload:
logger.debug(f"Token verified successfully for user: {payload.get('email', 'unknown')}")
return payload
except Exception as e:
logger.warning(f"Token verification failed: {e}")
return None
@staticmethod
async def track_login_attempt(email: str, ip_address: str, success: bool) -> None:
"""Track login attempts for security monitoring"""