Coffee bytes

Blog de desarrollo web con Python y Javascript

Autenticación usando JSON Web token JWT en Django

El lunes, 14 de junio de 2021

por Eduardo Zepeda

Tiempo de lectura: 6 minutos

Los JWT (JSON Web Tokens), se han popularizado enormemente, incluso algunos las consideran un reemplazo de los clásicos Tokens que usan otros frameworks, tales como DRF. Usar JWT o Tokens normales (SWT) permite guardar toda la información de nuestra sesión directo en el token y además están firmados criptográficamente, suena bien ¿no? Sigue leyendo hasta el final para profundizar al respecto.

¿Qué es JWT?

JWT es un estándar para la creación de tokens de acceso basado en JSON, para el intercambio de información entre dos partes. Estos tokens, y su contenido, pueden ser verificados porque están firmados digitalmente. Lo anterior garantiza que el contenido no ha sido alterado y que el emisor es quien dice ser. Lo anterior los vuelve perfectos para:

  • Autorización
  • Intercambio de información

Instalación de JWT en Django

Primero vamos a instalar las librerías necesarias: djangorestframework y djangorestframework_simplejwt. Para instalarlas usaré el administrador de entornos virtuales llamado Pipenv. También puedes usar pip si quieres.

pipenv install djangorestframework_simplejwt djangorestframework

Asegurate de agregar las aplicaciones que instalamos a la variable INSTALLED_APPS.

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'rest_framework_simplejwt'
]

Agregamos la clase de autenticación a nuestro archivo de configuraciones.

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

Recuerda correr las migraciones y crear un súper usuario, si puedes crear un par de cuentas a través del admin también te serían útiles.

python manage.py migrate
python manage.py createsuperuser

Vamos a agregar las url que necesitamos para generar nuestros JWT en Django

# urls.py
# ...
from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]

La primera vista nos devolverá un par de tokens; uno de acceso y otro para refrescar el primero. La segunda vista nos servirá para refrescar o actualizar el token de acceso.

Creemos también una vista protegida que sea accesible únicamente a los usuarios autenticados. Por simplicidad he puesto la función protegida dentro del mismo archivo urls.py

from django.contrib import admin
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

class Protegida(APIView):
    permission_classes = [IsAuthenticated]
    
    def get(self, request):
        return Response({"content": "Esta vista está protegida"})


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/token/', jwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
    path('protegida/', Protegida.as_view(), name='protegida')
]

Si probamos hacer una petición a la url /protegida/ nos advertirá de que no estamos mandando las credenciales adecuadas de autenticación.

curl http://127.0.0.1:8000/protegida/ {"detail":"Authentication credentials were not provided."}

Si no sabes usar curl revisa mi entrada de comandos básicos de GNU/Linux donde explico lo básico. También puedes usar Postman, http o cualquier otra opción.

Obteniendo los tokens

Si ahora hacemos una petición POST a la url /api/token/, enviando un nombre de usuario y contraseñas válidas tendremos de respuesta un par de tokens. Yo usé un usuario que cree, pero tú puedes usar tu superusuario o crear uno.

curl -d "username=kyoko&password=contrasenasegura" -X POST http://localhost:8000/api/token/

{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMzQ0NjEzNiwianRpIjoiMjcyOTI0OTkwOGVhNGQ2ZjkxMDFiMGI4ZjhlZDZkY2QiLCJ1c2VyX2lkIjoyfQ.zkCWbKBnkDCukZVB8cHiCnrUOHRl1vWF6Oqg29IFT7A",
"access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIzMzYwMDM2LCJqdGkiOiIzYzY2MDI3YzhiMjE0NmM4OGQ5NTY0MGUxYzc1ODAxYSIsInVzZXJfaWQiOjJ9.juG7sbemKUOTEnzNv4XiXCfChrG3q9wBw4Sj0g1L9EM"}

Token de acceso

El token de acceso sería el equivalente al token de acceso de DRF; usaremos este JWT para autenticarnos ante Django, para decirle a Django quienes somos.

Token de actualización

El token de acceso tiene una fecha de caducidad, una vez que esta fecha llegue dejará de ser valido, podemos crear otro sin necesidad de mandar nuestro usuario y contraseña usando únicamente el token de actualización.

Analizando los tokens

El token que recibimos está dividido por puntos en tres partes. La primera parte tiene el algoritmo que se usó, la segunda es la información que contiene el token, el último es la firma.

