close icon
Python

Como Lidar com JWTs em Python

Aprenda a criar, analisar e verificar seus JWTs em Python usando PyJWT

October 28, 2021

Os JSON Web Tokens, ou JWTs, estão em toda a Internet. Eles podem ser usados ​​para rastrear bits de informação sobre um usuário de uma forma muito compacta ou ​​em APIs para fins de autorização. Este artigo vai mostrar o que são os JSON Web Tokens e como criar JWTs no Python usando a biblioteca JWT mais popular: PyJWT. Você também vai descobrir como você pode assinar e verificar JWTs no Python usando algoritmos simétricos e algoritmos assimétricos.

Antes de começar, há uma coleção de scripts, com todos os códigos que abordarei neste artigo, disponível aqui neste repositório GitHub.

Se preferir, você pode assistir aos vídeos abaixo, onde eu explico tudo sobre os JWTs! 👇

Dica: Esses vídeos tem legenda em Português, não esqueça de ativá-las. 😉

Introdução à JSON Web Tokens

JSON Web Tokens são uma forma muito compacta de transmitir informações. São definidos com uma estrutura de 3 partes que consiste em um cabeçalho (header), um corpo (payload) e uma assinatura (signature). O header e o payload contém declarações (claims) sobre uma entidade e todos os dados adicionais que precisam ser passados ​​na solicitação:

  • No cabeçalho, encontramos declarações sobre o próprio token, como qual algoritmo foi usado para assinar esse token;
  • Enquanto o corpo carrega informações sobre um determinado ativo. Em um cenário de login, seriam informações sobre o usuário.

A parte final é a assinatura que ajuda a garantir que um determinado token não foi adulterado, porque a assinatura de um JWT requer um segredo ou um par de chaves pública/privada acordadas previamente. A assinatura em si é baseada no header e no payload, em combinação com uma chave secreta (comumente uma string randômica também chamada de segredo) ou par de chaves pública/privada, dependendo do algoritmo.

As declarações seguem o padrão de par chave/valor que você vê em dicionários e objetos JSON, e a maioria das declarações usadas em JWTs tem uma nomenclatura padronizada definida pela especificação de JWTs definida na RFC7519. A RFC7519 também contém a descrição do que significa cada uma das declações padrão.

Se você quiser saber mais sobre JWTs, deve conferir esta página em inglês que fala sobre JSON Web Tokens de uma forma muito prática, ou se quiser se aprofundar, recomendo o "JWT Handbook", disponível gratuitamente no link abaixo (em inglês).

Interested in getting up-to-speed with JWTs as soon as possible?

Download the free ebook
JWT Handbook

Embora seja bom ler definições e explicações, o melhor é ver como algo funciona. No restante deste artigo, você aprenderá sobre os JSON Web Tokens, criando, assinando, verificando e decodificando seu próprio JWT.

Requisitos

Para seguir os passos deste artigo, você vai precisar de:

  • Python 3.6 ou superior, eu estou usando 3.8.2;
  • Um ambiente virtual Python ativado, se você já sabe como criar seu próprio ambiente, pode pular para a seção "Instalando requisitos";
  • PyJWT com a dependência cryptography instalada. Confira como instalá-lo na seção "Instalando requisitos";
  • Também vou usar o iPython, um console Python interativo alternativo ao console tradicional, mas fique à vontade para escolher a interface ou console Python que preferir. 😉

Criando seu ambiente virtual Python

Para criar um ambiente, você deve criar e navegar até sua pasta de trabalho. Gosto de usar os seguintes comandos:

mkdir jwts-in-python
cd jwts-in-python

Depois disso, crie um ambiente chamado .env:

python3 -m venv .env

E depois que o ambiente for criado, você deve ativá-lo e instalar a versão mais recente de pip:

source .env/bin/activate
pip install -U pip

Observe que o comando para ativar seu ambiente irá variar de acordo com seu sistema operacional (SO). Nesta página da documentação do Python, você encontra uma lista de todas as maneiras de ativar um ambiente para ver qual funciona melhor para o seu sistema operacional.

Gerando um par de chaves RSA

