"""Security utilities for authentication and JWT token management.""" from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt import bcrypt from fastapi import HTTPException, status from .config import get_settings import time from collections import defaultdict # Simple in-memory rate limiter for login attempts login_attempts = defaultdict(list) MAX_LOGIN_ATTEMPTS = 5 LOGIN_WINDOW_SECONDS = 300 # 5 minutes def verify_password(plain_password: str, hashed_password: str) -> bool: """Verify a password against its hash.""" try: return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) except Exception as e: return False def get_password_hash(password: str) -> str: """Generate password hash.""" return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: """Create JWT access token.""" settings = get_settings() to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.jwt_expires_minutes) to_encode.update({"exp": expire, "iat": datetime.utcnow()}) encoded_jwt = jwt.encode( to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm ) return encoded_jwt def decode_access_token(token: str) -> dict: """Decode and verify JWT token.""" settings = get_settings() try: payload = jwt.decode( token, settings.jwt_secret, algorithms=[settings.jwt_algorithm] ) return payload except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) def check_rate_limit(identifier: str) -> bool: """ Simple rate limiter for login attempts. Returns True if rate limit exceeded. """ current_time = time.time() # Clean old attempts login_attempts[identifier] = [ attempt_time for attempt_time in login_attempts[identifier] if current_time - attempt_time < LOGIN_WINDOW_SECONDS ] # Check if limit exceeded if len(login_attempts[identifier]) >= MAX_LOGIN_ATTEMPTS: return True # Record this attempt login_attempts[identifier].append(current_time) return False def authenticate_user(username: str, password: str) -> bool: """Authenticate user with username and password.""" settings = get_settings() if username != settings.auth_username: return False if not verify_password(password, settings.auth_password_hash): return False return True