Observa como en la parte de contenido (data) se aprecia que el user_id es igual a 2, el cual es el id o primary key del usuario que obtuvo el token. El primer usuario en mi caso es el superusuario.

Autenticándonos con el token

Ahora intentemos usar el token de acceso que obtuvimos para acceder a la vista protegida. Asegurate de estar usando el token «access», no el de «refresh».

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIzxOTA5MmY4ZTJhNzNkZDM3YyIsInVzZXJfaWQiOjJ9.ibQPgQuEgnuTY6PGja-GLZv4TrAQtKKCgue_muJKlE4" http://127.0.0.1:8000/protegida/ {"content":"Esta vista está protegida"}

El token de acceso caduca

Si seguiste el ejemplo y dejaste que pasaran unos minutos te darás cuenta de que el token de acceso caduca y ya no será válido. El token de acceso tiene una duración predeterminada de 5 minutos, esto para evitar problemas si alguien logra interceptarlo.

curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIzxOTA5MmY4ZTJhNzNkZDM3YyIsInVzZXJfaWQiOjJ9.ibQPgQuEgnuTY6PGja-GLZv4TrAQtKKCgue_muJKlE4" http://127.0.0.1:8000/protegida/ {"detail":"Given token not valid for any token type","code":"token_not_valid","messages":[{"token_class":"AccessToken","token_type":"access","message":"Token is invalid or expired"}]}

Actualizar el token de acceso

Para obtener otro token válido basta que mandemos nuestro token de actualización al endpoint que creamos en /api/token/refresh/. El token de actualización tiene una duración predeterminada de 24 horas. Pasadas las 24 horas ya no podremos refrescar el token de acceso y tendremos que enviar nuevamente un nombre de usuario y contraseña.

curl -d "refresh=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYyMzQ0NjEzNiwianRpIjoiMjcyOTI0OTkwOGVhNGQ2ZjkxMDFiMGI4ZjhlZDZkY2QiLCJ1c2VyX2lkIjoyfQ.zkCWbKBnkDCukZVB8cHiCnrUOHRl1vWF6Oqg29IFT7A" -X POST http://127.0.0.1:8000/api/token/refresh/ {"access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjIzMzY1NDU3LCJqdGkiOiJlZjljNWFiYjI1MzU0YWJjYjc4YWRmNTI2MDA2OTEwNCIsInVzZXJfaWQiOjJ9.RPrfobpIF52W0wdNJk4zLYcgWpymZdgAPFxOIH0KEsk"}

Nuestra aplicación nos devolverá un nuevo token de acceso que podemos usar nuevamente para autenticarnos.

Modificar los valores por defecto de los JWT

Para hacerlo vamos al archivo de configuraciones de Django y creamos una variable llamada SIMPLE_JWT, en la que podemos sobreescribir los datos que querramos y colocarles la duración que más te convenga.

from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    # ...}

Por favor revisa la documentación oficial de django-rest-framework-simplejwt para ver todas las variables de configuración para tus JWT en Django.

Problemas con los JWT

Seguramente no quieres que tus usuarios estén colocando nombre de usuario y contraseña cada vez que usen tu aplicación, probablemente quieres conservar los valores de estos dos tokens para usarlos después y te estás preguntando cual es la opción correcta, ¿Local Storage o en las cookies?

Pues bien, la interrogante trae una serie de preguntas muy difíciles de contestar que dividen las opiniones de los desarrolladores y nos dejan sin una respuesta clara:

¿Como lidio con un JWT con información o permisos desactualizados? ¿Cuál es la mejor manera de invalidar un JWT un servidor externo o cambiar la llave critptográfica? ¿Qué pasa si la información que guardo en el JWT excede el tamaño permitido por cookie? Si en lugar de guardar contenido en el JWT solo guardo el identificador de usuario, ¿no es lo mismo que una cookie?

Para la siguiente publicación traduciré una entrada bastante popular llamada «Stop using JWT for sessions» (No uses JWT para gestionar sesiones), con una postura muy fuerte, que trata sobre esas preguntas.

Presume lo que aprendiste en redes

Posts de calidad en tu inbox

Regularmente publico posts relacionados con desarrollo web, principalmente con Python y Javascript. Suscríbete para recibirlos directamente en tu inbox.

* Campo obligatorio

Hola, ¿te está sirviendo el post?

Recibe mis posts por correo electrónico totalmente gratis. O por lo menos sígueme en Twitter. Me motivas a seguir creando contenido gratuito

* Campo obligatorio