Pular para o conteúdo
dicas de djangopor Regis do Python

Dica 49 - DRF: Autenticação via JWT com djoser - Django REST framework

7 min de leitura
Assistir ao vídeo no YouTube: Dica 49 - DRF: Autenticação via JWT com djoser - Django REST framework

Versões usadas no vídeo: Django 3.2.7, Django REST framework 3.12, djoser 2.1, djangorestframework-simplejwt 4.8.0 e Python 3.9.

Github: https://github.com/rg3915/drf-example

https://djoser.readthedocs.io/en/latest/jwt_endpoints.html

JSON Web Token Authentication: https://djoser.readthedocs.io/en/latest/authentication_backends.html#json-web-token-authentication

Continuando a série sobre Django REST framework, vamos ver como autenticar na API usando JWT (JSON Web Token). Continuamos usando o djoser, que já traz os endpoints de JWT prontos, com a ajuda do djangorestframework-simplejwt. No fim, vamos escrever um pequeno cliente em Python que consome a API autenticando com JWT.

Pré-requisitos

  • O projeto drf-example das dicas anteriores, com o djoser configurado (Dica 47 e Dica 48).
  • A app product, com o model Product (title, price e create_date) e a rota product/products/, criada com o dr-scaffold na Dica 45.
  • Um usuário cadastrado. No vídeo, o usuário é regis com a senha lorem1200, definida na dica anterior.
  • Postman e curl, para testar.

Token tradicional x JWT

jwt.png

Na autenticação tradicional por token (a da Dica 47), você informa username e password em auth/token/login/ e recebe um token que fica guardado no banco.

Com JWT, você também informa username e password, mas no endpoint auth/jwt/create/, e recebe dois tokens:

  • access: o token que vai no header de cada requisição, no formato Authorization: Bearer <access>. Ele tem vida curta.
  • refresh: serve para pedir um novo access em auth/jwt/refresh/, quando o access expirar.

São três endpoints novos: jwt/create/, jwt/refresh/ e jwt/verify/.

Instalação

Com a virtualenv ativa, na pasta do projeto:

pip install djoser djangorestframework-simplejwt
pip freeze | grep jwt >> requirements.txt

O requirements.txt fica assim:

click==8.0.*
django-extensions
Django==3.2.*
djangorestframework==3.12.*
djoser==2.1.*
dr-scaffold==1.4.*
drf-yasg==1.20.*
python-decouple
djangorestframework-simplejwt==4.8.0

settings.py

Edite backend/settings.py. Importe o timedelta:

# backend/settings.py
from datetime import timedelta
from pathlib import Path

from decouple import config

No REST_FRAMEWORK, acrescente a classe JWTAuthentication do simplejwt. Não é preciso colocar o rest_framework_simplejwt no INSTALLED_APPS: o djoser cuida dessa parte. O IsAuthenticated já estava configurado.

E crie o dicionário SIMPLE_JWT:

# backend/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',  # <--
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}


SIMPLE_JWT = {
    "AUTH_HEADER_TYPES": ("Bearer",),  # na doc está JWT mas pode mudar pra Bearer.
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    # "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
}
  • AUTH_HEADER_TYPES: a palavra que vai antes do token no header. Na documentação do djoser está ('JWT',), mas o mais comum, que você encontra na maioria dos lugares, é Bearer.
  • ACCESS_TOKEN_LIFETIME: o tempo de vida do access; aqui, 60 minutos. Defina como preferir.
  • REFRESH_TOKEN_LIFETIME: o tempo de vida do refresh; aqui, um dia.

urls.py

Edite backend/urls.py e acrescente as urls de JWT do djoser:

# backend/urls.py
urlpatterns += [
    path('api/v1/', include('djoser.urls')),
    path('api/v1/auth/', include('djoser.urls.authtoken')),
    path('api/v1/auth/', include('djoser.urls.jwt')),  # <--
]

Exigindo autenticação nos produtos

Para ter onde testar, vamos exigir autenticação na lista de produtos:

# product/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from product.models import Product
from product.serializers import ProductSerializer


class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    permission_classes = (IsAuthenticated,)  # <--

Rode o servidor:

python manage.py runserver

Testando no Postman

POST: http://localhost:8000/api/v1/auth/jwt/create/

Em Body, raw, JSON:

{
    "username": "regis",
    "password": "lorem1200"
}

A resposta traz o refresh e o access:

