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-exampledas dicas anteriores, com o djoser configurado (Dica 47 e Dica 48). - A app
product, com o modelProduct(title,priceecreate_date) e a rotaproduct/products/, criada com o dr-scaffold na Dica 45. - Um usuário cadastrado. No vídeo, o usuário é
regiscom a senhalorem1200, definida na dica anterior. - Postman e curl, para testar.
Token tradicional x JWT

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 formatoAuthorization: Bearer <access>. Ele tem vida curta.refresh: serve para pedir um novoaccessemauth/jwt/refresh/, quando oaccessexpirar.
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 doaccess; aqui, 60 minutos. Defina como preferir.REFRESH_TOKEN_LIFETIME: o tempo de vida dorefresh; 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 comusernameepasswordno endpoint informado e devolve o JSON da resposta.get_token: chama ofetch_tokenemauth/jwt/create/e devolve um dicionário só com oaccess_token.fetch: faz o GET no endpoint passando o headerAuthorization: Bearer <access_token>.post_product: faz o POST de um novo produto, comtitleeprice, também com o headerAuthorization.main: o comando do click. Pedeusernameepassword(se não forem passados, ele pergunta no terminal), pega o token, imprime a lista de produtos e, se você passar--titlee--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.