Para assinar seus tokens com um algoritmo assimétrico como RS256, você precisará de um par de chaves pública/privada que explicarei mais sobre na próxima seção. É possível que você já tenha um par de chaves, mas, caso precise gerar um novo par, aqui está um exemplo:

mkdir ~/jwts-in-python/.ssh
ssh-keygen -t rsa

Observe que se você estiver usando o Windows como sistema operacional, dependendo da versão, será necessário instalar o OpenSSH para ter acesso à ferramenta ssh-keygen. Neste tutorial (em inglês), você vai encontrar todas as maneiras de gerar um par de chaves no Windows.

E se quiser ter o par de chaves dentro do diretório de trabalho, lembre-se de passar o caminho para o seu diretório quando for solicitado. O caminho também deve conter o nome da chave privada. No meu caso, usei o seguinte caminho, caso você queira copiá-lo:

~/jwts-in-python/.ssh/id_rsa

Além disso, será solicitada uma senha. Para simplificar, o par de chaves que gerei não tem uma senha. Lembre-se de que você sempre deve usar senhas ao gerar chaves RSA para garantir um nível extra de segurança.

Instalando requisitos

Para fazer tudo o que abordarei neste artigo, você precisará instalar o PyJWT com o pacote cryptography como uma dependência. Essa dependência dará a você a capacidade de assinar e verificar JWTs assinados com algoritmos assimétricos.

pip install pyjwt[crypto]

Se você também deseja instalar o iPython, você pode fazer isso da seguinte forma:

pip install ipython

Agora você tem tudo o que precisa. 🎉

Crie um JWT em Python

Vou ensinar a criar um JWT porque, ao entender como um token é criado, você entenderá melhor como lidar com os JWTs.

Lembre-se de que se estiver usando uma plataforma como a Auth0, você não deve criar seus tokens; eles serão fornecidos pela própria plataforma. Como os JWTs podem ser lidos por qualquer pessoa, desde que tenham o segredo ou a chave pública, é realmente importante seguir os padrões do setor para evitar complicações como violações de dados e de segurança.

Abra seu console. Se você estiver usando iPython, você deve digitar ipython no terminal e pressionar Enter. Depois pode começar a codificar:

mensagem inicial do ipython, o console interativo e alternativo de Pytho

A primeira coisa que você precisa fazer é importar o objeto jwt que vem do pacote PyJWT:

import jwt

Antes de gerar um token, você deve criar alguns dados para passar pelo corpo do JWT e uma chave secreta para assinar o token usando o algoritmo HS256. Então, crie um dicionário para conter alguns dados de um usuário fictício e o segredo:

payload_data = {
    "sub": "4242",
    "name": "Jessica Temporal",
    "nickname": "Jess"
}

my_secret = 'my_super_secret'

Os dados de payload possuem três declarações:

  • sub: que é o identificador do usuário ou "assunto" deste token;
  • name: que é o nome completo do usuário;
  • nickname: também o apelido do usuário.

Lembre-se de que você está usando alguns dados fictícios que informam quem é o usuário neste exemplo, e agora a parte difícil está praticamente concluída!

Assine um token com um algoritmo de hashing

A seguir, você vai assinar o token com os dados que acabou de criar. Ao assinar um token, podemos ter certeza de que a integridade das declarações no token é verificável. Vamos usar o algoritmo HMAC (um algoritmo simétrico) primeiro. Um algoritmo simétrico usa uma função de hashing e uma chave secreta que ambas as partes usarão para gerar e validar a assinatura. Geralmente recomendamos o uso do algoritmo RS256, que é um algoritmo assimétrico, na próxima seção, você verá como assinar um token com um algoritmo assimétrico. Agora você já pode chamar o método encode do objeto jwt, passar o dicionário que acabou de criar e deixar que esse método faça a mágica por você. E com isso, quero dizer que o método encode se encarrega de criar o cabeçalho padrão, codificar tudo e assinar o token com o segredo:

token = jwt.encode(
    payload=payload_data,
    key=my_secret
)

