diff --git a/aulas/06.md b/aulas/06.md index 3724c1ac..6914284b 100644 --- a/aulas/06.md +++ b/aulas/06.md @@ -118,10 +118,10 @@ Nos próximos tópicos, vamos detalhar como podemos gerar e verificar tokens JWT ## Gerando tokens JWT -Para gerar tokens JWT, precisamos de duas bibliotecas extras: `pyjwt` e `pwdlib`. A primeira será usada para a geração do token, enquanto a segunda será usada para criptografar as senhas dos usuários. Para instalá-las, execute o seguinte comando no terminal: +Para gerar tokens JWT, precisamos de uma nova biblioteca que ainda não temos, a `pyjwt`. Que será usada para gerar nossos tokens seguindo as especificações necessárias na RFC: ```shell title="$ Execução no terminal!" -poetry add pyjwt "pwdlib[argon2]" +poetry add pyjwt ``` Agora, criaremos uma função para gerar nossos tokens JWT. Criaremos um novo arquivo para gerenciar a segurança: `security.py`. Nesse arquivo iniciaremos a geração dos tokens: @@ -131,12 +131,10 @@ from datetime import datetime, timedelta from zoneinfo import ZoneInfo from jwt import encode -from pwdlib import PasswordHash SECRET_KEY = 'your-secret-key' # Isso é provisório, vamos ajustar! ALGORITHM = 'HS256' ACCESS_TOKEN_EXPIRE_MINUTES = 30 -pwd_context = PasswordHash.recommended() def create_access_token(data: dict): @@ -155,7 +153,7 @@ Note que a constante `SECRET_KEY` é usada para assinar o token, e o algoritmo ` ### Testando a geração de tokens -Embora esse código será coberto no futuro com a utilização do token, é interessante criarmos um teste para essa função com uma finalidade puramente didática. De forma que vejamos os tokens gerados pelo `pyjwt` e interagirmos com ele. +Embora a função `create_access_token` deva ser usada em diferentes contextos do código, o que criaria uma cobertura para ela,, é interessante criarmos um teste para essa função com uma finalidade puramente didática. De forma que vejamos os tokens gerados pelo `pyjwt` e interagirmos com ele. Com isso criaremos um arquivo chamado `tests/test_security.py` para efetuar esse teste: @@ -166,38 +164,97 @@ from fast_zero.security import SECRET_KEY, create_access_token def test_jwt(): - data = {'test': 'test'} - token = create_access_token(data) + data = {'test': 'test'} #(1)! + token = create_access_token(data) #(2)! - decoded = decode(token, SECRET_KEY, algorithms=['HS256']) + decoded = decode(token, SECRET_KEY, algorithms=['HS256']) #(3)! assert decoded['test'] == data['test'] - assert decoded['exp'] # Testa se o valor de exp foi adicionado ao token + assert decoded['exp'] #(4)! ``` +1. Dados que serão assinados pelo token JWT. +2. Criação do nosso token JWT. O valor da variável `token` nesse momento deve ser algo parecido com isso `'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImV4cCI6MTczNjcwMTc4NX0.sFK48Oy6EbjkHgMMm272p-a6eAClKGP1Oo8ISDMNiuo'` +3. Nessa linha estamos chamando a função `decode` da própria biblioteca do jwt e passamos nosso token, o algorítimo que assinou e a nossa secret key. O resultado da função `decode` deve ser o valor que passamos para a assinatura `#!py {'test': 'test'}` adicionado a claim que adicionamos na função `create_access_token`. Algo como `#!py {'test': 'test', 'exp': 1736701785}`. +4. Checa somente se existe a claim de exp no token decodado. + +Agora podemos executar nosso teste e ver se tudo funciona como o esperado: + +```shell title="$ Execução no terminal!" +task test + +# ... + +tests/test_security.py::test_jwt PASSED +``` + +??? danger "Esse teste deu errado?" + Uma coisa que pode acontecer aqui, por conta de `create_access_token` usar funções de [fuso horário (timezone)](https://pt.wikipedia.org/wiki/Fuso_hor%C3%A1rio){:target="_blank"}: + + ```python + def create_access_token(data: dict): + # ... + expire = datetime.now(tz=ZoneInfo('UTC')) + timedelta( + minutes=ACCESS_TOKEN_EXPIRE_MINUTES + ) + # ... + ``` + + Caso o seu sistema operacional não tenha as propriedades para 'UTC' previamente configuradas, o python não saberá lidar com o fuso. Uma forma de passar por isso sem alterar as configurações do seu sistema operacional é instalar a biblioteca tzdata: + + ```shell title="$ Execução no terminal!" + poetry add tzdata + ``` + + Após isso, tente executar o teste novamente. + Na próxima seção, veremos como podemos usar a biblioteca `pwdlib` para tratar as senhas dos usuários. + ## Hashing de Senhas -Armazenar senhas em texto puro é uma prática de segurança extremamente perigosa. Em vez disso, é uma prática padrão criptografar ("hash") as senhas antes de armazená-las. Quando um usuário tenta se autenticar, a senha inserida é criptografada novamente e comparada com a versão criptografada armazenada no banco de dados. Se as duas correspondem, o usuário é autenticado. +Armazenar senhas em texto puro é uma prática de segurança extremamente perigosa. Qualquer pessoa mal intencionada com acesso ao banco poderia ver as credenciais na base de dados. Para evitar isso, uma prática padrão criptografar ("hash") as senhas antes de armazená-las. Quando um usuário tenta se autenticar, a senha inserida é criptografada novamente e comparada com a versão criptografada armazenada no banco de dados. Se as duas correspondem, o usuário é autenticado. + +Implementaremos essa funcionalidade usando a biblioteca `pwdlib` (password lib), que ainda não temos instalada: + +```shell title="$ Execução no terminal!" +poetry add "pwdlib[argon2]" #(1)! +``` -Implementaremos essa funcionalidade usando a biblioteca `pwdlib`. Criaremos duas funções: uma para criar o hash da senha e outra para verificar se uma senha inserida corresponde ao hash armazenado. Adicione o seguinte código ao arquivo `security.py`: +1. [Argon2](https://en.wikipedia.org/wiki/Argon2){:target="_blank"} é algorítimo de hash bastante seguro e confiável. + +Para lidar com a senha de forma segura, criaremos duas funções: uma para criar o hash da senha e outra para verificar se uma senha inserida corresponde ao hash armazenado. Adicione o seguinte código ao arquivo `security.py`: + +```python title="fast_zero/security.py" hl_lines="3 7" +# ... outros imports +from jwt import encode +from pwdlib import PasswordHash + +#... Outras constantes +ACCESS_TOKEN_EXPIRE_MINUTES = 30 +pwd_context = PasswordHash.recommended() #(1)! + +# ... outras funções -```python title="fast_zero/security.py" linenums="23" def get_password_hash(password: str): - return pwd_context.hash(password) + return pwd_context.hash(password) #(2)! def verify_password(plain_password: str, hashed_password: str): - return pwd_context.verify(plain_password, hashed_password) + return pwd_context.verify(plain_password, hashed_password) #(3)! ``` -A função `get_password_hash` recebe uma senha em texto puro como argumento e retorna uma versão criptografada dessa senha. A função `verify_password` recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde à senha criptografada. Ambas as funções utilizam o objeto `pwd_context`, que definimos anteriormente usando a biblioteca `pwdlib`. +1. Cria um contexto de hash de senhas com o algorítimo recomendado pela pwdlib. Por padrão é argon2. +2. Cria um hash argon2 da senha `password` +3. Verifica se `plain_password` é o mesmo valor de `hashed_password` quando aplicado ao contexto do argon2. + +A função `get_password_hash` recebe uma senha em texto puro como argumento e retorna uma versão criptografada dessa senha. A função `verify_password` recebe uma senha em texto puro e uma senha criptografada como argumentos, e verifica se a senha em texto puro, quando criptografada, corresponde à senha criptografada. Ambas as funções utilizam o objeto `pwd_context`, que definimos usando a biblioteca `pwdlib`. Agora, quando um usuário se registra em nossa aplicação, devemos usar a função `get_password_hash` para armazenar uma versão criptografada da senha. Quando um usuário tenta se autenticar, devemos usar a função `verify_password` para verificar se a senha inserida corresponde à senha armazenada. Na próxima seção, modificaremos nossos endpoints para fazer uso dessas funções. + ### Modificando o endpoint de POST para encriptar a senha Com as funções de criação de hash de senha e verificação de senha em vigor, agora podemos atualizar nossos endpoints para usar essa nova funcionalidade de encriptação.