ASP .NET Core - Minimal API com PostgreSQL e EF Core - II


Neste artigo veremos como criar uma API usando a abordagem das Minimal APIs para realizar um CRUD básico em um banco de dados PostgreSQL usando o EF Core.

Continuando o artigo anterior vamos agora implementar a segurança em nossa API e aproveitar para reorganizar os endpoints agrupando-os na interface do Swagger.

Para agrupar os endpoints vamos usar o método WithTags() que permite agrupar os endpoints na interface do Swagger. Assim vamos aplicar o método WithTags("Categorias") para os endpoints de categorias e WithTags("Produtos") para os endpoints de produtos.

A titulo de exemplo abaixo temos código para os endpoints que retornam todas as categorias e todos os produtos usando o método WithTags:

app.MapGet("/categorias", async (AppDbContext db) =>
  await db.Categorias.ToListAsync()
 )
 .WithTags("Categorias");

app.MapGet("/produtos", async (AppDbContext db) =>
await db.Produtos.ToListAsync())
.Produces<List<Produto>>(StatusCodes.Status200OK)

.WithTags("Produtos");

Assim aplicando o código para os respectivos endpoints de categorias e produtos teremos o seguinte resultado na exibição da interface do Swagger:

Vamos agora implementar a segurança em nossa aplicação usando a autenticação JWT.

Para saber mais sobre a autenticação JWT consulte os artigos :

Em nosso projeto a implementação vai seguir o roteiro padrão e eu não vou implementar o Identity para gerenciar os usuários. Vamos definir um usuário padrão com nome e senha para testar a geração do token Jwt e fazer a autenticação JWT na API de forma  simplificar o processo.

Implementando a autenticação JWT

Até o momento qualquer usuário anônimo pode acessar a nossa API pois ela esta aberta.  Para proteger a nossa API vamos implementar a autenticação JWT.

JWT (JSON Web Token) é um padrão definido na RFC 7519 para realizar autenticação entre duas partes por meio de um token assinado que autentica uma requisição web.  Esse token é um código em Base64 que armazena objetos JSON com os dados que permitem a autenticação da requisição.  Os dados nele contidos podem ser validados a qualquer momento pois o token é assinado digitalmente.

Em uma autenticação baseada em token, o cliente troca suas credenciais(exemplo: login e senha) por um token, depois disso em vez de enviar as credenciais a cada requisição o cliente so envia o token pra a autenticação e autorização

Para consultar a API é necessário obter um token de acesso temporário (Bearer). Esse token possui um tempo de validade e, sempre que expirado, o passo de requisição de um novo token de acesso deve ser repetido.

Vamos iniciar incluindo em nosso projeto o pacote :

Para representar o usuário vamos criar a classe UserModel na pasta Models:

namespace CatalogoApi.Models;

public class UserModel
{
    public string? UserName { get; set; }
    public string? Password { get; set; }
}

A seguir vamos criar no projeto a pasta Services e nesta pasta criar a interface ITokenService e sua implementação na classe TokenService

1- ITokenService

using CatalogoApi.Models;
namespace CatalogoApi.Services;

public interface ITokenService
{
    string GerarToken(string key, string issuer, string audience, UserModel user);
}

2- TokenService

using CatalogoApi.Models;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace CatalogoApi.Services;