Antes de continuar, quero de destacar três coisas:

  • A primeira é que o parâmetro key funciona para uma chave ou um segredo. Nesse caso, estou usando um segredo porque o algoritmo usado por padrão no método encode é o HS256, que requer apenas um segredo para assinar;
  • Isso me leva ao segundo ponto, que na realidade, você terá um segredo real sendo usado em vez desta amostra;
  • E o terceiro é se você estivesse usando um algoritmo assimétrico RS256 para assinar o token, precisaria usar a chave privada para a assinatura, e temos um exemplo disso mais adiante.

Se você imprimir o token, você verá esta enorme string:

token
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MjQyIiwibmFtZSI6Ikplc3NpY2EgVGVtcG9yYWwiLCJuaWNrbmFtZSI6Ikplc3MifQ.EDkUUxaM439gWLsQ8a8mJWIvQtgZe0et3O3z4Fd_J8o

E você pode copiar essa string e usá-la onde quiser. Por exemplo, no jwt.io.

Captura de tela com o comecinho da página JWT.io

jwt.io é uma ferramenta muito útil porque você pode usá-la em qualquer lugar, desde que tenha uma conexão com a internet, para que possa verificar a assinatura e o conteúdo de um determinado token diretamente no seu navegador. Você ainda pode usar jwt.io para criar tokens também no seu navegador!

Captura de tela com JWT.io mostrando a assinatura inválida depois de colar o token no campo correspondente

Se você nunca usou o site jwt.io antes e essa é a primeira vez que está colando seu token na área do token ("Encoded"), você deve estar vendo que a assinatura é inválida, como na imagem acima ("Invalid Signature"). Isso ocorre porque você precisa fornecer o segredo correto no campo "secret" da seção "Verify Signature". Depois de corrigir a chave secreta, a assinatura do token é verificada, como pode ver na imagem abaixo!

Captura de tela com JWT.io mostrando a assinatura válida depois de corrigir o segredo no campo "secret"

Na imagem acima também possível observar que o token possui um cabeçalho com duas declarações que o PyJWT adicionou ao criar esse token:

  • typ: usada para dizer o tipo de token;
  • alg: usada para dizer qual algoritmo foi usado para assinar o JWT.

E você também pode verificar os dados que passamos no corpo do token. É isso! Você acabou de gerar e verificar um token! 🎉

Assine um token com RS256 um algoritmo assimétrico

Até agora, você usou o HS256, um algoritmo de hashing, para assinar um token. Mas se você quiser levar sua assinatura de token um passo adiante, você pode usar algoritmos assimétricos. Essa é uma boa maneira de assinar tokens porque, se você tiver uma chave pública hospedada em algum lugar, digamos, em seu site, qualquer pessoa pode verificar se você assinou esse token. Apenas recapitulando, algoritmos assimétricos como o RS256 são aqueles algoritmos que usam uma chave privada para assinar e uma chave pública para verificar a assinatura.

É aqui que o pacote cryptography entra em jogo. Você precisará importar o módulo serialization do pacote para carregar uma chave RSA. Para simplificar, gerei um par de chaves pública/privada usando ssh-keygen sem senha na pasta .ssh dentro do meu diretório de trabalho. Agora tudo o que você precisa fazer é carregar essas chaves:

# first import the module
from cryptography.hazmat.primitives import serialization
# read and load the key
private_key = open('.ssh/id_rsa', 'r').read()
key = serialization.load_ssh_private_key(private_key.encode(), password=b'')

Vamos entender o que está acontecendo no exemplo acima:

  • Primeiro, uso a função open em combinação com o método read para obter a chave como uma string e, em seguida, guardo a chave na variável private_key;
  • Depois uso o serialization.load_ssh_private_key e passo a string encoded da chave privada porque o método load_ssh_private_key precisa receber um objeto do tipo string de bytes. Além disso, você pode notar que o parâmetro password recebeu uma string de bytes vazia, porque minha chave não requer uma senha. Se a sua exigir uma senha, lembre-se de atualizar esse valor de acordo.

E a parte pesada do trabalho para você começar a usar algoritmos assimétricos para assinatura está quase chegando ao final! Tudo o que você precisa fazer é passar a variável key para o parâmetro key enquanto faz o encoding do token e ajustar o valor do parâmetro do algorithm. Veja como fica o resultado recriando o token feito antes:

