Ferramentas necessárias:
Seguimos com a construção da nossa api, neste artigo vamos estruturar a parte de segurança de nossa api, bloqueando acesso de quem não esta autenticado ou sem autorização para acessar algum recurso.
Para realizar essa proteção vamos utilizar a autenticação via JWT JSON WEB TOKEN
bora la.
Para iniciar vamos instalar as bibliotecas necessárias, conforme vimos nos artigos anteriores vamos utilizar o gerenciador de pacotes do Nuget
Dentro do gerenciador busque e instale essas duas bibliotecas
Feito isso, dentro da pasta Controllers
que fica na camada de Infrastructure
de nossa api (vide artigo 1 para entender a definição da arquitetura escolhida) vamos proteger nossas classes de acesso conforme imagem abaixo.
Observação: Como passamos a anotação [Authorize(Roles = "ADMIN")]
acima do nome da nossa classe isso faz com que todos os endpoints de acesso sejam protegidos, atraves do JWTToken e a role de permissão concedida ao usuario, caso passemos a anotação acima de um método especifico dentro da nossa classe (conforme imagem abaixo) apenas esse método será protegido o acesso.
Feito isso e necessário mais uma configuração dentro na nossa classe Program.cs, dentro do metodo Main
vamos instanciar uma variável secretKey
, essa assinatura é utilizada para garantir a integridade do token, no caso, se ele foi modificado e se realmente foi gerado por você, você pode gerar um Guid para garantir a integridade dessa chave e que ninguem a conheça para decodificar seu token.
Logo acima da configuração app.UseAuthorization()
instancia a config app.UseAuthentication()
Agora adicionamos a configuração para geração do token conforme imagem abaixo.
ValidateIssuer = true: Configura que na geração do token seja validado se o Issuer contido no token e igual ao que passamos na config ValidIssuer.
ValidateAudience= true: : Configura que na geração do token seja validado se o Audience contido no token e igual ao que passamos na config ValidAudience.
ValidateLifetime = true: Configura a validação de expiração do token gerado.
ValidateIssuerSigningKey: Valida se a chave secreta contida na geração do token e a mesma que passamos na variavel secretKey
Nossa classe Main
ficara conforme abaixo:
using FirstApi.Application.UseCases.CasesEmployer.ConsultEmployer;
using FirstApi.Application.UseCases.CasesEmployer.DeleteEmployer;
using FirstApi.Application.UseCases.CasesEmployer.Register;
using FirstApi.Application.UseCases.CasesEmployer.UpdateEmployer;
using FirstApi.Application.UseCases.CasesUser.ConsultUser;
using FirstApi.Application.UseCases.CasesUser.DeleteUser;
using FirstApi.Application.UseCases.CasesUser.RegisterUser;
using FirstApi.Application.UseCases.CasesUser.UpdateUser;
using FirstApi.Application.UseCases.PasswordHasher;
using FirstApi.Domain.Repositories;
using FirstApi.Infrastructure.Data;
using FirstApi.Infrastructure.Handler;
using FirstApi.Infrastructure.Integration.ViaCep;
using FirstApi.Infrastructure.Integration.ViaCep.Refit;
using FirstApi.Infrastructure.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Refit;
using System.Text;
namespace FirstApi
{
public class Program
{
public static void Main(string[] args)
{
string secretKey = "e0606215-c2e4-46fe-8d73-a77e1fa4f45a";
var builder = WebApplication.CreateBuilder(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// Configure data base access
builder.Services.AddDbContext<SystemDbContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add repositories to the container.
builder.Services.AddScoped<IEmployerRepository, EmployerRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
// Add services to the container.
// employer
builder.Services.AddScoped<IRegisterEmployerService, RegisterEmployerService>();
builder.Services.AddScoped<IUpdateEmployerService, UpdateEmployerService>();
builder.Services.AddScoped<IConsultEmployerService, ConsultEmployerService>();
builder.Services.AddScoped<IDeleteEmployerService, DeleteEmployerService>();
// user
builder.Services.AddScoped<IRegisterUserService, RegisterUserService>();
builder.Services.AddScoped<IUpdateUserService, UpdateUserService>();
builder.Services.AddScoped<IConsultUserService, ConsultUserService>();
builder.Services.AddScoped<IDeleteUserService, DeleteUserService>();
builder.Services.AddScoped<IPasswordHasher, PasswordHasher>();
// client viacep
builder.Services.AddScoped<IViaCepIntegrationService, ViaCepIntegrationService>();
// Add client refit
builder.Services.AddRefitClient<IViaCepIntegrationRefit>().ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://viacep.com.br");
}
);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Config authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience= true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "emprise",
ValidAudience = "firstApi",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
// global error handler
app.UseMiddleware<GlobalExceptionHandler>();
app.MapControllers();
app.Run();
}
}
}
Pronto nossa api ja esta protegida, se fizermos um request sem passar o token de acesso vamos receber um status code 401
.
Vamos agora implementar um endpoint para criar um login e gerar um JsonToken
para acessarmos nossa api, na nossa camada Application
vamos criar um caso de uso PasswordHasher
responsavel por criptografar o password do usuario e armazenar banco de dados e mais um caso de uso chamado Login conforme imagens abaixo.
namespace FirstApi.Application.UseCases.PasswordHasher
{
public interface IPasswordHasher
{
public string Hash(string password);
public bool Verify(string passwordHash, string password);
}
}
PasswordHasher
using System.Security.Cryptography;
namespace FirstApi.Application.UseCases.PasswordHasher
{
public class PasswordHasher : IPasswordHasher
{
private int SaltSize = 128 / 8;
private int KeySize = 256 / 8;
private int Iterations = 10000;
private static HashAlgorithmName _hash = HashAlgorithmName.SHA256;
private char Delimiter = ';';
string IPasswordHasher.Hash(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations,_hash, KeySize);
return string.Join(Delimiter, Convert.ToBase64String(salt), Convert.ToBase64String(hash));
}
bool IPasswordHasher.Verify(string passwordHash, string password)
{
var elements = passwordHash.Split(Delimiter);
var salt = Convert.FromBase64String(elements[0]);
var hash = Convert.FromBase64String(elements[1]);
var hashInput = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, _hash, KeySize);
return CryptographicOperations.FixedTimeEquals(hash, hashInput);
}
}
}
ILoginService
namespace FirstApi.Application.UseCases.CasesAuth.Login
{
public interface ILoginService
{
public LoginOutput Execute(LoginInput input);
}
}
LoginService
using FirstApi.Application.UseCases.PasswordHasher;
using FirstApi.Domain.Entities;
using FirstApi.Domain.Enums;
using FirstApi.Domain.Repositories;
using FirstApi.Infrastructure.CustomException;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
namespace FirstApi.Application.UseCases.CasesAuth.Login
{
public class LoginService : ILoginService
{
private readonly IUserRepository _userRepository;
private readonly IPasswordHasher _passwordHasher;
public LoginService(IUserRepository userRepository, IPasswordHasher passwordHasher)
{
_userRepository = userRepository;
_passwordHasher = passwordHasher;
}
LoginOutput ILoginService.Execute(LoginInput input)
{
User user = _userRepository.FindUserByEmail(input.Email).Result;
if (user == null || user.Password == null || user.Email == null)
{
throw new AppNotFoundException("Credenciais Invalidas teste");
}
if (_passwordHasher.Verify(user.Password, input.Password))
{
return new LoginOutput().Convert(user, this.generateJwtToken(user.Roles));
}
throw new BadHttpRequestException("Credenciais Invalidas");
}
private string generateJwtToken(List<Roles> roles)
{
string secretKey = "e0606215-c2e4-46fe-8d73-a77e1fa4f45a";
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var claims = new List<Claim>
{
new Claim("login","admin"),
new Claim("name", "System Administrator")
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())));
var token = new JwtSecurityToken(
issuer: "emprise",
audience: "firstApi",
claims: claims,
expires: DateTime.UtcNow.AddHours(2),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
LoginInput
namespace FirstApi.Application.UseCases.CasesAuth.Login
{
public class LoginInput
{
public required string Email { get; set; }
public required string Password { get; set; }
}
}
LoginOutput
using FirstApi.Domain.Entities;
namespace FirstApi.Application.UseCases.CasesAuth.Login
{
public class LoginOutput
{
public int Id { get; set; }
public string? UserName { get; set; }
public string? AccessToken { get; set; }
internal LoginOutput Convert(User user, string token)
{
Id = user.Id;
UserName = user.Name;
AccessToken = token;
return this;
}
}
}
Obs: Duvida sobre a implementação veja a primeira parte do artigo pois explico a responsabilidade de cada classe construido dentro do caso de uso.
Será necessário também mudarmos nossa entity User.cs
pois precisamos adicionar uma lista de Roles
para cada usuário salvo que será as permissões de acessos que esse usuário possui mesmo tendo um token JwtToken
gerado.
Mude a classe User.cs para que fique assim
using FirstApi.Domain.Enums;
using FirstApi.Domain.ValueObjects;
using FirstApi.Infrastructure.Integration.ViaCep;
namespace FirstApi.Domain.Entities
{
public class User
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public List<Roles>? Roles { get; set; }
public Endereco? Endereco { get; set; }
public Endereco GetEndereco(ViaCepResponse response)
{
Endereco endereco = new Endereco();
if(response is null)
{
return endereco;
}
endereco.Complemento = response.Complemento;
endereco.Cep = response.Cep;
endereco.Bairro = response.Bairro;
endereco.Logradouro = response.Logradouro;
endereco.Localidade = response.Localidade;
endereco.Cep = response.Cep;
endereco.Unidade = response.Unidade;
endereco.Uf = response.Uf;
return endereco;
}
}
}
Crie o enum contendo as roles de permissões que seu sistema possui conforme exemplo abaixo.
Obs: Como estamos mudando nossa entity será necessário mudar o mapeamento dessa tabela na classe UserMap.cs.
UserMap
using FirstApi.Domain.Entities;
using FirstApi.Domain.Enums;
using FirstApi.Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace FirstApi.Infrastructure.Data.Map
{
public class UserMap : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
var userRolesConverter = new ValueConverter<List<Roles>, string>(
v => string.Join(',', v.Select(e => e.ToString())),
v => v.Split(new[] { ',' }, StringSplitOptions.None)
.Select(e => (Roles)Enum.Parse(typeof(Roles), e)).ToList());
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).IsRequired().HasMaxLength(255);
builder.Property(x => x.Email).IsRequired();
builder.Property(x => x.Password).IsRequired();
builder.Property(x => x.Roles).HasConversion(userRolesConverter).IsRequired();
builder.ToTable("Users")
.OwnsOne(x => x.Endereco, x =>
{
x.Property(a => a.Unidade)
.HasColumnName("unidade")
.IsRequired();
x.Property(a => a.Complemento)
.HasColumnName("complemento")
.IsRequired();
x.Property(a => a.Cep)
.HasColumnName("cep")
.IsRequired();
x.Property(a => a.Bairro)
.HasColumnName("bairro")
.IsRequired();
x.Property(a => a.Logradouro)
.HasColumnName("logradouro")
.IsRequired();
x.Property(a => a.Uf)
.HasColumnName("uf")
.IsRequired();
x.Property(a => a.Localidade)
.HasColumnName("localidade")
.IsRequired();
});
}
}
}
Feito isso, para facilitar, apague todas as tabelas do seu banco de dados e exclua a pasta ´Migrations´ do seu projeto, apos isso rode os seguintes comandos em ordem dentro do console gerenciador de pacotes.
1- Add-Migration InitialDB -Context <nome do contexto a ser migrado>
2- Update-Database -Context <nome do contexto a ser migrado>
Como citamos nos artigos anteriores, por ter construído nossa app utilizando o padrão Clean Architecture
e o conceito de domínio não amenico, nos traz uma facilidade de mudar alguma logica em nossa api. Precisamos mudar a logica de salvar o usuário em nosso banco de dados, pois e necessário agora salvar também as Roles de acesso que esse usuário terá para acessar nossa aplicação, para isso vamos fazer apenas mudanças na classe de input e output desse caso de uso, que ficara assim.
RegisterUserInput
using FirstApi.Application.CustomValidations;
using FirstApi.Application.UseCases.PasswordHasher;
using FirstApi.Domain.Entities;
using FirstApi.Domain.Enums;
using FirstApi.Infrastructure.Integration.ViaCep;
using System.ComponentModel.DataAnnotations;
namespace FirstApi.Application.UseCases.CasesUser.RegisterUser
{
public class RegisterUserInput
{
public string? Name { get; set; }
[EmailAddress(ErrorMessage = "Invalid email address.")]
public required string Email { get; set; }
[Password(ErrorMessage = "Password must be at least 8 characters long and contain an uppercase letter, a lowercase letter, a number, and a special character.")]
public string? Password { get; set; }
public List<Roles>? Roles { get; set; }
public string? Cep { get; set; }
public User Convert(IPasswordHasher hasher, ViaCepResponse response)
{
User user = new User();
user.Name = Name;
user.Password = hasher.Hash(Password);
user.Email = Email;
user.Roles = Roles;
user.Endereco = user.GetEndereco(response);
return user;
}
}
}
RegisterUserOutput
using FirstApi.Domain.Entities;
using FirstApi.Domain.ValueObjects;
namespace FirstApi.Application.UseCases.CasesUser.RegisterUser
{
public class RegisterUserOutput
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public Endereco? Endereco { get; set; }
public List<string>? Roles { get; set; }
public RegisterUserOutput Convert(User user)
{
Id = user.Id;
Name = user.Name;
Email = user.Email;
Endereco = user.Endereco;
Roles = user.Roles.Select(x => x.ToString()).ToList();
return this;
}
}
}
Pronto, feito isso vamos implementar agora na camada de acesso externo de nossa aplicação um controller para fazer a autenticação/autorização do usuário.
AuthController
using FirstApi.Application.UseCases.CasesAuth.Login;
using Microsoft.AspNetCore.Mvc;
namespace FirstApi.Infrastructure.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController
{
private ILoginService _loginService;
public AuthController(ILoginService loginService)
{
_loginService = loginService;
}
[HttpPost]
public LoginOutput Login(LoginInput login)
{
return _loginService.Execute(login);
}
}
}
Como adicionamos interfaces de services e necessário configurar a inejçao de dependência destes na nossa classe Program.cs
e também adicionar uma configuração para que o swagger entenda o fluxo de autenticação da nossa api. Sua classe Program.cs
devera ficar assim.
using FirstApi.Application.UseCases.CasesAuth.Login;
using FirstApi.Application.UseCases.CasesEmployer.ConsultEmployer;
using FirstApi.Application.UseCases.CasesEmployer.DeleteEmployer;
using FirstApi.Application.UseCases.CasesEmployer.Register;
using FirstApi.Application.UseCases.CasesEmployer.UpdateEmployer;
using FirstApi.Application.UseCases.CasesUser.ConsultUser;
using FirstApi.Application.UseCases.CasesUser.DeleteUser;
using FirstApi.Application.UseCases.CasesUser.RegisterUser;
using FirstApi.Application.UseCases.CasesUser.UpdateUser;
using FirstApi.Application.UseCases.PasswordHasher;
using FirstApi.Domain.Repositories;
using FirstApi.Infrastructure.Data;
using FirstApi.Infrastructure.Handler;
using FirstApi.Infrastructure.Integration.ViaCep;
using FirstApi.Infrastructure.Integration.ViaCep.Refit;
using FirstApi.Infrastructure.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Refit;
using System.Text;
namespace FirstApi
{
public class Program
{
public static void Main(string[] args)
{
string secretKey = "e0606215-c2e4-46fe-8d73-a77e1fa4f45a";
var builder = WebApplication.CreateBuilder(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// Configure data base access
builder.Services.AddDbContext<SystemDbContext>(
options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add repositories to the container.
builder.Services.AddScoped<IEmployerRepository, EmployerRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
// Add services to the container.
// employer
builder.Services.AddScoped<IRegisterEmployerService, RegisterEmployerService>();
builder.Services.AddScoped<IUpdateEmployerService, UpdateEmployerService>();
builder.Services.AddScoped<IConsultEmployerService, ConsultEmployerService>();
builder.Services.AddScoped<IDeleteEmployerService, DeleteEmployerService>();
// user
builder.Services.AddScoped<IRegisterUserService, RegisterUserService>();
builder.Services.AddScoped<IUpdateUserService, UpdateUserService>();
builder.Services.AddScoped<IConsultUserService, ConsultUserService>();
builder.Services.AddScoped<IDeleteUserService, DeleteUserService>();
builder.Services.AddScoped<IPasswordHasher, PasswordHasher>();
builder.Services.AddScoped<ILoginService,LoginService>();
// client viacep
builder.Services.AddScoped<IViaCepIntegrationService, ViaCepIntegrationService>();
// Add client refit
builder.Services.AddRefitClient<IViaCepIntegrationRefit>().ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://viacep.com.br");
}
);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(sw =>
{
sw.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo {
Title = "ApiFirst", Version = "v1"
});
var securitySchema = new OpenApiSecurityScheme
{
Name = "Jwt Authentication",
Description = "Enter with your Jwt Bearer Token",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
sw.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, securitySchema);
sw.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ securitySchema, new string[] { } }
});
});
// Config authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience= true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "emprise",
ValidAudience = "firstApi",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
// global error handler
app.UseMiddleware<GlobalExceptionHandler>();
app.MapControllers();
app.Run();
}
}
}
Pronto, agora e só rodar a aplicação e testarmos a parte de segurança no swagger.
Teste usuario com token mas sem role de acesso
Dados usuarios
Gerando token usuario id 3, sem role de Admin
Request endpoint lista usuários, necessário role de Admin para acesso
Retorno statuscode 403, ou seja estamos autenticado porem nao autorizados a acessar o recurso.
Teste usuario com token e role de acesso
Vamos utilizar o mesmo token gerado com user id = 3, que possui a role USERCOMMON solicitada para buscar os dados de Employers.
Request endpoint lista employers, necessário role de USERCOMMON para acesso
Retorno statuscode 200, ou seja estamos autenticado e autorizados a acessar o recurso.
Finalizamos por aqui galerinha, deixe seu feedback por favor sobre o artigo e deixo aqui minhas redes sociais para quem quiser me adicionar e trocar uma ideia sobre desenvolvimento de softwares para aprendermos juntos, ate a próxima.
Top comments (0)