Concepto clave
La autenticación JWT con OAuth2 y contraseñas hasheadas es el estándar industrial para proteger APIs modernas. Imagina que tu API es un edificio de oficinas: OAuth2 es el sistema de seguridad que verifica credenciales en la entrada, JWT es la credencial temporal que se entrega tras la verificación, y el hashing de contraseñas es la caja fuerte donde se guardan las llaves maestras de forma irreversible.
En términos técnicos, OAuth2 define los flujos de autorización, JWT proporciona tokens portátiles y auto-contenidos, y el hashing (con algoritmos como bcrypt) transforma contraseñas en valores irreversibles. Esta combinación resuelve tres problemas críticos: autenticación segura, autorización granular y protección de datos sensibles. En producción, esto se traduce en APIs que resisten ataques comunes como fuerza bruta, inyección de tokens y exposición de credenciales.
Cómo funciona en la práctica
El flujo completo sigue estos pasos:
- El cliente envía credenciales (usuario/contraseña) al endpoint de login
- El servidor verifica las credenciales contra la base de datos (contraseña hasheada)
- Si son válidas, genera un JWT con claims específicos
- El token se devuelve al cliente en la respuesta
- El cliente incluye el token en el header Authorization de peticiones subsiguientes
- El servidor valida el token en cada request protegido
- El token expira según configuración, forzando reautenticación
Este patrón permite escalabilidad horizontal, ya que el token contiene toda la información necesaria sin requerir sesiones en el servidor.
Código en acción
Implementación completa con FastAPI, OAuth2PasswordBearer y bcrypt:
from datetime import datetime, timedelta
from typing import Optional
from fastapi import FastAPI, Depends, 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 = "tu_clave_secreta_super_segura_cambia_en_produccion"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Modelos
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
access_token: str
token_type: str
# Base de datos simulada
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", # "secret"
"disabled": False,
}
}
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_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: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="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"}
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No se pudieron 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=username)
if user is None:
raise credentials_exception
return user
@app.get("/users/me/")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_userRefactorización para producción: antes usábamos una clave simple, después implementamos variables de entorno y rotación de claves:
# ANTES: Clave hardcodeada
SECRET_KEY = "clave_facil_de_adivinar"
# DESPUES: Configuración segura
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY no configurada en variables de entorno")Errores comunes
- No usar salt en el hashing: bcrypt lo incluye automáticamente, pero otros algoritmos no. Siempre verifica que tu contexto CryptContext use esquemas modernos.
- Exponer información sensible en el JWT: Los tokens viajan en headers, pero pueden ser decodificados (aunque no modificados). Nunca incluyas contraseñas o datos críticos.
- Tokens con expiración muy larga: En producción, 30 minutos es razonable para access tokens. Implementa refresh tokens para sesiones prolongadas.
- Validación incompleta del token: Siempre verifica expiración, issuer (iss) y audience (aud) cuando corresponda.
- Almacenar contraseñas en texto plano temporalmente: Hash inmediatamente al recibirlas, incluso en memoria.
Checklist de dominio
- Configuré OAuth2PasswordBearer con endpoint personalizado
- Implementé bcrypt para hashing y verificación de contraseñas
- Generé JWTs con expiración configurable y claims relevantes
- Validé tokens en cada request protegido, manejando excepciones JWT
- Protegí rutas con dependencias de autenticación
- Use variables de entorno para claves secretas
- Implementé manejo seguro de errores (HTTP 401, 403 apropiados)
Implementa un sistema de registro y login con JWT, OAuth2 y hashing seguro
Extiende el ejemplo proporcionado para crear un sistema completo de autenticación:
- Crea un endpoint POST
/registerque acepte username, email y password. Hashea la contraseña con bcrypt y almacena el usuario en una base de datos simulada. - Modifica el endpoint
/tokenpara que verifique contra la nueva base de datos. - Añade un endpoint GET
/users/{username}protegido que solo pueda acceder el usuario dueño de ese perfil o un admin. - Implementa logout invalidando tokens (simula con una blacklist en memoria).
- Añade validación: password mínimo 8 caracteres, con letras y números.
Entrega el código completo en un archivo main.py funcional.
Pistas- Usa Pydantic para validar los datos de registro
- Considera agregar un campo 'role' a los usuarios para implementar autorización
- Para la blacklist, puedes usar un set en memoria pero en producción usa Redis
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.