Código Módulo 1 -> Repositório GitHub
Olá Devs, este artigo vai mostrar de forma simples como criar um API de autenticação e autorização de API's através de JWT Token e definindo Roles(cargos) pra cada usuário do sistema.
Sumário - Clique e ache rápido o que deseja ler
- Regra de Negócio e Contextualização
- Banco de dados Postgres
- Criando API Básica com JWT - Models
- Configuração do JWT
- Controller Auth
- Criptografia da senha de usuário
- Configuração Startup
- Populando o Banco de Dados
- Teste com Swagger
Regra de Negócio
Modulo 1
Nesse primeiro módulo, iremos focar na aplicação pura do JWT Token com Roles. Nos próximos módulos teremos os CRUDs completos e complementos de funcionalidades.
O usuário Admin é cadastrado na mão, no banco de dados e ele é o único que tem o poder de mudar as Roles dos usuários.
As roles são cadastradas direto no banco de dados também.
O sistema cadastra um usuário.
O sistema faz Login do usuário.
Importância do Controle de Acesso
Imagina que você tem um sistema que vários usuários com cargos diferentes e você quer que cada cargo tenha um tipo de acesso diferente. Exemplo: O Assistente Financeiro terá acesso a relatórios da área de finanças, mas, apenas o Diretor financeiro terá acesso a liberar verbas, aumentar e diminuir budgets (despesas) de cada departamento.
Para que você consiga fazer esse controle por tipo de usuário, uma solução pode ser definir Roles
.
Roles
No nosso caso, no Modulo 1, teremos 2 tipos de usuários: Admin
e o User
.
-
Admin
- Acesso a todos os endpoints -
User
- Acesso ao endpoint de registro de usuários e o de login de usuários
Qual a diferença entre Autenticação e Autorização?
Autenticação
é o processo de ver quem você é, qual seu nome, senha, email, você realmente é quem você diz ser? Isso é AUTENTICARAutorização
é o processo de verificar aonde você tem permissão de acessar. Quais endpoints você terá acesso. Isso é AUTORIZAR
Exemplo: Você chega numa balada e logo na entrada precisa ser Autenticado
, eles verificaram seu nome, seu cartão de crédito e vão verificar pelo seu nome aonde você tem Autorização
de entrar se você vai apenas na pista é uma pulseira, se pode ir na pista e no camarote, já é outra pulseira. No programação essa pulseira de identificação seria o seu Token
, contendo suas informações básicas e suas Claims
, que seriam as autorizações
que você tem, compactadas naquela pulseira (token).
Criando uma API básica usando o processo de autenticação COM JWT token
Models e View Models
Role
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
}
User
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string Slug { get; set; }
public int RolesId { get; set; }
public Role Roles { get; set; }
}
LoginViewModel
public class LoginViewModel
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Email must be valid")]
public string Email { get; set; }
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; }
}
RegisterViewModel
public class RegisterViewModel
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Email must be valid")]
public string Email { get; set; }
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; }
}
Banco de Dados - Postgres
Mapeamentos
UserMap
public class UserMap : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
// Tabela
builder.ToTable("User");
// Chave Primária
builder.HasKey(x => x.Id);
// Identity
builder.Property(x => x.Id)
.ValueGeneratedOnAdd()
.UseIdentityColumn();
// Propriedades
builder.Property(x => x.Name)
.IsRequired()
.HasColumnName("Name")
.HasColumnType("VARCHAR")
.HasMaxLength(80);
builder.Property(x => x.Email)
.IsRequired()
.HasColumnName("Email")
.HasColumnType("VARCHAR")
.HasMaxLength(160);
builder.Property(x => x.PasswordHash).IsRequired()
.HasColumnName("PasswordHash")
.HasColumnType("VARCHAR")
.HasMaxLength(255);
builder.Property(x => x.Slug)
.IsRequired()
.HasColumnName("Slug")
.HasColumnType("VARCHAR")
.HasMaxLength(80);
// Índices
builder
.HasIndex(x => x.Slug, "IX_User_Slug")
.IsUnique();
}
}
RolesMap
public class RolesMap : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.ToTable("Role");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedOnAdd()
.UseIdentityColumn();
builder.Property(x => x.Name)
.HasMaxLength(128)
.HasColumnType("VARCHAR")
.HasColumnName("Name");
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
//DBContext
services.AddDbContext<DataContext>();
...
...
}
DataContext
public class DataContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
private IConfiguration configuration;
public DataContext(IConfiguration _configuration)
{
configuration = _configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNpgsql(configuration["ConnectionString:Postgres"].ToString());
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new UserMap());
modelBuilder.ApplyConfiguration(new RolesMap());
}
}
ConnectionString - secrets.json
{
"ConnectionString:Postgres": "Host=localhost;Database=Teste;Username=seu username;Password=sua senha"
}
Token de Assinatura
Configuration.cs
A chave de assinatura ou JWTKey
é no nosso caso um GUID
que colocamos dentro da
public static class Configuration
{
//Token
public static string JwtKey { get; set; } = "12345678-1234-1234-1234-123456789123";
}
O segredo por trás de tudo
TokenService.cs
Bibliotecas usadas
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
public string GenerateToken (User user)
{
//Estancia do manipulador de Token
var tokenHandler = new JwtSecurityTokenHandler();
//Chave da classe Configuration. O Token Handler espera um Array de Bytes, por isso é necessário converter
var key = Encoding.ASCII.GetBytes(Configuration.JwtKey);
//
var claims = user.GetClaims();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims), //Claims que vão compor o token
Expires = DateTime.UtcNow.AddHours(8), //Por quanto tempo vai valer o token?
SigningCredentials = //Assinatura do token, serve para identificar que mandou o token e garantir que o token não foi alterado no meio do caminho.
new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
//Gerando o token
var token = tokenHandler.CreateToken(tokenDescriptor);
//Retornando tudo como uma string
return tokenHandler.WriteToken(token);
}
tokenHandle
- Chave da classe Configuration. O Token Handler espera um Array de Bytes, por isso é necessário converter
key
- JWTKey em formato byte
claims
- Resultado do método GetClaims() que gera as Claims que vão copor o token
tokenDescriptor = new SecurityTokenDescriptor{}
- Montando o token com todos seus parâmetros.
Há mais comentários detalhando cada linha do código no próprio código.
GetClaims()
Método que adiciona dentro de uma lista todas as informações que você quer export do usuário no Jwt token através da Claim.
public static class RoleClaimExtention
{
public static IEnumerable<Claim> GetClaims(this User user)
{
var result = new List<Claim>
{
new(ClaimTypes.Name, user.Name),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Role, user.Roles.Name)
};
return result;
}
}
Controller
Bibliotecas
using SecureIdentity.Password;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
Endpoints -
-
Post
Register
- Rota -api/auth/v1/register
Registra uma nova conta de usuário no banco de dados -
Post
Login
- Rota-api/auth/v1/login
Autoriza o usuário através da validação das credenciais, e gera um JWT Token -
Patch
ChangeRole
- Rota-api/auth/v1/changeUserRole
Altera a Role do usuário no banco de dados
Classe da Controller
[Authorize]
[Route("api/auth")]
[ApiController]
public class AuthController : ControllerBase
EndPoint Register
[AllowAnonymous]
[HttpPost("v1/register")]
public async Task<IActionResult> Register([FromBody] RegisterViewModel model,
[FromServices] DataContext context)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
var user = new User
{
Name = model.Name,
Email = model.Email,
Slug = model.Email.Replace("@", "-").Replace(".", "-"),
RolesId = 1,
PasswordHash = PasswordHasher.Hash(model.Password),
};
try
{
await context.Users.AddAsync(user);
await context.SaveChangesAsync();
return Ok($"{user.Email} {user.PasswordHash}");
}
catch (DbUpdateException)
{
return StatusCode(400, "Duplicate Email");
}
catch
{
return StatusCode(500, "Internal Error");
}
}
Criptografia da senha do usuário
Usamos a classe PasswordHasher
da biblioteca SecureIdentity
para criptografar a senha digitada pelo usuário e salvar no banco esse senha criptografada. PasswordHash = PasswordHasher.Hash(model.Password)
O que é Hash?
Para segurança do usuário nunca salvar a senha do usuário sem criptografia no banco de dados.
Na hora de fazer a autenticação, e verificar a senha, é necessário discriptografar a senha do banco de dados e comparar com a senha que o usuário digitou.
PasswordHasher.Verify(Senha do banco de dados, Senha digitada pelo usuário)
EndPoint Login
[AllowAnonymous]
[HttpPost("v1/login")]
public async Task<IActionResult> Login(
[FromBody] LoginViewModel model,
[FromServices] DataContext context,
[FromServices] TokenService tokenService)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.Values);
var user = await context
.Users
.Include(x => x.Roles)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Email == model.Email);
if (user == null)
return StatusCode(401, "User or password invalid");
if (!PasswordHasher.Verify(user.PasswordHash, model.Password))
return StatusCode(401, "User or password invalid");
try
{
var token = tokenService.GenerateToken(user);
return Ok(token);
}
catch
{
return StatusCode(500, "Internal Error");
}
}
EndPoint changeUserRole
[Authorize(Roles = "admin")]
[HttpPatch("v1/changeUserRole")]
public async Task<IActionResult> ChangeRole(int UserId,
int NewRoleId,
[FromServices] DataContext context)
{
var user = await context
.Users
.FirstOrDefaultAsync(x => x.Id == UserId);
var role = await context
.Roles
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == NewRoleId);
if (user == null || role == null)
return StatusCode(401, "Invalid Id");
user.Roles = role;
await context.SaveChangesAsync();
return Ok();
}
[Authorize]
- Data Notation que limita o acesso apenas para quem está autorizado/Logado apenas.
[AllowAnonymous]
- Data Notation que permite acesso de usuários que não estão autorizados/logados na API.
[Authorize(Roles = "admin")]
- Data Notation que permite acesso de usuários que contem a role admin
em seu Claim
Configuração da Startup
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//DBContext
services.AddDbContext<DataContext>();
//JWTConfig
var key = Encoding.ASCII.GetBytes(JwtKey);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false
};
});
//Injeção de Dependencia
services.AddTransient<TokenService>();
services.AddControllers();
//Configuração do Swagger para adicionar o Bearer Token na auth
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWTAuthAuthentication", 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",
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "JWTAuthAuthentication v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
////JWTConfig - Adicionar sempre Authentication antes de Authorization
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Populando o Banco de dados
Da forma que achar melhor, através de GUIs como PgAdmin ou console, popúle a tabela de Role
como na imagem abaixo, pois ainda não criaram um endpoint para isso.
E na tabela User
crie um usuário com a role de Admin, pois ele será o ínico que terá acesso ao endpoint changeRoles
.
Teste com Swagger
Register
Login
Antes de seguirmos com a validação do token, tente alterar as roles no endpoint changeRoles
e repare que o retorno é 401, não autorizado.
O retorno que temos do login é o JTW token com as claims que definimos do token service. Para se autorizar, pegue esse Token e se tudo deu certo na configuração do Swagger, você deve ver um ícone no lado direito superior da pagina do Swagger:
Clique nele e insira o token nesse formado Bearer + Token
, como na imagem abaixo.
Porém, mesmo assim você não terá acesso ao endpoint changeRoles
, pois quando você cria um usuário, o role dele não é de admin.
Faça o login com o usuário Admin
que criou no banco de dados.
Insira o Bearer Token e altere a role do usuário que acabou de criar.
E veja que o resultado é 200, sucesso.
Para verificar o JWT token e suas claims, acesse o site Jwt.io e cole o token no quadro de Encoded.
Repare o payload do token tem o nome, email, a role e os timestamps de quando foi gerando e até quando dura o token.
Obrigado por ler este artigo. Abaixo colocarei os links dos próximos módulos quando forem lançados, a ideia é evolouirmos pouco a pouco essa aplicação.
Spoiler
Modulo 2
- CRUD da Roles
- O sistema tem sempre que ter um Admin
- Admin não pode mudar suas próprias roles
- E muito mais.
Top comments (0)