paint-brush
Por que você precisa do JWT em seu projeto ASP.NET Core?por@igorlopushko
7,810 leituras
7,810 leituras

Por que você precisa do JWT em seu projeto ASP.NET Core?

por Igor Lopushko16m2024/02/13
Read on Terminal Reader

Muito longo; Para ler

A história é sobre como criar uma API Web para gerar JWT e depois usá-la para autorização na API Web CRUD.
featured image - Por que você precisa do JWT em seu projeto ASP.NET Core?
Igor Lopushko HackerNoon profile picture
0-item

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?

Autenticação vs Autorização


É 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.

Sessões

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.

Autorização baseada em sessão

Fichas

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.

Autorização baseada em token

Sessão vs Token

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.

Compartilhamento de estado de autorização baseado em sessão


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.

JWT

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 .

Estrutura JWT

JSON Web Tokens consistem em três partes separadas por pontos .


  • Cabeçalho
 { "alg": "HS256", "typ": "JWT" }

O cabeçalho geralmente consiste em duas partes: o tipo de token e o algoritmo de assinatura usado.


  • Carga útil
 { "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 .


  • Assinatura
 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.

depurador jwt.io

Projeto .NET

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.

Cenário do projeto .NET

API de café

Primeiro, vamos criar um projeto simples de API ASP.NET Core que representa a API Coffee. Aqui está a estrutura deste projeto:

API Café - Estrutura do 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:

API de café - UI Swagger

API de identidade

Vamos criar outro projeto de API ASP.NET Core que representa a API Identity. Aqui está a estrutura deste projeto:

API de identidade – estrutura do 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:

API de identidade - IU Swagger


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

Habilitando autorização na API Coffee

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.

API Coffee - As declarações são preenchidas a partir do JWT

Adicionar autorização à IU do Swagger

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:

Coffee API - botão Autorizar apareceu


Ao clicar no botão Autorizar você poderá inserir o JWT da seguinte forma:

API Coffee - Insira o valor JWT

Use o Postman para testes

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.

API de identidade - Especifique cabeçalhos


Depois disso, podemos chamar o endpoint /token e obter um novo JWT.

API de identidade - Gerar 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.

API Coffee - Obtenha todas as entidades

Autorização baseada em função

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 .

Autorização baseada em reivindicação

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")]

Resumo

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.