""" [C-01] JWT Authentication Module Implement Bearer token authentication for API endpoints. """ import os from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer from jose import JWTError, jwt from pydantic import BaseModel # Configuration SECRET_KEY = os.environ.get("JWT_SECRET_KEY") if not SECRET_KEY: # Generate fallback key for dev (NOT FOR PRODUCTION) import secrets SECRET_KEY = secrets.token_urlsafe(32) import sys print(f"[WARNING] JWT_SECRET_KEY not set. Generated ephemeral key: {SECRET_KEY[:20]}...", file=sys.stderr) ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 480 # 8 hours security = HTTPBearer() class TokenData(BaseModel): sub: int # user_id username: str role: str exp: datetime class TokenResponse(BaseModel): access_token: str token_type: str user_id: int username: str role: str def create_access_token(user_id: int, username: str, role: str, expires_delta: Optional[timedelta] = None): """Create JWT token with expiration.""" if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode = { "sub": str(user_id), # JWT spec requires sub to be a string "username": username, "role": role, "exp": expire, "iat": datetime.now(timezone.utc) } encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def get_current_user(credentials = Depends(security)): """ Dependency that validates JWT token from Authorization header. Returns TokenData with user_id, username, role. """ token = credentials.credentials import logging log = logging.getLogger("ainventory") log.debug(f"[AUTH] Validating token (first 10 chars): {token[:10] if token else 'None'}") try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id: int = int(payload.get("sub")) # sub is stored as string, convert back to int username: str = payload.get("username") role: str = payload.get("role") log.debug(f"[AUTH] Token valid — user_id={user_id}, username={username}, role={role}") if user_id is None or username is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token claims" ) token_data = TokenData( sub=user_id, username=username, role=role, exp=datetime.fromtimestamp(payload.get("exp"), tz=timezone.utc) ) except JWTError as e: log.error(f"[AUTH] JWTError: {type(e).__name__}: {str(e)}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"}, ) return token_data async def get_current_admin(current_user: TokenData = Depends(get_current_user)): """Dependency that checks if user has 'admin' role.""" if current_user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required" ) return current_user