DEV Community

Cover image for C# - Criando API com Jwt Token - Autorização e Autenticação - Módulo 1
Matheus Paixão
Matheus Paixão

Posted on

C# - Criando API com Jwt Token - Autorização e Autenticação - Módulo 1

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

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.

Gif

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

Gif
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?

Gif

  • Autenticação é o processo de ver quem você é, qual seu nome, senha, email, você realmente é quem você diz ser? Isso é AUTENTICAR

  • Autorizaçã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

Gif

Models e View Models

Role



public class Role
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }


Enter fullscreen mode Exit fullscreen mode

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; }
    }


Enter fullscreen mode Exit fullscreen mode

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; }
    }


Enter fullscreen mode Exit fullscreen mode

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; }
    }


Enter fullscreen mode Exit fullscreen mode

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();            
        }
    }


Enter fullscreen mode Exit fullscreen mode

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

        }
    }


Enter fullscreen mode Exit fullscreen mode

Startup.cs



public void ConfigureServices(IServiceCollection services)
        {
...
            //DBContext
            services.AddDbContext<DataContext>();
...
...
        }


Enter fullscreen mode Exit fullscreen mode

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());
        }
    }


Enter fullscreen mode Exit fullscreen mode

ConnectionString - secrets.json



{
  "ConnectionString:Postgres": "Host=localhost;Database=Teste;Username=seu username;Password=sua senha"
}


Enter fullscreen mode Exit fullscreen mode

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";
    }


Enter fullscreen mode Exit fullscreen mode

O segredo por trás de tudo

TokenService.cs

Bibliotecas usadas



using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;


Enter fullscreen mode Exit fullscreen mode


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);
        }


Enter fullscreen mode Exit fullscreen mode

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;
        }
    }


Enter fullscreen mode Exit fullscreen mode

Controller

Bibliotecas



using SecureIdentity.Password;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;


Enter fullscreen mode Exit fullscreen mode

Endpoints -

  1. Post Register - Rota - api/auth/v1/register Registra uma nova conta de usuário no banco de dados
  2. Post Login - Rota- api/auth/v1/login Autoriza o usuário através da validação das credenciais, e gera um JWT Token
  3. 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


Enter fullscreen mode Exit fullscreen mode

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");
            }
        }


Enter fullscreen mode Exit fullscreen mode

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");
            }
        }


Enter fullscreen mode Exit fullscreen mode

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();
        }


Enter fullscreen mode Exit fullscreen mode

[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();
            });
        }
    }


Enter fullscreen mode Exit fullscreen mode

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.

ROLES

E na tabela User crie um usuário com a role de Admin, pois ele será o ínico que terá acesso ao endpoint changeRoles.
User

Teste com Swagger

Register

Register

Login

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.

Logins

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:

Retornojtw

Clique nele e insira o token nesse formado Bearer + Token, como na imagem abaixo.

Bearertoken

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.

Sucesso

Para verificar o JWT token e suas claims, acesse o site Jwt.io e cole o token no quadro de Encoded.

Image description

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)