En el desarrollo de aplicaciones web modernas, proteger los datos y las operaciones sensibles ya no es opcional. Los ataques son cada vez más sofisticados y las normativas exigen controles rigurosos. Para un backend construido con Django y Django REST Framework, la combinación de autenticación multifactor y autorización contextual marca la diferencia entre una API vulnerable y una blindada. Este artículo te guiará a través de estrategias avanzadas que integran OAuth 2.0, Tokens Web JSON (JWT) y autenticación de dos factores, logrando un equilibrio entre seguridad robusta y experiencia de desarrollo eficiente.
Muchas implementaciones se quedan en la simple verificación de contraseña, pero los estándares actuales exigen múltiples capas. Primero, asegurarse de que quien solicita acceso es quien dice ser (autenticación), y segundo, determinar exactamente a qué recursos y operaciones tiene derecho (autorización). Django, con su ecosistema maduro, permite incorporar estas capas sin reinventar la rueda, aprovechando bibliotecas como django-otp, djangorestframework-simplejwt y validadores de JWT externos. El resultado es una API preparada para entornos multi-inquilino, cumplimiento de GDPR o PCI-DSS y escenarios de alto tráfico.
En las siguientes secciones desglosaremos los fundamentos de OAuth 2.0 y JWT, la implementación de un segundo factor de autenticación con contraseñas de un solo uso basadas en tiempo (TOTP), la creación de permisos granulares que consideran roles y alcances, y finalmente el flujo completo que une todas las piezas. Tanto si estás diseñando una nueva API como si necesitas reforzar una existente, encontrarás patrones reutilizables y consejos de producción.
OAuth 2.0 es un marco de autorización estándar que permite a aplicaciones cliente obtener acceso limitado a recursos protegidos sin exponer las credenciales del usuario. En el contexto de una API Django, actúa como la capa que emite tokens de acceso tras una autenticación exitosa. Estos tokens, generalmente en formato JWT, contienen declaraciones (claims) que el servidor puede verificar sin necesidad de almacenar estado de sesión, una ventaja crucial para arquitecturas escalables.
Los Tokens Web JSON (JWT) son cadenas compactas y autónomas que transmiten información entre partes de forma segura mediante una firma digital. Un JWT se compone de tres partes: encabezado, carga útil y firma. En Django, podemos tanto generar nuestros propios JWT con bibliotecas como Simple JWT, como validar tokens emitidos por servicios externos de identidad (como Auth0 o Logto) utilizando PyJWT. Esta flexibilidad permite integrar autenticación federada y control de acceso centralizado.
En un escenario típico, el cliente (una aplicación frontend o un servicio servidor-a-servidor) solicita un token al servidor de autorización presentando sus credenciales y el alcance deseado. El servidor valida la identidad y devuelve un token de acceso y, opcionalmente, un token de actualización. En Django, puedes implementar tu propio servidor de autorización con Django OAuth Toolkit o delegar en un proveedor externo, validando los tokens entrantes en tu API.
Los flujos más comunes son el código de autorización para aplicaciones web y el flujo de credenciales de cliente para comunicación entre servidores. Cada flujo está diseñado para un tipo de cliente y nivel de confianza. Comprenderlos te permitirá elegir el más adecuado para tu caso de uso y evitar fugas de seguridad, como exponer credenciales en el frontend.
Los claims de un JWT transportan la identidad del sujeto (sub), el emisor (iss), la audiencia (aud), el tiempo de expiración (exp) y los alcances (scope) o roles. Al validar con PyJWT, verificarás la firma usando la clave pública del emisor (obtenida desde un punto de descubrimiento OpenID Connect o un JWKS), y luego comprobarás que la audiencia coincida con tu recurso de API y que el token no haya expirado.
Un error común es confiar solo en la validez de la firma sin verificar los claims de alcance o audiencia. Un token firmado correctamente pero destinado a otra API podría ser reutilizado maliciosamente. Por eso es crítico implementar una validación completa dentro de un middleware o permiso personalizado de Django, como veremos más adelante.
La autenticación de dos factores añade una capa adicional de seguridad combinando algo que el usuario sabe (su contraseña) con algo que posee (un dispositivo generador de códigos temporales). La biblioteca django-otp facilita la implementación del estándar TOTP, el mismo que usan aplicaciones como Google Authenticator o Authy. Con esta integración, incluso si una contraseña se ve comprometida, el atacante necesitaría acceso físico al dispositivo del usuario.
Además de proteger el inicio de sesión, 2FA puede aplicarse de forma granular: solo para acciones sensibles como cambiar la contraseña o acceder a datos financieros. Django OTP permite almacenar múltiples dispositivos por usuario, confirmar el primer uso y manejar códigos de respaldo, ofreciendo una experiencia completa lista para producción.
Tras instalar django-otp y qrcode, debes añadir la aplicación a INSTALLED_APPS y ejecutar migraciones. Cada usuario tendrá asociado uno o varios dispositivos TOTP con una clave secreta única. La función get_or_create_totp_device permite reutilizar dispositivos no confirmados y evitar duplicados, una práctica recomendada para no bloquear al usuario antes de la primera verificación.
La generación de un código QR a partir de la URL de configuración del dispositivo (device.config_url) permite al usuario escanearlo con su aplicación autenticadora favorita. El secreto solo debe mostrarse en el momento de la configuración o en entornos controlados, nunca en producción a menos que sea estrictamente necesario.
Dos vistas basadas en APIView de Django REST Framework cubren el flujo: una GET para obtener el dispositivo y QR, y otra POST para verificar el código OTP. La primera requiere autenticación previa (por ejemplo, un JWT básico) y devuelve el identificador del dispositivo y la URL del QR. La segunda recibe el token OTP y confirma el dispositivo si la verificación es exitosa.
Es importante mantener estos endpoints bajo rate limiting para prevenir ataques de fuerza bruta. También se recomienda invalidar cualquier token de acceso previo una vez que el dispositivo TOTP se confirma, forzando al cliente a obtener un nuevo JWT enriquecido con la verificación de segundo factor. Esto refuerza el modelo de dos tokens que veremos más adelante.
Django OTP fue diseñado originalmente para sesiones tradicionales, donde el estado de verificación se guarda en el servidor. En APIs REST sin estado con JWT, necesitamos una estrategia diferente: extender el payload del JWT con un identificador de dispositivo OTP confirmado (otp_device_id). Así, cada petición puede autoverificar que el segundo factor fue satisfecho.
Esto se logra creando una clase de token personalizada que herede de RefreshToken y añada el claim. Luego, un permiso personalizado en DRF consulta este claim y verifica que el dispositivo exista y esté confirmado para el usuario. Esta técnica mantiene la naturaleza sin estado de JWT mientras incorpora 2FA de manera elegante y segura.
Más allá del segundo factor, el control de acceso basado en roles (RBAC) permite definir exactamente qué puede hacer cada usuario o aplicación. Los alcances (scope) incluidos en el JWT representan permisos atómicos como lectura:productos o escritura:pedidos. Al validarlos en cada endpoint, te aseguras de que incluso un token auténtico y con 2FA no pueda realizar operaciones no autorizadas.
La autorización contextual añade otra dimensión: limitar el acceso a recursos dentro de una organización específica. Esto es vital en aplicaciones SaaS multi-inquilino, donde un usuario de la empresa A no debe ver datos de la empresa B. Los claims de audiencia y organización dentro del JWT permiten implementar este aislamiento de forma natural.
Los tres modelos principales son: (1) Recursos de API globales, donde un token con los alcances adecuados permite acceder a endpoints compartidos por todos los clientes; (2) Permisos de organización, enfocados en controlar acciones no relacionadas con una API específica (como invitar miembros) y cuyo contexto se define mediante una audiencia con formato urn:logto:organization:id; (3) Recursos de API a nivel de organización, que combinan ambos validando tanto el claim de organización como los alcances de API.
Elegir el modelo correcto depende de la arquitectura de tu aplicación. Si trabajas con microservicios aislados por inquilino, el modelo mixto ofrece la mayor granularidad. Para una API monolítica con distintos niveles de suscripción, los recursos globales pueden ser suficientes, siempre que los roles y alcances estén bien definidos.
| Modelo | Audiencia esperada | Claim de organización | Ejemplo de uso |
|---|---|---|---|
| Recursos de API globales | Indicador del recurso API | No presente | APIs comunes, endpoints de administración |
| Permisos de organización | urn:logto:organization:id |
No presente (implícito en aud) | Gestión de miembros, facturación |
| Recursos de API a nivel de organización | Indicador del recurso API | ID de organización explícito | APIs multi-inquilino con datos aislados |
La función verify_payload centraliza las comprobaciones. Para recursos globales, basta con verificar que la audiencia coincida con el indicador de tu API registrado y que los alcances requeridos estén presentes. Para organización, se extrae el ID del contexto de la solicitud (p. ej., del subdominio o parámetro) y se compara con el claim correspondiente.
Herramientas como PyJWT facilitan la decodificación y verificación de firma, pero la lógica de negocio de los claims es responsabilidad del desarrollador. Se recomienda crear utilidades reutilizables que encapsulen estas validaciones y lanzar excepciones personalizadas con códigos HTTP 401 o 403 según el fallo.
El escenario completo comienza cuando un usuario inicia sesión con sus credenciales y recibe un primer JWT con alcances básicos y sin confirmación de segundo factor. Si el usuario tiene 2FA habilitado, el frontend solicita el código TOTP y lo envía al endpoint de verificación. Una verificación exitosa devuelve un nuevo par de tokens (acceso y refresco) con el claim otp_device_id, que certifica el segundo factor.
A partir de ese momento, todas las peticiones a endpoints protegidos deben incluir este JWT enriquecido. Un permiso personalizado IsOTPVerified inspecciona el claim y permite o deniega el acceso. Para endpoints especialmente sensibles, se pueden combinar IsAuthenticated, IsOTPVerified y validación de alcances, logrando un control de acceso multicapa sin código repetitivo.
La implementación práctica implica extender el TokenObtainPairView de Simple JWT para que, tras la autenticación inicial, incluya el otp_device_id si el usuario ya tiene un dispositivo confirmado. Para nuevos dispositivos, se emite un JWT temporal con permisos limitados, suficiente para acceder a los endpoints de configuración 2FA, pero no a la API principal. Esto evita puntos ciegos donde un atacante pudiera configurar su propio segundo factor.
En el frontend, la lógica suele almacenar solo el último JWT recibido, descartando el anterior. Así se garantiza que todas las operaciones posteriores llevan la marca de verificación completa. Para mejorar la experiencia, se pueden emitir tokens de refresco de larga duración que conserven el claim OTP, permitiendo renovar el acceso sin reautenticación completa.
La clase IsOTPVerified extiende BasePermission y utiliza JWTAuthentication para extraer y validar el token. Comprueba la existencia del claim otp_device_id y que el dispositivo asociado esté confirmado y pertenezca al usuario autenticado. Si alguna comprobación falla, retorna False y DRF responde con 403 Forbidden.
Este enfoque modular permite aplicar el permiso solo en vistas específicas, combinándolo con otros como IsAdminUser o un permiso que valide alcances. Así se evita la tentación de crear un middleware monolítico que ralentice toda la aplicación. La granularidad es clave para un rendimiento óptimo y una seguridad proporcionada al riesgo de cada endpoint.
En producción, es fundamental implementar rate limiting en los endpoints de verificación TOTP y de emisión de tokens. Un límite de 5 intentos fallidos por minuto y por usuario mitiga ataques de fuerza bruta. Además, genera códigos de respaldo (backup codes) durante la configuración inicial del 2FA; estos códigos de un solo uso permiten al usuario recuperar el acceso en caso de pérdida del dispositivo, almacenándolos de forma segura con hash.
La rotación periódica de claves secretas utilizadas para firmar los JWT y la monitorización de logs de errores de autenticación son igualmente importantes. Registra eventos como fallos de verificación 2FA o tokens con audiencia incorrecta, anonimizando identificadores personales para cumplir con normativas de privacidad. Si utilizas un proveedor externo de identidad, asegúrate de mantener sincronizadas las claves públicas mediante su endpoint JWKS.
Para aplicaciones multi-inquilino, considera almacenar el contexto de organización en el token pero validarlo también contra la base de datos de tu aplicación. No confíes únicamente en el claim; un atacante podría manipularlo si el token no está correctamente firmado. La defensa en profundidad siempre es tu aliada. Para ampliar tus conocimientos sobre protección, te invitamos a leer nuestro artículo sobre avances en seguridad cibernética para aplicaciones web con Django, donde analizamos vulnerabilidades y cómo mitigarlas.
Implementar estas estrategias equivale a instalar un sistema de seguridad por capas en tu plataforma digital. El usuario solo percibe un inicio de sesión normal y, si tiene activada la verificación en dos pasos, un código temporal que llega a su móvil. Detrás de escena, cada petición verifica múltiples condiciones: que el token sea auténtico, que el segundo factor esté confirmado y que los permisos coincidan con la operación solicitada. Esto disuade a los atacantes incluso si obtienen contraseñas.
El beneficio directo es la protección de datos sensibles y la confianza del cliente, lo que se traduce en cumplimiento normativo y ventaja competitiva. Además, la flexibilidad de los modelos de permisos permite adaptar el acceso según el plan de suscripción o el rol dentro de una organización, sin complicar la experiencia de usuario final.
La combinación de OAuth 2.0, JWT extendidos con claims personalizados y django-otp ofrece una arquitectura de seguridad sin estado, escalable y mantenible. La clave reside en separar la emisión de tokens en fases, añadiendo el claim de dispositivo OTP sin sacrificar la naturaleza stateless de JWT. Los permisos granulares basados en alcances y organización convierten la autorización en un aspecto declarativo, fácil de auditar y ampliar.
Como próximos pasos, evalúa la adopción de WebAuthn para autenticación sin contraseña, integra sistemas de gestión de identidad externos para desacoplar la autenticación de tu lógica de negocio y refuerza la observabilidad con dashboards de intentos fallidos. La seguridad de una API Django no es un producto terminado, sino un proceso continuo que estas estrategias y nuestros servicios de desarrollo web te ayudan a cimentar correctamente.
Soluciones personalizadas en desarrollo web, enfocadas en backend y tecnología Django. Transformamos ideas en aplicaciones exitosas con experiencia y dedicación.