Quando trabalhamos com autenticação baseada em tokens JWT temos que considerar que ele tem um tempo de expiração que irá invalidá-lo em determinado momento. Em alguns cenários pode ser interessante obter um novo token de acesso sem precisar forçar o usuário a fazer um novo login informando seu usuário e senha. Isso é muito comum quando trabalhamos com aplicativos mobile.
Para esse artigo usei o mesmo exemplo de código de um artigo > anterior onde mostro como implementar uma API de Autenticação simples com JWT. Você pode conferir em meu Github.
Imagine que o cliente faça login no seu aplicativo informando usuário e senha, sua API devolve um token JWT que seu app deverá armazenar internamente. Sabemos que em todas as requisições seguintes o app deverá enviar o token no header Authorization para a API que irá consumir.
O cliente coloca o app em background e só volta a usá-lo após algumas horas. Muito provavelmente o token de acesso criado anteriormente já expirou e ao tentar fazer qualquer chamada na API o app irá receber um HTTP Status Code 401 — Unauthorized. Você tem duas opções, a) redirecionar o cliente para a tela de login e forçá-lo a fazer um novo acesso, b) pedir para que a API de autenticação forneça um novo token JWT válido de forma transparente para o cliente.
Caso seu app não necessite que o cliente faça um novo login a cada tempo de expiração, como é o caso de aplicativos bancários por exemplo, você pode optar por gerar um novo token de forma transparente para o cliente fazendo uso do que chamamos de Refresh Token.
Para isso, além do token de acesso principal, você deve gerar um token secundário com tempo de expiração maior. Por exemplo, se o access_token expira em 8h, seu **refresh_token **irá expirar em 16 horas (não precisar ser necessariamente esse tempo, você deve definir um tempo adequado).
O refresh_token dever ser devolvido para seu aplicativo junto com o token JWT principal no momento do login. O app então deverá armazenar o refresh_token internamente, assim como já faz com o access_token.
Conjunto de tokens de acesso
Como implementar?
No projeto de exemplo, você irá encontrar uma classe chamada **JwtService **que é responsável por gerar um token JWT. O que eu fiz foi implementar a criação do Refresh Token da seguinte forma.
O método CreateRefreshToken irá gerar um valor aleatório de 32 bytes, que é convertido para string, onde em seguida são removidos alguns caracteres indesejados. Nada demais!!
Esse método deverá ser usado no momento da criação do token JWT, no método CreateJsonWebToken.
Como vocês já sabem, eu gosto muito de usar o MediatR em meus projetos, inclusive já escrevi um artigo sobre ele.
Tenho um request chamado Authenticate, que irá disparar o handler AuthenticateHandler.
A classe Authenticate não tem nada demais, ela contém apenas as propriedades necessárias para o input dos dados pelo aplicativo, como e-mail, password, refresh token e grant type, além de algumas validações.
Na classe AuthenticateHandler, que é responsável por responder à uma solicitação de login, no método Handle verifico qual tipo de autenticação está sendo feita através da propriedade GrantType do request do MediatR e então executo o método apropriado.
- GrantType == password → método comum de autenticação, onde verificamos as credenciais de acesso fornecidas pelo usuário, como usuário e senha por exemplo; * GrantType == refresh_token → método de autenticação baseado no refresh token que geramos anteriormente.
Isso é necessário pois normalmente existe apenas um único endpoint de autenticação na API que irá gerar um token de acesso, e é através do GrantType que diferenciamos a forma de autenticação. Existem implementações diferentes disso, onde temos dois endpoints de autenticação, um para validar as credenciais inseridas pelo usuário (login e senha) e outro exclusivo para o Refresh Token. Em nosso exemplo vamos usar um único endpoint.
Além disso, veja que ao gerar um token JWT no método HandleJwt, o Refresh Token gerado é salvo no banco de dados, sempre sobrescrevendo o anterior de forma que só exista um único hash de refresh por usuário. Isso é importante pois quando o app tentar fazer a atualização do token de acesso, devemos verificar se o refresh token informado é válido, nesse caso, no método **HandleRefreshToken **faço uma busca no banco de dados pelo hash informado, verifico se ele realmente existe e se está expirado, lembre-se que ele também tem um tempo de expiração. Além dessas duas validações, você pode implementar um mecanismo de revogação do refresh token, para inutilizá-lo imediatamente, e então poderia fazer essa validação de revogação nesse momento.
Caso o refresh token seja válido, basta consultar os dados do usuário no banco e gerar um novo JWT, bem como um novo refresh token.
A controller na API é bem simples, ela apenas recebe o request HTTP de login, o mecanismo de Model Binding do ASP.Net se encarrega de fazer o bind apropriado para o objeto Authenticate, e então ele é enviado para o MediatR que irá disparar o handler AuthenticateHandler que vimos anteriormente.
Além disso, existe a configuração do tempo de expiração do Refresh Token, que fica no arquivo appsettings.json da nossa API e é representado pela chave RefreshTokenValidForMinutes. Nesse exemplo o access_token (JWT) tem um tempo de expiração de 60 minutos, já o refresh_token irá expirar em 120 minutos. Como eu disse anteriormente, o tempo de expiração do refresh token deve ser maior que o tempo de expiração do JWT. Essa configuração é materializada na classe JwtSettings.
Testando
Ao executar a API uma tela do Swagger será aberta, mas nesse caso vou usar o Postman para fazer os testes.
Com a API em execução e com um usuário criado, devo fazer um novo login informando um usuário e senha. Note que informei o GrantType com o valor password. Caso as credenciais informadas estejam corretas um token JWT é devolvido.
Login na API de Autenticação
Conforme disse anteriormente, o aplicativo deve armazenar os valores de access_token e também do **refresh_token **internamente para posterior uso.
Em posse do token JWT, o app deverá enviá-lo através do header **Authorization **em todas as demais requisições nas APIs que são utilizadas. Quando o token estiver expirado, a API irá retornar um HTTP Status Code 401 — Unauthorized. Nesse momento o app pode pedir um novo token de acesso fazendo uso do hash de refresh token armazenado, desde que ele também não esteja expirado, sendo que nesse caso o usuário deveria fazer um novo login.
Para pedir um novo token basta chamar novamente o endpoint de login, entretanto agora devemos informar o GrantType com o valor refresh_token e o hash armazenado anteriormente. Caso tudo esteja certo, um novo token JWT é devolvido, juntamente com um novo hash de refresh token.
Caso o hash de refresh não seja válido a API de autenticação poderá exibir as devidas mensagens de erro durante a atualização do token.
Refresh Token não encontrado
Refresh Token expirado
E com isso temos um mecanismo simples de atualização de tokens de acesso.
Conclusão
Conforme vimos, a atualização de tokens de acesso é um recurso muito poderoso e pode nos poupar algumas dores de cabeça, afinal, ninguém quer um cliente insatisfeito por ter que fazer login seguidas vezes em seu aplicativo.
Lembrando que essa é apenas uma das várias formas de implementar o Refresh Token em sua API de Autenticação. Se você procurar, irá encontrar outros modelos de implementação.
Soluções como o Identity Server já possuem esse e outros recursos de segurança nativos e mais completos, sendo essa demo apenas um modelo simples para implementar autenticação em seu app.
Espero que tenham gostado, e se ficou alguma dúvida ou tenham críticas e sugestões, não deixem de entrar em contato.
Abraços!