Files
mve-micro-docs/app/core/security.py

100 lines
2.8 KiB
Python

"""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