JWT significa JSON Web Token e é um mecanismo de autorização, não de autenticação. Então, vamos descobrir qual é a diferença entre os dois.
A autenticação é o mecanismo que permite verificar se o usuário é exatamente quem afirma ser. É um processo de login em que um usuário fornece um nome de usuário e uma senha e o sistema os verifica. Portanto, a autenticação responde à pergunta: quem é o usuário?
Autorização é o mecanismo que permite verificar quais direitos de acesso o usuário possui a determinado recurso. É um processo de concessão aos usuários de algumas funções e um conjunto de permissões que uma determinada função possui. Então, a autorização responde a essa pergunta: quais direitos o usuário tem no sistema?
É importante entender que a Autenticação sempre vem em primeiro lugar e a Autorização em segundo. Em outras palavras, você não pode obter permissão antes de verificar sua identidade. Mas quais são os métodos de autorização mais populares? Existem duas abordagens principais para lidar com a autorização do aplicativo da web.
Uma abordagem tradicional na web para usuários autorizados é uma sessão do lado do servidor baseada em cookies. O processo começa quando um usuário faz login e um servidor o autentica. Depois disso, o servidor cria uma sessão com um ID de sessão e a armazena em algum lugar da memória do servidor. O servidor envia de volta o ID da sessão ao cliente e o cliente armazena o ID da sessão em cookies. Para cada solicitação, o cliente envia um ID de sessão como parte da solicitação, e o servidor verifica o ID de sessão em sua memória e as permissões do usuário relacionadas a esta sessão.
Outra abordagem popular é usar tokens para autorização. O processo começa de forma semelhante quando um usuário insere login e senhas e um cliente envia uma solicitação de login a um servidor. Em vez de criar uma sessão, o servidor gera um token assinado com o token secreto. Em seguida, o servidor envia o token de volta ao cliente, e o cliente deve armazená-lo em um armazenamento local. Semelhante à abordagem baseada em sessão, o cliente deve enviar um token ao servidor para cada solicitação. Entretanto, o servidor não armazena nenhuma informação adicional sobre a sessão do usuário. O servidor deve validar se o token não mudou desde que foi criado e assinado com a chave secreta.
A abordagem de autorização baseada em sessão pode ser vulnerável a um ataque conhecido como Cross-Site Request Forgery (CSRF). É um tipo de ataque quando o invasor aponta para um site no qual está logado para realizar ações que não pretendia, como enviar um pagamento ou alterar uma senha.
Outra coisa é que, ao usar uma abordagem de autorização baseada em sessão, cria-se uma sessão com estado entre um cliente e um servidor. O problema é que se um cliente quiser acessar servidores diferentes no escopo da mesma aplicação, esses servidores terão que compartilhar um estado de sessão. Caso contrário, o cliente precisará estar autorizado em cada servidor, pois a sessão será diferente.
Por outro lado, a abordagem de autorização baseada em token não requer o armazenamento de dados de sessão no lado do servidor e pode simplificar a autorização entre vários servidores.
No entanto, os tokens ainda podem ser roubados por um invasor e também pode ser difícil invalidá-los. Veremos os detalhes e como lidar com a invalidação mais adiante neste artigo.
JSON Web Token (JWT) é um padrão aberto que define uma maneira compacta e independente de transmitir informações com segurança entre as partes como um objeto JSON. Essas informações podem ser verificadas e confiáveis porque são assinadas digitalmente. Os JWTs podem ser assinados usando um segredo (com o algoritmo HMAC ) ou um par de chaves pública/privada usando RSA ou ECDSA .
JSON Web Tokens consistem em três partes separadas por pontos .
{ "alg": "HS256", "typ": "JWT" }
O cabeçalho geralmente consiste em duas partes: o tipo de token e o algoritmo de assinatura usado.
{ "sub": "1234567890", "name": "John Doe", "admin": true }
A carga útil contém as declarações, que são declarações sobre o usuário. A carga útil é então codificada em Base64Url para formar a segunda parte do JSON Web Token. Você pode encontrar uma descrição dos campos padrão usados como declarações aqui .
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Para criar a parte da assinatura, você deve pegar o cabeçalho codificado, a carga útil codificada, um segredo e o algoritmo especificado no cabeçalho e assiná-los.
O token normalmente se parece com o seguinte:
xxxxx.yyyyy.zzzzz
Você pode navegar até jwt.io e depurar um token de amostra ou o seu próprio. Basta colar seu token no campo Codificado e selecionar o Algoritmo da assinatura do token.
Agora que temos conhecimento teórico de como o JWT funciona, podemos aplicá-lo ao projeto da vida real. Vamos supor que temos uma API simples que representa operações CRUD para a entidade coffee. Vamos criar um projeto de API ASP.NET Core que representa a API Coffee. Depois disso, criaremos outro projeto de API ASP.NET Core que representaria uma API de identidade que poderia gerar JWT. Na vida real, você provavelmente usaria Identity Server ou Okta ou Auth0 para fins de autenticação/autorização. No entanto, criaríamos nossa própria API de identidade para demonstrar como gerar JWT. Quando a API de identidade estiver concluída, podemos chamar seu controlador e gerar JWT com base nos dados do usuário. Além disso, podemos proteger a API Coffee com uma configuração de autorização que requer a passagem de JWT em cada solicitação.
Primeiro, vamos criar um projeto simples de API ASP.NET Core que representa a API Coffee. Aqui está a estrutura deste projeto:
Vamos começar com Coffee.cs
na pasta Model
. É uma entidade simples com propriedades Id
e Name
.
namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }
Precisamos armazenar nossas entidades enquanto trabalhamos com a API. Então, vamos apresentar um armazenamento simples na memória. Ele está localizado no arquivo Storage.cs
na pasta Data
.
namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }
Precisamos de uma classe que represente solicitações para a API Coffee. Então, vamos criar CoffeeRequest.cs
na pasta Contracts
.
namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }
Quando terminar, podemos implementar CoffeeController.cs
na pasta Controller
que representa as operações CRUD para a entidade coffee.
using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }
A API Coffee está pronta e podemos executar o projeto e ver a UI do Swagger da seguinte maneira:
Vamos criar outro projeto de API ASP.NET Core que representa a API Identity. Aqui está a estrutura deste projeto:
Vamos começar pela pasta TokenGenerationRequest.cs
na pasta Contracts
, que representa a solicitação de geração de um novo JWT com propriedades Email
e Password
.
namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }
Precisamos implementar apenas TokenController.cs
que representa a lógica de geração do JWT. Mas antes de fazermos isso, o pacote Microsoft.AspNetCore.Authentication.JwtBearer
NuGet precisa ser instalado.
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }
Observe que const sensíveis como SecretKey
, Issuer
e Audience
devem ser colocados em algum lugar da configuração. Eles são codificados apenas para simplificar este projeto de teste. O campo Lifetime
está definido para 20 minutos, o que significa que o token será válido durante esse período. Você também pode configurar esse parâmetro.
Agora podemos executar o projeto e ver a UI do Swagger da seguinte maneira:
Vamos fazer uma chamada para o endpoint /token
e gerar um novo JWT. Experimente a seguinte carga:
{ "email": "[email protected]", "password": "password" }
A API de identidade gerará o JWT correspondente:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM
Agora, quando a API Identity estiver pronta e nos fornecer tokens, podemos proteger a API Coffee com autorização. Novamente, o pacote Microsoft.AspNetCore.Authentication.JwtBearer
NuGet precisa ser instalado.
Precisamos registrar os serviços necessários pelos serviços de autenticação. Adicione o código a seguir ao arquivo Program.cs
logo após criar um construtor.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();
É importante lembrar que a ordem no middleware é importante. Habilitamos a autenticação chamando o método AddAuthentication()
e especificando JwtBearerDefaults.AuthenticationScheme
como um esquema de autenticação. É uma constante que contém um valor Bearer
.
namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }
Precisamos especificar TokenValidationParameters
que descreve quais parâmetros do JWT serão validados durante a autorização. Também especificamos IssuerSigningKey
semelhante ao signingCredentials
na API Identity para verificar a assinatura JWT. Confira mais detalhes sobre TokenValidationParameters
aqui .
O próximo trecho de código adiciona middleware ao construtor que habilita recursos de autenticação e autorização. Deve ser adicionado entre os métodos UseHttpsRedirection()
e MapControllers()
.
app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
Agora podemos usar o atributo Authorize
sobre o controlador ou suas ações. Ao aplicar este código, agora todas as ações no CoffeeController
ficam protegidas por um mecanismo de autorização, e o JWT deve ser enviado como parte da solicitação.
[Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..
Se fizermos uma chamada para qualquer endpoint da API Coffee, podemos depurar HttpContext.User
e ver se ele está preenchido e tem uma Identity
com declarações que especificamos no JWT. É importante entender como o ASP.NET Core lida com a autorização nos bastidores.
Fizemos um ótimo trabalho para proteger a Coffee API com a autorização. Mas se você executar o projeto Coffee API e abrir a UI do Swagger, não poderá enviar JWT como parte da solicitação. Para corrigir isso, precisamos atualizar o arquivo Program.cs
com o seguinte código:
builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });
Depois disso, poderemos ver o botão Autorizar no canto superior direito:
Ao clicar no botão Autorizar você poderá inserir o JWT da seguinte forma:
Você não pode se limitar a usar o Swagger UI e pode realizar testes da API através da ferramenta Postman. Vamos chamar o endpoint /token
da API Identity primeiro. Precisamos especificar o cabeçalho Content-Type
com o valor application/json
na seção Headers , pois usaremos JSON como carga útil.
Depois disso, podemos chamar o endpoint /token
e obter um novo JWT.
Agora podemos copiar o JWT e usá-lo para chamar a API Coffee. Precisamos especificar o cabeçalho Content-Type
semelhante à API Identity se quisermos testar, criar e atualizar endpoints. O cabeçalho Authorization
também deve ser definido com o valor Bearer [your JWT value]
. Depois disso, basta clicar no botão Enviar e ver o resultado.
Como você se lembra, a parte da carga útil do JWT é um conjunto de declarações com valores que são exatamente pares de valores-chave. A autorização baseada em função permite diferenciar o acesso aos recursos do aplicativo dependendo da função à qual o usuário pertence.
Se atualizarmos o método Create()
no arquivo TokenController.cs
na API Identity com o código que adiciona uma nova declaração para a função; podemos lidar com a autenticação baseada em funções na API Coffee. ClaimTypes.Role
é um nome predefinido da declaração de função.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };
Atualize o atributo Authorize
no arquivo CoffeeController.cs
especificando o nome da função:
[Authorize(Roles = "Barista")]
Agora, todos os usuários que fizerem uma chamada para a API Coffee deverão ter a reivindicação role com o valor Barista
. Caso contrário, eles receberão o código de status 403 Forbidden
.
Um atributo Authorize
pode lidar facilmente com a autenticação baseada em função. Mas e se não for suficiente e quisermos diferenciar o acesso com base em algumas propriedades do usuário, como idade ou qualquer outra? Você provavelmente já adivinhou que pode adicionar suas declarações ao JWT e usá-las para construir lógica de autorização. A própria autorização baseada em função é um caso especial de autorização baseada em declarações, assim como uma função é o mesmo objeto de declaração de um tipo predefinido.
Vamos atualizar o método Create()
no arquivo TokenController.cs
na API Identity com o código que adiciona uma nova declaração IsGourmet
.
var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };
No arquivo Program.cs da API Coffee, precisamos criar uma política que verifique uma reclamação e possa ser usada no atributo Authorize
. O código a seguir deve ser adicionado logo após a chamada do método AddAuthentication()
.
builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });
Atualize o atributo Authorize
no arquivo CoffeeController.cs
especificando o nome da política:
[Authorize(Policy = "OnlyForGourmet")]
Parabéns! Você fez um grande esforço para aprender JWT em .NET. Agora, você precisa ter um conhecimento sólido dos princípios do JWT e por que é importante usá-lo para realizar autorização em aplicativos .NET. Mas apenas arranhamos a superfície na área de autenticação e autorização em aplicativos ASP.NET Core.
Sugiro consultar a documentação da Microsoft sobre os tópicos que discutimos neste artigo. Existem também muitos recursos integrados para autorização e gerenciamento de funções na plataforma .NET. Uma boa adição a este artigo poderia ser a documentação da Microsoft sobre autorização.