Autenticación responde "¿quién eres?". Autorización responde "¿qué puedes hacer?". Son preguntas distintas, y mezclarlas en un solo chequeo disperso por los endpoints (if user.role == "admin" or user.role == "coordinador": ... repetido veinte veces) es de las formas más comunes en que una API termina con un endpoint que se le olvidó proteger. Esto es lo que uso en APIs con FastAPI que manejan roles reales — admin, coordinador, supervisor, auxiliar en un sistema de almacén; niveles de aprobación en un sistema de viáticos — para mantener esa separación clara.
Access token corto, refresh token largo
Un JWT de acceso con vida de 15-30 minutos limita la ventana de exposición si alguien lo roba: aunque se filtre, deja de servir pronto. El problema es que eso obligaría a reloguear al usuario cada 15 minutos si no hubiera nada más — ahí entra el refresh token, con vida de días o semanas, cuyo único trabajo es pedir un access token nuevo cuando el actual expira.
Dónde se guarda cada uno importa tanto como su duración:
- Access token en memoria (una variable de JavaScript, no
localStorage). Si un XSS logra ejecutar código en la página,localStoragees legible por ese script; una variable en memoria que se pierde al recargar la página es una superficie de ataque menor. - Refresh token en cookie HttpOnly + Secure + SameSite.
HttpOnlylo hace invisible para JavaScript (inmune a XSS),Securelo limita a HTTPS, ySameSite=StrictoLaxreduce el riesgo de CSRF.
El costo de este esquema es que el frontend necesita lógica para refrescar el access token en cuanto expira (normalmente interceptando la respuesta 401 y reintentando tras pedir uno nuevo), pero es un costo pequeño comparado con el problema que resuelve.
Roles como tabla, no como string mágico repetido
En vez de comparar user.role == "admin" dentro de cada función de endpoint, conviene centralizar el chequeo en una dependencia de FastAPI reutilizable:
def require_role(*roles: str):
def checker(user: User = Depends(get_current_user)):
if user.role not in roles:
raise HTTPException(status_code=403, detail="No autorizado")
return user
return checker
@router.delete("/almacenes/{id}")
def eliminar_almacen(id: int, user: User = Depends(require_role("admin"))):
...
La ventaja no es solo menos código repetido: es que el rol requerido queda visible en la firma del endpoint, así que revisar qué puede hacer cada rol es leer las firmas, no rastrear ifs dispersos por todo el archivo de rutas. get_current_user, a su vez, es la dependencia que decodifica el JWT del header Authorization, valida la firma y expiración, y devuelve (o falla) el usuario — se reutiliza en cada endpoint protegido sin reescribir esa lógica.
2FA como capa adicional, no como reemplazo
En sistemas donde se autorizan descuentos o montos (por ejemplo, un cotizador B2B), agregar TOTP (Time-based One-Time Password, el mismo estándar de Google Authenticator) como segundo factor para ciertas acciones sensibles reduce el riesgo si la contraseña se filtra. Un detalle que se pasa por alto seguido: el secreto del 2FA nunca debe viajar dentro del JWT. El JWT prueba que ya pasaste el login; el secreto TOTP es un dato de configuración de la cuenta que vive solo en el backend, y el flujo de verificación del código de 6 dígitos es una llamada aparte, no algo que se decodifique del token.
Errores comunes que veo repetirse
- No verificar expiración en cada request. Si tu middleware de auth decodifica el JWT pero no comprueba explícitamente el campo
exp, un token vencido puede seguir siendo aceptado dependiendo de la librería usada. - No invalidar sesiones tras cambio de contraseña. Si un usuario cambia su contraseña porque sospecha que la filtraron, cualquier refresh token emitido antes debería dejar de servir. Esto requiere guardar al menos un identificador de sesión o una versión de token por usuario que se pueda invalidar del lado del servidor — un JWT puro, sin estado, no puede revocarse por sí solo antes de su expiración.
- CORS demasiado permisivo. Un
Access-Control-Allow-Origin: *combinado con cookies de sesión es una combinación peligrosa; la lista de orígenes permitidos debe ser explícita, no un comodín, especialmente si el refresh token vive en una cookie. - Confundir "autenticado" con "autorizado". Que un endpoint solo pida
Depends(get_current_user)sin verificar el rol significa que cualquier usuario logueado — sin importar su rol — puede llamarlo. Es un error fácil de cometer en endpoints que se agregan rápido bajo presión de fecha de entrega.
Conclusión
JWT y RBAC no son complicados por sí mismos; se vuelven riesgosos cuando la autenticación y la autorización se mezclan en el mismo chequeo ad-hoc, endpoint por endpoint. Centralizar ambas en dependencias reutilizables de FastAPI — una para "quién eres", otra para "qué rol necesitas" — hace que el sistema de permisos sea algo que se puede auditar leyendo las firmas de las funciones, no reconstruyendo lógica dispersa por todo el proyecto.