ASP.NET Core - Autenticação JWT com Refresh Token - I
Neste artigo veremos a autenticação JWT com Refresh Token em uma aplicação ASP .NET Core. |
Como eu já escrevi diversos artigos sobre o assunto vou realizar uma abordagem bem objetiva e direta sem entrar em detalhes nos conceitos envolvidos.
Para saber mais sobre os Json Web Tokens - JWT consulte meus artigos:
Neste artigo, que foi baseado em outros artigos sobre o assunto, vamos criar uma API ASP .NET Core, proteger os seus endpoints, gerar o token JWT, registrar usuários e tratar com a autorização baseada em roles usando o Identity.
O que é um Refresh Token e porque precisamos deles ?
De forma bem objetiva, um Refresh Token ou token de atualização é um token especial usado para obter um Access Token ou token de acesso adicional. Isso permite que você tenha tokens de acesso de curta duração sem precisar coletar credenciais sempre que um deles expirar.
Assim, se
estivermos usando um token de acesso por muito tempo, há uma chance maior
de um hacker roubar nosso token e usá-lo de forma inadequada. Portanto, não é
muito seguro usar o token de acesso por um longo período.
Os refresh tokens ou tokens de atualização são
tipos de tokens que podem ser usados para obter novos tokens de acesso. Quando
os tokens de acesso expiram, podemos usar os tokens de atualização para obter um
novo token de acesso. (Em geral, a vida útil de um token de atualização é maior
que a vida útil de um token de acesso.)
Na grande parte dos artigos que encontramos na web o refresh token é gerado e armazenado em um banco de dados e isso sempre me incomodou, visto que não via muito sentido em ter que armazenar o token de atualização em uma base de dados. No entanto se a sua aplicação precisa forçar o usuário a refazer o login isso justificaria armazenar o token de atualização. Um exemplo deste caso seriam as aplicações bancárias que após um tempo de inatividade forçam o cliente a refazer o login.
Assim neste artigo iremos realizar as seguintes tarefas:
Para isso vamos usar os seguintes recursos:
A ASP.NET Core Identity é um sistema de associação que permite adicionar funcionalidade de login ao seu aplicativo onde os usuários podem criar uma conta e fazer login com um nome de usuário e senha, ou podem usar um provedor de login externo, como Facebook, Google, Conta da Microsoft, Twitter e muito mais.
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
Sempre que o usuário fizer login no aplicativo usando credenciais válidas, atualizaremos o token de atualização e o tempo de expiração do token na tabela de usuários dentro do banco de dados do Identity.
Após a expiração do token de acesso, se o usuário tentar novamente obter um recurso protegido do aplicativo, ele vai gerar um erro 401 não autorizado. Em seguida, o usuário pode tentar atualizar o token usando o token de acesso atual e o token de atualização.
No método de
atualização, o aplicativo confirmará o token expirado e o token de
atualização e, se ambos forem válidos, o aplicativo emitirá um novo
token de acesso e atualizará o token para o usuário. O usuário
correspondente pode usar esse novo token para acessar recursos protegidos no
aplicativo.
Se algo der errado, o token de atualização pode ser revogado, o que
significa que quando o aplicativo tentar usá-lo para obter um novo token de
acesso, essa solicitação será rejeitada e o usuário precisará inserir as
credenciais novamente e se autenticar.
Criando o projeto Web API
Abra o VS 2022 Community e selecione a opção Create New Project;
Selecione o template ASP.NET Core Web API e informe o nome AspnJwt_RefreshToken;
A seguir defina as configurações conforme a figura abaixo e clique em Create;
A seguir vamos incluir os seguintes pacotes Nuget no projeto usando o comando install-package <nome> na janela Package Manager Console.
Vamos criar no projeto a pasta Models onde iremos criar as entidades usadas em nosso projeto.
Nesta pasta crie a classe ApplicationUser que herda de IdentityUser e representa um usuário da aplicação:
public class ApplicationUser : IdentityUser { public string? RefreshToken { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } } |
Ainda nesta pasta crie a classe AppDbContext que vai herdar da classe IdentityDbContext<ApplicationUser> :
using
Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace AspnJwt_RefreshToken.Models; public class AppDbContext :
IdentityDbContext<ApplicationUser> |
Esta classe representa o contexto do EF Core e realiza o mapeamento ORM.
Continuando vamos criar na pasta Models as seguintes classes que darão suporte para a nossa implementação.
1 - UserRoles
public static class UserRoles { public const string Admin = "Admin"; public const string User = "User"; } |
Nesta classe definimos dois perfis que iremos usar: Admin e User.
2- Response
public class Response { public string? Status { get; set; } public string? Message { get; set; } } |
Esta classe será usada para tratar o response após o registro e o login do usuário.
3- TokenModel
public class TokenModel { public string? AccessToken { get; set; } public string? RefreshToken { get; set; } } |
Nesta classe definimos as propriedades AccesToken e RefreshToken que representam o token e o refresh token.
4- LoginModel
using System.ComponentModel.DataAnnotations; namespace AspnJwt_RefreshToken.Models; public class LoginModel [Required(ErrorMessage = "Password is required")] |
Esta classe usaremos para realizar o login.
5- RegisterModel
using System.ComponentModel.DataAnnotations; namespace AspnJwt_RefreshToken.Models; public class RegisterModel [EmailAddress] [Required(ErrorMessage = "Password is required")] |
Esta classe será usada para registrar o usuário.
Vamos definir no arquivo appsettings.json a string de conexão e as configurações usadas para criar o token JWT e a nossa chave privada.
{ "ConnectionStrings": { "DefaultConnection": "Data Source=desktop-bhp8771\\sqlexpress;Initial Catalog=JwtRefreshTokenDB; Integrated Security=True" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "JWT": { "ValidAudience": "http://localhost:7043", "ValidIssuer": "http://localhost:5043", "SecretKey": "Minha@Super#Secreta&Chave*Privada!2022", "TokenValidityInMinutes": 30, "RefreshTokenValidityInMinutes": 60 } } |
-
SecretKey é uma string secreta que é usada para
assinatura do token;
- ValidIssuer - Indica a parte que esta emitindo o JWT;
- ValidAudience - Indica os destinatários do JWT;
Criamos duas seções neste arquivo. A seção ConnectionStrings onde definimos a string de conexão e a seção JWT onde definimos valores de configuração para a autenticação JWT . Definimos um tempo de expiração para o token de 30 minutos e de 60 minutos para o refresh token.
Agora vamos registrar os serviços na classe Program:
using AspnJwt_RefreshToken.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Text; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Entity Framework // Identity // Authentication // Jwt Bearer ValidAudience = builder.Configuration["JWT:ValidAudience"], var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthentication(); app.MapControllers(); app.Run(); |
Nesta classe registramos os serviços para o contexto do EF Core definindo o provedor usado e a string de conexão, o serviço do Identity e da autenticação e do JWT Bearer.
Podemos agora aplicar o Migrations usando a abordagem Code-First e gerar o banco de dados onde iremos armazenar o refresh token.
Para isso vamos abrir uma janela do Package Manager Console e usar a ferramenta EF Core Tools dotnet ef emitindo os comandos:
dotnet ef migrations add Inicial - Cria o script de migração
dotnet ef database update - Aplica o script e gera o banco e as tabelas
Ao final podemos confirmar a criação do banco de dados e das tabelas do Identity usando o SQL Server Management Studio:
Criando o controller AuthenticateController
Vamos criar na pasta Controllers do projeto o controlador AutenticateController onde iremos implementar as funcionalidades para :
using AspnJwt_RefreshToken.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace AspnJwt_RefreshToken.Controllers public AuthenticateController( [HttpPost] if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) var authClaims = new List<Claim> foreach (var userRole in userRoles) var token = CreateToken(authClaims); _ = int.TryParse(_configuration["JWT:RefreshTokenValidityInMinutes"],
user.RefreshToken = refreshToken; await _userManager.UpdateAsync(user); return Ok(new [HttpPost] if (userExists != null) ApplicationUser user = new() var result = await _userManager.CreateAsync(user, model.Password); if (!result.Succeeded) if (!await _roleManager.RoleExistsAsync(UserRoles.Admin)) if (!await _roleManager.RoleExistsAsync(UserRoles.User)) if (await _roleManager.RoleExistsAsync(UserRoles.Admin)) if (await _roleManager.RoleExistsAsync(UserRoles.Admin)) return Ok(new Response { Status = "Success", Message = "User created successfully!" }); if (userExists != null) ApplicationUser user = new() if (!result.Succeeded) return Ok(new Response { Status = "Success", Message = "User created successfully!" }); string? accessToken = tokenModel.AccessToken; var principal = GetPrincipalFromExpiredToken(accessToken); string username = principal.Identity.Name; if (user == null || user.RefreshToken != refreshToken ||
var newAccessToken = CreateToken(principal.Claims.ToList()); user.RefreshToken = newRefreshToken; return new ObjectResult(new user.RefreshToken = null; return NoContent(); [Authorize] return NoContent(); private JwtSecurityToken CreateToken(List<Claim> authClaims) var token = new JwtSecurityToken( return token; private static string GenerateRefreshToken() private ClaimsPrincipal? GetPrincipalFromExpiredToken(string? token) var tokenHandler = new JwtSecurityTokenHandler(); var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, if (securityToken is not JwtSecurityToken jwtSecurityToken ||
return principal; |
Para testar a nossa implementação vamos aplicar o atributo [Authorize] no controlador WeatherForecastController do projeto e a seguir vamos usar a nossa implementação.
Executando o projeto teremos o seguinte resultado exibido na interface do Swagger:
Na próxima parte do artigo iremos testar a nossa implementação.
"Se o SENHOR não edificar a casa, em vão trabalham os que
a edificam; se o SENHOR não guardar a cidade, em vão vigia a sentinela."
Salmos 127:1
Referências:
ASP .NET Core 2 - MiniCurso Básico
ASP .NET Core - Macoratti
Conceitos - .NET Framework versus .NET Core
ASP .NET Core - Conceitos Básicos
ASP .NET Core MVC - CRUD básico com ADO .NET
ASP .NET Core - Implementando a segurança com .
ASP .NET Core - Apresentando Razor Pages
Minicurso ASP .NET Core 2.0 - Autenticação com JWT - I
Minicurso ASP .NET Core 2.0 - Autenticação com JWT - II