Autenticación por sesión#

Este es el artículo número 28 de la serie de tutoriales de Django Ninja.
¡Bienvenidos al Capítulo 7! Este capítulo consta de dos artículos:
- Entrega 28: Autenticación — Autenticación por sesión y configuración global
- Entrega 29: Pruebas unitarias — Uso de Test Client y pytest para probar APIs
Las funciones centrales de estos temas no son implementadas por Django Ninja, pero el framework aún proporciona cierto nivel de integración. Además, estas funciones son de vital importancia para cualquier proyecto Django.
Este artículo presenta algo que casi todos los proyectos de API necesitan: la autenticación (Authentication).
Exploraremos cómo aprovechar la autenticación basada en sesiones integrada de Django en Django Ninja para lograr una función completa de verificación de inicio de sesión, y explicaremos más a fondo cómo configurar la autenticación global para reducir la duplicación de código.
Todos los cambios de código de este artículo se pueden consultar en este PR.
Proyecto de ejemplo en GitHub#
Los dos niveles de la autenticación#
Antes de pasar a la implementación, primero debemos comprender qué significa exactamente la autenticación.
Tomando como ejemplo «nombre de usuario y contraseña + autenticación por sesión», el alcance de la autenticación abarca principalmente dos etapas.
En primer lugar, cuando el usuario inicia sesión mediante su nombre de usuario y contraseña, el sistema verifica dicho contenido y confirma que la identidad sea legítima. Una vez que el inicio de sesión es exitoso, el sistema guarda la información del usuario (como el id de usuario) en la sesión para mantener el estado de sesión iniciada.
Esta es la autenticación al iniciar sesión, y es la autenticación a la que más nos referimos comúnmente (autenticación en sentido estricto).
A continuación, cuando el usuario intenta acceder a una API protegida por «protección de autenticación», el sistema verifica la sesión y confirma la identidad, asegurando que cada solicitud a la API provenga de un usuario que ha iniciado sesión legítimamente.
En resumen:
- Primera etapa: Confirmación de identidad durante el inicio de sesión inicial.
- Segunda etapa: Confirmación de identidad en las solicitudes posteriores.
Ambos niveles se complementan entre sí y son dos caras de la misma moneda, garantizando que el servicio proporcione las salvaguardas de seguridad adecuadas durante el inicio de sesión y las operaciones posteriores del usuario.
Implementación de la API de «inicio de sesión de usuario»#
Una vez comprendidos los dos niveles anteriores, primero implementaremos la autenticación en «sentido estricto», es decir, la verificación de inicio de sesión en sí.
Crearemos una API de «inicio de sesión de usuario» y manejaremos directamente la verificación de usuario/contraseña y el estado de inicio de sesión a través de las funciones authenticate y login de Django, ¡lo cual es muy conveniente!
authenticate se utiliza para validar si la cuenta (username) y la contraseña ingresadas por el usuario son correctas, mientras que login guarda el estado de inicio de sesión del usuario en la sesión.
Implementación en código#
Primero agregamos un Schema para la solicitud de inicio de sesión:
# user/schemas.py
class LoginRequest(Schema):
username: str = Field(examples=['Alice'])
password: str = Field(examples=['password123'])
Luego la función view:
from django.contrib.auth import authenticate, login
from user.schemas import CreateUserRequest, LoginRequest
...
@router.post('/users/login/', summary='Iniciar sesión de usuario')
def login_user(
request: HttpRequest, payload: LoginRequest
) -> dict[str, str]:
"""
Iniciar sesión de usuario
"""
user = authenticate(
request,
username=payload.username,
password=payload.password
)
if user is not None:
login(request, user) # Guardar el estado de inicio de sesión del usuario en la sesión
return {'message': 'Inicio de sesión exitoso'}
else:
raise HttpError(401, 'Nombre de usuario o contraseña incorrectos')
¡Muy simple!
Por cierto, no me gusta mucho tener else «innecesarios» en el código, por lo que la forma de escribir en este punto aún no es ideal: el else se puede omitir completamente.
En el código más reciente, puedes ver que lo he cambiado a:
user = authenticate(...)
if user is None:
raise HttpError(401, 'Nombre de usuario o contraseña incorrectos')
login(request, user) # Guardar el estado de inicio de sesión del usuario en la sesión
return {'message': 'Inicio de sesión exitoso'}
Este enfoque es lo que se conoce como Guard Clause o Early Return (aunque aquí sea un raise).
Comentarios breves y notas importantes#
El uso de authenticate y login es casi fijo y muy fácil de entender:
authenticatedevuelve el objetoUsercorrespondiente cuando la verificación es exitosa y devuelveNoneen caso de falla.loginno devuelve nada, perorequestyuserson parámetros obligatorios.
Tras un inicio de sesión exitoso, obtendrás una respuesta 200 y dos conjuntos de cookies:

Esto es muy importante para los usuarios de clientes de API (como Postman); al fin y al cabo, los navegadores las guardarán automáticamente, pero estas herramientas no... Bueno, me equivoqué, ¡al menos el RapidAPI que yo uso las almacena y envía automáticamente!
(¡Cuando estaba probando la API me pareció extraño por qué las protecciones de autenticación habían dejado de funcionar 🤣!)
Si tu herramienta no lo hace por ti, recuerda agregar en los headers de la solicitud:
authenticate utiliza por defecto el campo username y la contraseña del AbstractUser como criterio de autenticación; si deseas usar otro campo, como email, deberás sobrescribir el backend de autenticación de Django tú mismo.
Agregar «protección de autenticación» a la API#
Una vez completada la función de inicio de sesión, el siguiente paso es añadir protección de autenticación individualmente a las APIs que «requieren inicio de sesión para acceder», utilizando django_auth proporcionado por Django Ninja (diseñado específicamente para la autenticación basada en sesiones integrada de Django).
Tomemos como ejemplo la API de «subir avatar»:
from ninja.security import django_auth
...
@router.post(
path='/users/{int:user_id}/avatar/',
summary='Subir avatar',
auth=django_auth # Agregar este parámetro
)
En este ejemplo, auth=django_auth garantiza que solo los «usuarios que hayan iniciado sesión» puedan acceder a esta API; de lo contrario, obtendrán una respuesta 401 o 403.
request.auth en Django Ninja#
Pero podrías pensar:
¿No es insuficiente con validar solo «que se haya iniciado sesión»?
«Subir avatar» solo debería permitir subir el de «uno mismo»; ¡no se debería poder subir la foto de perfil de «otros»!
Así es, por lo que dentro de la función view debemos agregar una capa adicional de verificación.
request.user de Django#
En los proyectos tradicionales de Django, obtenemos la información del usuario actual a través del primer parámetro de la función, request, mediante request.user, por ejemplo: (consultar documentación)
if request.user.is_authenticated:
# Hacer algo para usuarios autenticados.
...
else:
# Hacer algo para usuarios anónimos.
...
Específicamente:
- Cuando el usuario ha iniciado sesión, request.user será una instancia de User, representando al usuario con sesión iniciada actual.
- Cuando no ha iniciado sesión, request.user será una instancia de AnonymousUser, representando a un usuario no autenticado.
Cuando el usuario ha iniciado sesión, podemos verificar las propiedades de request.user, como request.user.id, para confirmar si es «él mismo».
request.auth de Django Ninja#
Sin embargo, al escribir Django Ninja es necesario utilizar el request.auth que este proporciona. El resultado de la implementación es el siguiente:
...
def upload_avatar(...) -> dict[str, str]:
"""
Subir avatar
"""
# Verificar si el usuario autenticado es «él mismo»
if request.auth.id != user_id:
raise HttpError(403, 'Sin permiso para subir el avatar de otros usuarios')
...
Hagamos una prueba: después de iniciar sesión, coloca el id de otra persona en el path de la URL para llamar a esta API:
¡Excelente!
Análisis de request.auth#
Aunque aquí usamos request.auth para reemplazar a request.user, la esencia de ambos tiene grandes diferencias.
En Django Ninja, request.auth representa el resultado devuelto por el flujo de autenticación. Además, Django Ninja te permite personalizar los métodos de autenticación, por lo que el contenido de request.auth no es fijo.
Profundicemos un poco más al respecto.
Resultado de la autenticación#
request.auth contiene el valor devuelto por el método de autenticación actual.
- Este valor puede ser de cualquier tipo, dependiendo de cómo implementes la lógica de autenticación.
- Esto otorga a los desarrolladores una enorme flexibilidad: puede ser un objeto User, una cadena de texto, un diccionario de Python, etc.
Métodos de autenticación y casos de uso comunes#
- Al usar la autenticación por sesión de Django,
request.authes el objetoUserde Django. - Para la autenticación por API key,
request.authpuede ser la API key misma o información relacionada con ella. - En la autenticación JWT,
request.authpuede contener la información del token decodificado.
En resumen, solo recuerda que si deseas obtener más información de autenticación dentro de la función view, debes hacerlo a través de request.auth.
Así ya hemos completado la implementación de la autenticación, pero podemos hacer las cosas un poco más «simples».
Configuración y excepciones de autenticación global#
Configurar la protección de autenticación individualmente para cada API resulta un poco tedioso, especialmente cuando hay muchas APIs.
Al respecto, Django Ninja admite la autenticación global, permitiendo que todas las APIs estén protegidas directamente por defecto, y los desarrolladores solo necesitan manejar excepciones en rutas específicas para excluir las APIs a las que no deseen aplicarla.
La implementación es muy sencilla: Django Ninja proporciona directamente la clase de autenticación SessionAuth para manejar la autenticación basada en sesiones a nivel global.
Implementar autenticación global: usar SessionAuth#
Agrega el siguiente contenido en el api.py del proyecto:
# NinjaForum/api.py
from ninja.security import SessionAuth
...
api = NinjaAPI(
auth=SessionAuth(), # Configurar autenticación global
...
)
De esta manera, todas las APIs tienen protección de autenticación por defecto, y puedes excluirla en APIs específicas, como en «Inicio de sesión de usuario»:
En el decorador de ruta, define auth como None para desactivar la protección de autenticación.
Probar la protección de autenticación#
Probemos una API «con protección de autenticación». Descubrirás que sin haber iniciado sesión, al intentar probar APIs con diferentes métodos HTTP, obtendrás distintas respuestas de error:
- GET: 401 Unauthorized
- POST: 403 Forbidden
Es por esto que se dijo anteriormente que obtendrías una respuesta «401 o 403».
Probar la API «Obtener todos los usuarios»#
En el diseño de nuestro proyecto, solo los usuarios autenticados pueden acceder a la API «Obtener todos los usuarios».
Si no se ha iniciado sesión, obtendrás una respuesta 401:
Probar la API «Crear nuevo artículo»#
Sin iniciar sesión tampoco se puede acceder a la API «Crear nuevo artículo» (lo cual es obviamente muy razonable; de lo contrario, el artículo se quedaría sin autor 😅).
Obtendrás una respuesta 403:
Pensarás: «¿Qué raro? ¿Por qué dice CSRF check Failed?».
Este es el mecanismo de protección CSRF de Django. Como nuestra API utiliza el método POST, Django verifica automáticamente el token CSRF; dado que no proporcionamos el token CSRF, aparece este error.
Resumen y siguientes pasos#
En este artículo hemos explorado la integración de la autenticación de sesión de Django con Django Ninja, implementado la API de «inicio de sesión de usuario» y añadido protección de autenticación a otras APIs. Finalmente, mostramos cómo implementar la autenticación global para simplificar todo el proceso.
¡En la práctica final de esta serie vamos a escribir pruebas para el proyecto!
El próximo artículo explorará cómo usar el test client y pytest para escribir pruebas unitarias para nuestras APIs de Django. Esto no solo nos ayudará a verificar las funcionalidades existentes, sino que también ofrecerá una capa adicional de garantía para futuros desarrollos y refactorizaciones.