new_token = jwt.encode(
    payload=payload_data,
    key=key,
    algorithm='RS256'
)

E se você imprimir o token, verá algo assim:

new_token
# 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI0M...81gVns6I_j4kSuyuRxlAJBe3pHi-yS2'

Verifique um JWT

A esta altura, você deve estar se perguntando: "Legal! Eu sei como verificar um token usando jwt.io e assinar meu próprio token usando algoritmos diferentes. Mas como faço para verificar um token usando Python?".

E para responder à sua pergunta, aqui está: com o PyJWT, verificar um token assinado com um algoritmo de hashing exige apenas uma linha de código! Você precisa apenas usar o decode e passar para o método o token e o segredo desta forma:

jwt.decode(token, key='my_super_secret', algorithms=['HS256', ])
# {'sub': '4242', 'name': 'Jessica Temporal', 'nickname': 'Jess'}

Observe que o resultado aqui é o payload, o que significa que você verificou o token com sucesso. Se a verificação tivesse falhado, você veria um InvalidSignatureError, dizendo que a verificação da assinatura falhou (Signature verification failed).

Além disso, essa etapa foi simples porque você já sabe que o token foi gerado usando o HS256 e também já sabe a chave necessária decodificá-lo. Mas digamos que você não saiba qual algoritmo foi usado para gerar esse token, certo? Então você pode acessar jwt.io novamente e verificar o conteúdo do cabeçalho para encontrar a declaração alg ou pode usar o PyJWT para fazer isso.

Encontre o algoritmo usado em um JWT

Você poderia verificar o conteúdo do cabeçalho manualmente se quisesse: para isso precisaria separar a string por cada ponto, decodificar a parte do cabeçalho e assim por diante, mas adivinhe? O PyJWT é uma ferramenta tão boa que existe um método para fazer tudo isso! 🎉

Em vez de fazer tudo isso manualmente, você pode usar o get_unverified_header, veja só:

jwt.get_unverified_header(token)
# {'typ': 'JWT', 'alg': 'RS256'}

Como você pode ver no exemplo acima, você deve chamar o get_unverified_header a partir do jwt e passar o token para o método. Como resultado, você obterá um dicionário com os dados do cabeçalho. Agora você pode armazená-lo em uma variável para usar esses dados para tornar seus scripts um pouco mais inteligentes. Vamos ver como isso funcionaria no exemplo abaixo:

# saving the header claims into a variable
header_data = jwt.get_unverified_header(token)
# using that variable in the decode method
jwt.decode(
    token,
    key='my_super_secret',
    algorithms=[header_data['alg'], ]
)

Com essa atualização, seu código encontra o algoritmo usado para assinar um token quase como mágica!

Decodifique um token com um algoritmo assimétrico

Vamos combinar tudo o que você fez até agora e verificar a assinatura de um token que o algoritmo usado era assimétrico. Então, use esse outro token que você vê abaixo que foi criado usando RS256 e uma chave SSH privada.

token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI0MjQyIiwibmFtZSI6Ikplc3NpY2EgVGVtcG9yYWwiLCJuaWNrbmFtZSI6Ikplc3MifQ.HgHJPl6b5W0CiDz4cNuyRcs5B3KgaoRbMvZBgCkcXOSOCAc0m7R10tSm6d86u8oW8NgzGoIAlKxBw0CIPhdx5N7MWTE2gshzQqhuq5MB9tNX1pYrLsiOMbibeMasvcf97Kd3JiLAzPPJe6XXB4PNL4h_4RcW6aCgUlRhGMPx1eRkGxAu6ndp5zzWiHQH2KVcpdVVdAwbTznLv3OLvcZqSZj_zemj__IAZPMkBBnhdjYPn-44p9-xrNmFZ9qBth4Ps1ZC1_A6lH77Mi1zb48Ou60SUT1-dhKLU09yY3IX8Pas6xtH6NbZ-e3FxjofO_OL47p25CvdqMYW50JVit2tjU6yzaoXde8JV3J40xuQqwZeP6gsClPJTdA-71PBoAYbjz58O-Aae8OlxfWZyPsyeCPQhog5KjwqsgHUQZp2zIE0Y50CEfoEzsSLRUbIklWNSP9_Vy3-pQAKlEpft0F-xP-fkSf9_AC4-81gVns6I_j4kSuyuRxlAJBe3pHi-yS2'

