# backend/auth/middleware.py # FastAPI dependencies for authentication and authorization. # Reads JWT from Authorization header OR access_token cookie. import logging import os from fastapi import Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from backend.services.net import get_client_ip from .jwt_handler import decode_token, is_token_revoked from .user_store import get_user logger = logging.getLogger("obsigate.auth.middleware") security = HTTPBearer(auto_error=False) def is_auth_enabled() -> bool: """Check if authentication is enabled via environment variable. Default: True (auth enabled). Set OBSIGATE_AUTH_ENABLED=false to disable. """ return os.environ.get("OBSIGATE_AUTH_ENABLED", "true").lower() != "false" def get_current_user( request: Request, credentials: HTTPAuthorizationCredentials | None = Depends(security), ) -> dict | None: """Extract and validate the current user from JWT. Reads token from Authorization header first, falls back to access_token cookie. Returns None if no valid token is found. """ # If auth is disabled, return a fake admin user with full access if not is_auth_enabled(): return { "username": "anonymous", "display_name": "Anonymous", "role": "admin", "vaults": ["*"], "active": True, "_token_vaults": ["*"], "_request_ip": get_client_ip(request), } token = None if credentials: token = credentials.credentials elif "access_token" in request.cookies: token = request.cookies["access_token"] if not token: return None payload = decode_token(token) if not payload or payload.get("type") != "access": return None # BUG-027: access tokens revoked at logout must be rejected immediately. jti = payload.get("jti") if jti and is_token_revoked(jti): return None user = get_user(payload["sub"]) if not user or not user.get("active"): return None # BUG-028: a password change invalidates every token issued before it. pca = user.get("password_changed_at") iat = payload.get("iat") if pca is not None and iat is not None: try: if int(iat) < int(float(pca)): return None except (TypeError, ValueError): return None # Attach vault permissions from the token (snapshot at login time) user["_token_vaults"] = payload.get("vaults", []) # Attach the token id for per-token rate limiting (AI tool layer). user["_token_jti"] = payload.get("jti") # BUG-030: expose the real client IP to the audit log. user["_request_ip"] = get_client_ip(request) return user def require_auth(current_user=Depends(get_current_user)): """Dependency: require a valid authenticated user.""" if not current_user: raise HTTPException( status_code=401, detail="Authentification requise", headers={"WWW-Authenticate": "Bearer"}, ) return current_user def require_admin(current_user=Depends(require_auth)): """Dependency: require admin role.""" if current_user.get("role") != "admin": raise HTTPException(status_code=403, detail="Accès admin requis") return current_user def check_vault_access(vault_name: str, user: dict) -> bool: """Check if a user has access to a specific vault. Rules: - vaults == ["*"] → full access (admin default) - vault_name in vaults → access granted - otherwise → denied """ vaults = user.get("_token_vaults") or user.get("vaults", []) if "*" in vaults: return True return vault_name in vaults def require_vault_access(vault_name: str, user: dict = Depends(require_auth)): """Dependency: require access to a specific vault.""" if not check_vault_access(vault_name, user): raise HTTPException( status_code=403, detail=f"Accès refusé à la vault '{vault_name}'", ) return user