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:
| Esquema | Escenario de aplicación | Descripción |
|---|---|---|
| HTTP Basic Auth | Servicios internos simples | Nombre de usuario y contraseña codificados en el encabezado de la solicitud, seguridad baja. |
| API Key | Llamadas entre servicios | Transmitir la clave mediante encabezados de solicitud, parámetros de consulta o cookies. |
| OAuth2 + JWT | Aplicaciones con frontend y backend separados | El 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:
- El cliente envía el nombre de usuario y la contraseña al
/tokenendpoint - El servidor valida las credenciales y devuelve un token de acceso JWT.
- El cliente incluye el token en solicitudes posteriores (
Authorization: Bearer <token>) - El servidor valida el token e identifica al usuario.
1. Instalar dependencias
pip install "python-jose[cryptography]" passlib[bcrypt]
2. Ejemplo completo
Ejemplo
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ón | Descripció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ón | Función | Descripción |
|---|---|---|
| Crear token | jwt.encode() | Codifica los datos como una cadena JWT |
| Analizar token | jwt.decode() | Analiza y valida la cadena JWT |
| Establecer expiración | "exp": expire | Tiempo de expiración del token |
| Almacenar identificador de usuario | "sub": username | El 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:
- Haga clic en"Authorize"
- Ingrese el nombre de usuario y la contraseña (por ejemplo,
alice/secret) - Haga clic en"Authorize"Obtener token
- 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.