public class TokenService : ITokenService
{
    public string GerarToken(string key, string issuer, string audience, UserModel user)
    {
        var claims = new[]
        {
          new Claim(ClaimTypes.Name, user.UserName),
          new Claim(ClaimTypes.NameIdentifier,Guid.NewGuid().ToString())
        };

        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        var credentials = new SigningCredentials(securityKey,
                                                  SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(issuer: issuer,
                                         audience: audience,
                                         claims: claims,
                                         expires: DateTime.Now.AddMinutes(120),
                                         signingCredentials: credentials);

        var tokenHandler = new JwtSecurityTokenHandler();
        var stringToken = tokenHandler.WriteToken(token);
        return stringToken;
    }
}

Acabamos de definir a implementação do token JWT e que vai usar as configurações que precisamos definir no arquivo appsettings.json na seção Jwt :

{
  "ConnectionStrings": {
    "DefaultConnection": "<string de conexão ;"
  },
  "Jwt": {
    "Key": "#5994471ABB01112AFCC18159F6CC74B4F511B99806DA59B3CAF5A9C173CACFC5@",
    "Issuer": "Macoratti.net",
    "Audience": "http://www.macoratti.net"
  },

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

A seguir vamos definir a seguinte configuração do middleware de autenticação no arquivo Program, que vai permitir usar a autenticação bearer e também validar o token gerado :

...

builder.Services.AddSingleton<ITokenService>(new TokenService());

builder.Services.AddAuthentication
                 (JwtBearerDefaults.AuthenticationScheme)
                 .AddJwtBearer(options =>
                 {
                     options.TokenValidationParameters = new TokenValidationParameters
                     {
                         ValidateIssuer = true,
                         ValidateAudience = true,
                         ValidateLifetime = true,
                         ValidateIssuerSigningKey = true,

                         ValidIssuer = builder.Configuration["Jwt:Issuer"],
                         ValidAudience = builder.Configuration["Jwt:Audience"],
                         IssuerSigningKey = new SymmetricSecurityKey
                         (Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
                     };
                 });

builder.Services.AddAuthorization();
...

...

app.UseAuthentication();
app.UseAuthorization();

app.Run();

Neste código registramos o serviço da geração do token e  definimos a autenticação usando o schema Jwt e incluímos o serviço da autorização.

Agora precisamos incluir na classe Program um endpoint para fazer o login do usuário e a geração do token.

Para isso estou definindo um usuário com nome 'macoratti'   e senha 'numsey#123' e a seguir invocando o método para gerar o token :

app.MapPost("/login", [AllowAnonymous] (UserModel userModel,ITokenService tokenService) =>
{
    if (userModel == null)
    {
        return Results.BadRequest("Login Inválido");
    }
    if (userModel.UserName == "macoratti" && userModel.Password == "numsey#123")
    {
        var tokenString = tokenService.GerarToken(builder.Configuration["Jwt:Key"],
            builder.Configuration["Jwt:Issuer"],
            builder.Configuration["Jwt:Audience"],
            userModel);
        return Results.Ok(new { token = tokenString });
    }
    else
    {
        return Results.BadRequest("Login Inválido");
    }
}).Produces(StatusCodes.Status400BadRequest)
  .Produces(StatusCodes.Status200OK)
  .WithName("Login")
  .WithTags("Autenticacao");

Com isso podemos testar a geração do token e a seguir fazer a chamada a um endpoint protegido. Para fazer um teste vamos proteger os endpoints que usam MapGet e retornam todas as categorias e todos os produtos incluindo o método de extensão .RequireAuthorization();

app.MapGet("/categorias", async (AppDbContext db) =>
     await db.Categorias.ToListAsync())
     .WithTags("Categorias")
     .RequireAuthorization();

app.MapGet("/produtos", async (AppDbContext db) =>
     await db.Produtos.ToListAsync())
     .WithTags("Produtos")
     .RequireAuthorization();

Para testar a nossa implementação teremos que gerar o token e enviar este token com cada requisição para acessar os endpoints seguros.

Para facilitar este procedimento podemos configurar o Swagger na API ASP.NET Core para poder enviar o token JWT em cada request bastando para isso informar uma única vez o token.

Eu apresento isso em detalhes neste artigo : ASP.NET Core - Usando o token JWT com o Swagger

Para fazer isso temos que ajustar a configuração do Swagger no arquivo Program do projeto incluindo o código abaixo:

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "apiagenda", Version = "v1" });

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = @"JWT Authorization header using the Bearer scheme.
                    Enter 'Bearer'[space].Example: \'Bearer 12345abcdef\'",
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                          new OpenApiSecurityScheme
                          {
                              Reference = new OpenApiReference
                              {
                                  Type = ReferenceType.SecurityScheme,
                                  Id = "Bearer"
                              }
                          },
                         new string[] {}
                    }
                });
});

Agora é só alegria...

Vamos executar o projeto e tentar acessar um endpoint protegido GET /categorias e a seguir vamos fazer o login, gerar o token e informar o token no Swagger e depois vamos acessar o endpoint novamente. Veja o resultado:

Na próxima parte do artigo iremos mostrar como organizar o código da classe Program.

Pegue o projeto completo aqui :  CatalogoApi_Inicio_Jwt.zip

"Então ele disse: "Jesus, lembra-te de mim quando entrares no teu Reino".
Jesus lhe respondeu: "Eu lhe garanto: Hoje você estará comigo no paraíso"."
Lucas 23:42,43

Referências:


José Carlos Macoratti