Autenticación de seguridad de FastAPI

FastAPI incluye varias herramientas de seguridad que admiten métodos comunes de autenticación y autorización, como OAuth2, tokens JWT, API Key, etc. Esta sección presenta cómo implementar la autenticación de usuarios basada en OAuth2 + JWT.


Descripción general de la autenticación de seguridad

Esquemas de seguridad compatibles con FastAPI:

EsquemaEscenario de aplicaciónDescripción
HTTP Basic AuthServicios internos simplesNombre de usuario y contraseña codificados en el encabezado de la solicitud, seguridad baja.
API KeyLlamadas entre serviciosTransmitir la clave mediante encabezados de solicitud, parámetros de consulta o cookies.
OAuth2 + JWTAplicaciones con frontend y backend separadosEl esquema más utilizado, seguro y flexible.

Modo de contraseña OAuth2 + JWT

Este es el esquema de autenticación más común en aplicaciones con frontend y backend separados. Flujo:

  1. El cliente envía el nombre de usuario y la contraseña al/tokenendpoint
  2. El servidor valida las credenciales y devuelve un token de acceso JWT.
  3. El cliente incluye el token en solicitudes posteriores (Authorization: Bearer <token>)
  4. El servidor valida el token e identifica al usuario.

1. Instalar dependencias

pip install "python-jose[cryptography]" passlib[bcrypt]

2. Ejemplo completo

Ejemplo

from datetime import datetime, timedelta
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

# ===== Configuración =====
SECRET_KEY = "your-secret-key-keep-it-secret"  # En producción, usar variables de entorno
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# ===== Hash de contraseña =====
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# ===== Esquema OAuth2 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# ===== Modelo de datos =====
class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str | None = None


class User(BaseModel):
    username: str
    email: str | None = None
    full_name: str | None = None
    disabled: bool | None = None


class UserInDB(User):
    hashed_password: str


# ===== Base de datos simulada =====
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "[email protected]",
        "hashed_password": pwd_context.hash("secret"),  # Contraseña: secret
        "disabled": False,
    }
}


# ===== Funciones de utilidad =====
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Validar contraseña"""
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
    """Generar hash de contraseña"""
    return pwd_context.hash(password)


def get_user(db: dict, username: str) -> UserInDB | None:
    """Obtener usuario de la base de datos"""
    if username in db:
        return UserInDB(**db[username])
    return None


def authenticate_user(db: dict, username: str, password: str):
    """Validar credenciales del usuario"""
    user = get_user(db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(data: dict, expires_delta: timedelta | None = None):
    """Crear token de acceso JWT"""
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    """Obtener el usuario actual del token (función de dependencia)"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="No se pueden validar las credenciales",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = get_user(fake_users_db, username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(
    current_user: Annotated[User, Depends(get_current_user)],
):
    """Obtener el usuario activo actual"""
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="El usuario ha sido deshabilitado")
    return current_user


# ===== Rutas =====
app = FastAPI()


@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    """Iniciar sesión para obtener token"""
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Nombre de usuario o contraseña incorrectos",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(
    current_user: Annotated[User, Depends(get_current_active_user)],
):
    """Obtener información del usuario actual (requiere autenticación)"""
    return current_user


@app.get("/users/me/items")
async def read_own_items(
    current_user: Annotated[User, Depends(get_current_active_user)],
):
    """Obtener los elementos del usuario actual (requiere autenticación)"""
    return [{"item_id": "Foo", "owner": current_user.username}]

Análisis del código

Hash de contraseña

Usarpasslibel algoritmo bcrypt para aplicar hash a las contraseñas, asegurando que no se almacenen contraseñas en texto plano en la base de datos:

FunciónDescripción
pwd_context.hash(password)Convierte la contraseña en texto plano a un valor hash
pwd_context.verify(plain, hashed)Verifica si la contraseña en texto plano coincide con el valor hash

Token JWT

JWT (JSON Web Token) es un formato de token seguro que contiene información del usuario y tiempo de expiración:

OperaciónFunciónDescripción
Crear tokenjwt.encode()Codifica los datos como una cadena JWT
Analizar tokenjwt.decode()Analiza y valida la cadena JWT
Establecer expiración"exp": expireTiempo de expiración del token
Almacenar identificador de usuario"sub": usernameEl sujeto del token (normalmente el nombre de usuario)

OAuth2PasswordBearer

Le dice a FastAPI que desde elAuthorization: Bearer <token>encabezado de solicitud obtenga el token:

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

tokenUrl="token"Especifica la ruta del endpoint desde la que el cliente obtiene el token; aparecerá en la documentación de la API.

SECRET_KEYDebe mantenerse en secreto y ser suficientemente compleja; en producción debe almacenarse mediante variables de entorno. Si la clave se filtra, un atacante puede falsificar tokens de cualquier usuario.


Probar con la documentación de la API

Después de configurar OAuth2, el botón "Authorize" aparecerá en Swagger UI:

  1. Haga clic en"Authorize"
  2. Ingrese el nombre de usuario y la contraseña (por ejemplo,alice / secret)
  3. Haga clic en"Authorize"Obtener token
  4. Todas las solicitudes posteriores incluirán automáticamente el token.

Resumen

  • OAuth2 + JWT es el esquema de autenticación más común en aplicaciones con frontend y backend separados.
  • Las contraseñas deben almacenarse con hash, no en texto plano.
  • El token JWT contiene el identificador del usuario y el tiempo de expiración.
  • Usar inyección de dependencias (Depends) para reutilizar la lógica de autenticación.
  • En producción, SECRET_KEY debe mantenerse en secreto.
Otras extensiones