{
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

Copie o access e faça um GET em http://localhost:8000/product/products/. Na aba Headers, acrescente:

KEY VALUE
Authorization Bearer <access>

(a palavra Bearer, um espaço, e o token que você copiou).

Com o header, a resposta é 200 OK com a lista de produtos. Sem o header, a resposta é 401 Unauthorized:

{
    "detail": "As credenciais de autenticação não foram fornecidas."
}

Testando com curl

O login tradicional, por token, continua funcionando:

curl -X POST http://127.0.0.1:8000/api/v1/auth/token/login/ --data 'username=admin&password=d'

E o login via JWT:

curl -X POST \
http://127.0.0.1:8000/api/v1/auth/jwt/create/ \
--data 'username=regis&password=lorem1200'

Ele retorna um novo refresh e um novo access:

{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...","access":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}

Pegue o access e use no header Authorization:

curl -X GET \
http://127.0.0.1:8000/product/products/ \
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLC...'

Resposta:

[{"url":"http://127.0.0.1:8000/product/products/1/","title":"banana","price":"7.50","create_date":"2021-08-31T06:18:06.794070Z"},{"url":"http://127.0.0.1:8000/product/products/2/","title":"laranja","price":"8.00","create_date":"2021-09-05T03:09:56.703361Z"},...]

Quando o access expirar, peça um novo usando o refresh:

# atualizar
curl -X POST \
http://127.0.0.1:8000/api/v1/auth/jwt/refresh/ \
-H 'Content-Type: application/json' \
--data '{"refresh": ""}'

(coloque o seu refresh entre as aspas). A resposta traz um novo access.

Consumindo a API com Python

Agora vamos criar um cliente, consumer.py, na raiz do projeto, que pega o token JWT e consome a lista de produtos. Ele usa o requests e o click (para ler as opções da linha de comando).

pip install requests click
# consumer.py
# ... (veja o arquivo completo no GitHub)
from pprint import pprint
from typing import Dict

import click
import requests
from requests.auth import HTTPBasicAuth

BASE_URL = 'http://localhost:8000/api/v1/'

# ... (fetch_token: veja o arquivo completo no GitHub)


def get_token(username: str, password: str) -> Dict[str, str]:
    '''
    Pega o access_token do usuário logado.
    '''
    with requests.Session() as session:
        endpoint = f'{BASE_URL}auth/jwt/create/'
        response = fetch_token(session, endpoint, username, password)
        data = {
            'access_token': response['access'],
        }
        return data


def fetch(session, endpoint, access_token):
    '''
    Faz a autenticação usando JWT.
    '''
    headers = {'Authorization': f'Bearer {access_token}'}
    with session.get(endpoint, headers=headers) as response:
        return response.json()

# ... (post_product: veja o arquivo completo no GitHub)


@click.command()
@click.option('--username', '-u', prompt='username', help='Type the username.')
@click.option('--password', '-p', prompt='password', help='Type the password.')
@click.option('--title', '-t', help='Type the title.')
@click.option('--price', '-pr', help='Type the price.')
def main(username, password, title=None, price=None):
    '''
    Consumindo a lista de produtos.
    '''
    token = get_token(username, password)
    access_token = token['access_token']
    with requests.Session() as session:
        endpoint = 'http://127.0.0.1:8000/product/products/'
        response = fetch(session, endpoint, access_token)
        pprint(response)

        if title and price:
            print(f'Salvando produto: {title}')
            post_product(session, endpoint, access_token, title, price)

# ... (veja o arquivo completo no GitHub)

Código completo: consumer.py

O que cada função faz:

  • fetch_token: faz o POST com username e password no endpoint informado e devolve o JSON da resposta.
  • get_token: chama o fetch_token em auth/jwt/create/ e devolve um dicionário só com o access_token.
  • fetch: faz o GET no endpoint passando o header Authorization: Bearer <access_token>.
  • post_product: faz o POST de um novo produto, com title e price, também com o header Authorization.
  • main: o comando do click. Pede username e password (se não forem passados, ele pergunta no terminal), pega o token, imprime a lista de produtos e, se você passar --title e --price, cadastra um produto novo.

O servidor tem que estar rodando; senão, você recebe um requests.exceptions.ConnectionError (Connection refused). Com o runserver no ar:

python consumer.py

Ele pergunta o usuário e a senha e imprime os produtos. Passando tudo pela linha de comando, e cadastrando um produto:

python consumer.py -u regis -p lorem1200 -t Batata -pr 5.52

Saída (o fim dela):

 {'create_date': '2021-09-05T04:02:27.218256Z',
  'price': '9.00',
  'title': 'melancia',
  'url': 'http://127.0.0.1:8000/product/products/4/'}]
Salvando produto: Batata
<Response [201]>
{'create_date': '2021-09-30T01:48:12.186273Z',
 'price': '5.52',
 'title': 'Batata',
 'url': 'http://127.0.0.1:8000/product/products/5/'}

Pronto: a API está autenticando via JWT, tanto no Postman e no curl quanto num cliente Python.

Observação: no simplejwt 4.8.0, o endpoint jwt/verify/ dá erro (BlacklistedToken sem objects) quando a app rest_framework_simplejwt.token_blacklist não está no INSTALLED_APPS. O vídeo não usa esse endpoint; o jwt/create/ e o jwt/refresh/, usados aqui, funcionam normalmente.

Sugerir uma correção nesta página