Para fazer isso, você precisará carregar a chave pública (eu forneci essa chave neste repositório GitHub aqui e carregar a chave pública leva as mesmas 3 etapas que você executou para carregar a chave privada, apenas ajustando o método usado para que carregue uma chave pública e o caminho da chave, veja só:

# first import the module
from cryptography.hazmat.primitives import serialization
# read and load the key
public_key = open('.ssh/id_rsa.pub', 'r').read()
key = serialization.load_ssh_public_key(public_key.encode())

Agora que você tem um token e a chave pública, pode decodificá-lo. Você só precisa fazer a mesma coisa que fez antes, chamar o método jwt.decode, passando o token, a chave e o algoritmo usado:

jwt.decode(jwt=token, key=key, algorithms=['RS256', ])
# {'sub': '4242', 'name': 'Jessica Temporal', 'nickname': 'Jess'}

É isso! Você verificou o JWT assinado com um algoritmo assimétrico! 🎉

Confira a Data de Validade de Um JWT

Outra coisa que você deve fazer ao validar um JWT é verificar se ele expirou ou não, ou melhor ainda, o serviço que você está criando não deve aceitar tokens expirados. O token no exemplo abaixo tem uma data de validade (exp) definida no passado para você que está lendo este artigo hoje. Vamos nos preparar para decodificar o token:

token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJKZXNzIFRlbXBvcmFsIiwiZXhwIjoxNTE2MjM5MDIyfQ.uqeQ60enLaCQEZ-7C0d_cgQSrWfgXRQuoB1LZD0j06E'

header_data = jwt.get_unverified_header(token)

E se você tentar decodificá-lo hoje vai ver o erro ExpiredSignatureError, vá em frente, experimente:

payload = jwt.decode(
    token,
    key='my_super_secret',
    algorithms=[header_data['alg'], ]
)

Como o PyJWT é uma ferramenta excelente, ele já cuidou de lidar com esta verificação de validade para você, portanto, se você tentar decodificar um token expirado, você verá um erro como na imagem abaixo:

Captura de tela com o erro ExpiredSignatureError ao tentar validar um token vencido

Para evitar o erro ExpiredSignatureError seu código deve ter uma cláusula try/except para tratá-lo, algo assim:

from jwt.exceptions import ExpiredSignatureError

try:
    payload = jwt.decode(
        token,
        key='my_super_secret',
        algorithms=[header_data['alg'], ]
    )
except ExpiredSignatureError as error:
    print(f'Unable to decode the token, error: {error}')

A maioria dos erros que você pode enfrentar ao verificar os tokens já está implementada no módulo jwt.exceptions. Você precisa apenas se lembrar de usá-los e certificar-se de que seu código esteja bem preparado para lidar com eles conforme aparecerem.

Se você precisar de mais detalhes sobre as etapas necessárias para validar os tokens, recomendo a leitura da documentação da Auth0 (em inglês) sobre o assunto.

Concluindo

Os JWTs são fantásticos e podem ser usados ​​em muitos cenários. Aprender como lidar com eles é algo que toda pessoa desenvolvedora web precisa entender algum dia, e embora eu apenas tenha mostrado como é simples criar, verificar e decodificar JWTs em Python, vale a pena mencionar que há muito espaço para erros quando usamos JWTs para autorização.

A quebra controle de acesso, do inglês broken access control, é consistentemente parte dos dez principais riscos de segurança de aplicativos web da OWASP, portanto, é importante seguir os padrões da indústria e saber o que pode dar errado se você estiver projetando sua própria solução de autorização.

Se você quiser se livrar do fardo de gerenciar autorização por conta própria, você pode criar uma conta gratuita na Auth0 e começar a usar com JWTs em apenas alguns minutos.

E agora me conte, você usa JWTs? Qual pacote ou biblioteca você mais gosta para trabalhar com eles?

Deixe seus comentários e perguntas no fórum da comunidade. 😉

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon