QPANC - Parte 7 - ASP.NET - Autenticação e Autorização
Tags: quasar, postgres, aspnetcore, docker
Published: 2020-04-19 23:37:10

> **QPANC** são as iniciais de **Q**uasar **P**ostgreSQL **A**SP **N**ET **C**ore.

  * [Source](https://github.com/TobyMosque/qpanc)
  * [Introdução](https://dev.to/quasar-pt-br/arquiteturando-uma-aplicacao-com-quasar-postgresql-asp-net-core-e-docker-introducao-o7o)
  * [Parte I - ASP.NET - Inicializando os Projetos](https://dev.to/quasar-pt-br/qpanc-parte-1-asp-net-inicializando-os-projetos-lkm)
  * [Parte 2 - PostgreSQL](https://dev.to/quasar-pt-br/qpanc-parte-2-postgresql-51pb)
  * [Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente](https://dev.to/quasar-pt-br/qpanc-parte-3-asp-net-registrando-servicos-e-lendo-variaveis-de-ambiente-3do2)
  * [Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity](https://dev.to/quasar-pt-br/qpanc-parte-4-asp-net-entity-framework-e-asp-net-core-identity-40f9)
  * [Parte 5 - ASP.NET - Documentação Interativa com Swagger](https://dev.to/quasar-pt-br/qpanc-parte-5-asp-net-documentacao-interativa-com-swagger-4n0o)
  * [Parte 6 - ASP.NET - Regionalização](https://dev.to/quasar-pt-br/qpanc-parte-6-asp-net-regionalizacao-13p5)
  * [Parte 7 - ASP.NET - Autenticação e Autorização](https://dev.to/quasar-pt-br/qpanc-parte-7-asp-net-autenticacao-e-autorizacao-bpd)
  * [Parte 8 - ASP.NET - CORS](https://dev.to/quasar-pt-br/qpanc-parte-8-asp-net-cors-52dn)
  * [Parte 9 - Quasar - Criação e Configuração do Projeto](https://dev.to/quasar-pt-br/qpanc-parte-9-quasar-criacao-e-configuracao-do-projeto-5hbi)
  * [Parte 10 - Quasar - Configurações e Customizações](https://dev.to/quasar-pt-br/qpanc-parte-10-quasar-configuracoes-e-customizacoes-2i86)
  * [Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR](https://dev.to/quasar-pt-br/qpanc-parte-11-quasar-componentes-diferenca-entre-spa-e-ssr-1a3c)
  * [Parte 12 - Quasar - Serviços](https://dev.to/quasar-pt-br/qpanc-parte-12-quasar-servicos-4e61)
  * [Parte 13 - Quasar - Regionalização e Stores](https://dev.to/quasar-pt-br/qpanc-parte-13-quasar-regionalizacao-e-stores-1kep)
  * [Parte 14 - Quasar - Consumindo a API](https://dev.to/quasar-pt-br/qpanc-parte-14-quasar-consumindo-a-api-fn6)
  * [Parte 15 - Quasar - Login](https://dev.to/quasar-pt-br/qpanc-parte-15-quasar-login-jc6)
  * [Parte 16 - Quasar - Áreas Protegidas](https://dev.to/quasar-pt-br/qpanc-parte-16-quasar-areas-protegidas-3jkn)
  * [Parte 17 - Quasar - Registro](https://dev.to/quasar-pt-br/qpanc-parte-17-quasar-registro-3b8c)
  * [Parte 18 - Docker - Maquina Virtual Linux](https://dev.to/quasar-pt-br/qpanc-parte-18-docker-maquina-virtual-linux-g5c)
  * [Parte 19 - Docker - Registro e Build](https://dev.to/quasar-pt-br/qpanc-parte-19-docker-registro-e-build-31i4)
  * [Parte 20 - Docker - Traefik e Publicação](https://dev.to/quasar-pt-br/qpanc-parte-20-docker-traefik-e-publicacao-3g3d)
  * [Demo Online](https://qpanc.tobiasmesquita.dev)

##12 Autenticação usando tokens JWT

Agora que temos o Swagger configurado, precisamos configurar um aspecto importante da aplicação, a geração dos tokens que serão utilizados para autenticação e autorização.

Como de costume, iremos precisar instalar alguns pacotes, iremos começar pelo `System.IdentityModel.Tokens.Jwt` e `Microsoft.AspNetCore.Authentication.JwtBearer` no projeto `QPANC.Api`

```
cd QPANC.Api
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

O próximo passo, é criar os serviços necessários para expor as configurações de geração dos Tokens.

**QPANC.Services.Abstract/IJwtBearer.cs**
```cs
namespace QPANC.Services.Abstract
{
    public interface IJwtBearer
    {
        byte[] IssuerSigningKey { get; }
        byte[] TokenDecryptionKey { get; }
        string ValidIssuer { get; }
        string ValidAudience { get; }
    }
}
```
**QPANC.Services/JwtBearer.cs**
```cs
using Microsoft.Extensions.Configuration;
using QPANC.Services.Abstract;
using System;
using IConfiguration = QPANC.Services.Abstract.IConfiguration;

namespace QPANC.Services
{
    public class JwtBearer : IJwtBearer
    {
        private IConfiguration _configuration;

        public JwtBearer(IConfiguration configuration)
        {
            this._configuration = configuration;
        }

        public string ValidIssuer { get { return this._configuration.Root.GetValue<string>("JWTBEARER_VALIDISSUER"); } }

        public string ValidAudience { get { return this._configuration.Root.GetValue<string>("JWTBEARER_VALIDAUDIENCE"); } }

        public byte[] IssuerSigningKey { get { return this.Base64AsBinary("JWTBEARER_ISSUERSIGNINGKEY"); } }

        public byte[] TokenDecryptionKey { get { return this.Base64AsBinary("JWTBEARER_TOKENDECRYPTIONKEY"); } }

        private byte[] Base64AsBinary(string key)
        {
            var base64 = _configuration.Root.GetValue<string>(key);
            if (string.IsNullOrWhiteSpace(base64))
                return new byte[] { };
            return Convert.FromBase64String(base64);
        }
    }
}
```

E claro, não podemos deixar de adicionar este serviço no nosso agregador de configurações, o `IAppSettings`

**QPANC.Services.Abstract/IAppSettings.cs**
```cs
namespace QPANC.Services.Abstract
{
    public interface IAppSettings
    {
        IConnectionStrings ConnectionString { get; }
        IJwtBearer JwtBearer { get; }
    }
}
```
**QPANC.Services/AppSettings.cs**
```cs
using QPANC.Services.Abstract;

namespace QPANC.Services
{
    public class AppSettings : IAppSettings
    {
        public IConnectionStrings ConnectionString { get; }
        public IJwtBearer JwtBearer { get; }

        public AppSettings(IConnectionStrings connectionStrings, IJwtBearer jwtBetter)
        {
            this.ConnectionString = connectionStrings;
            this.JwtBearer = jwtBetter;
        }
    }
}
```

e o respectivo registro no `QPANC.Api/Extensions/ServiceCollectionExtensions.cs`:

```cs
using Microsoft.Extensions.DependencyInjection;
using QPANC.Services;
using QPANC.Services.Abstract;

namespace QPANC.Api.Extensions
{
    public static class ServiceCollectionExtensions
    {
        public static void AddAppSettings(this IServiceCollection services)
        {
            services.AddSingleton<IConfiguration, Configuration>();
            services.AddSingleton<IConnectionStrings, ConnectionStrings>();
            services.AddSingleton<IJwtBearer, JwtBearer>();
            services.AddSingleton<IAppSettings, AppSettings>();
        }
    }
}
```

Agora que conseguimos acessar as chaves, teremos criar as chaves propriamente dito, para tal, é recomendado que se gere 96 bytes de forma aleatória, iremos usar 64 destas bytes para criar a chave para assinar o token e 32 para a chave para descriptografar o token.:

Segue uma sugestão de implementação para geração destas chaves:

```cs
namespace QPANC.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var random = RandomNumberGenerator.Create();
            var binarySigningKey = new byte[64];
            var binaryTokenDecryptionKey = new byte[32];
            random.GetBytes(binarySigningKey);
            random.GetBytes(binaryTokenDecryptionKey);
            var signingKey = Convert.ToBase64String(binarySigningKey);
            var tokenDecryptionKey = Convert.ToBase64String(binaryTokenDecryptionKey);
            Console.WriteLine($"Sign: ${signingKey} | Decrypt: ${tokenDecryptionKey}");
        }
    }
}
```

Não esqueça de remover, ou pelo menos desativar este trecho do código.

Agora que geramos as chaves, devemos salvar elas em algum lugar, neste caso, no `docker-compose.override.yml`

```
version: '3.4'

services:
  qpanc.api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=https://+:443;http://+:80
      - DEFAULT_CONNECTION=Server=qpanc.database;Port=5432;Database=postgres;User Id=postgres;Password=keepitsupersecret;
      - JWTBEARER_VALIDISSUER=https://api.qpanc.app/
      - JWTBEARER_VALIDAUDIENCE=https://api.qpanc.app/
      - JWTBEARER_ISSUERSIGNINGKEY=itUXC7iVRsofSDWNeg/aLYpc4bMzHAsMPzeItE1PQi2tMK2f4t0InRgTE5B/4IAjhAX5LQSIGL1CaUHSSzED8A==
      - JWTBEARER_TOKENDECRYPTIONKEY=7hfboHG0d4GnXjVng0ukMo+IgrKKrPLUMtOvnt4S514=
```

Agora que criamos as chaves, falta apenas realizar a implementação do `LoggedUser`, mova ela de `QPANC.Services` para `QPANC.Api/Services` e altere a sua implementação para:

**QPANC.Api.Services/LoggedUser**
```cs
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.JsonWebTokens;
using QPANC.Services.Abstract;
using System;
using System.Linq;

namespace QPANC.Api.Services
{
    public class LoggedUser : ILoggedUser
    {
        private IHttpContextAccessor _context;
        public LoggedUser(IHttpContextAccessor context)
        {
            this._context = context;
            if (this._context.HttpContext.User.Identity.IsAuthenticated)
            {
                var sub = this._context.HttpContext.User.Claims
                        .Where(x => x.Type == ClaimTypes.NameIdentifier)
                        .Select(x => x.Value)
                        .FirstOrDefault();
                var jti = this._context.HttpContext.User.Claims
                        .Where(x => x.Type == JwtRegisteredClaimNames.Jti)
                        .Select(x => x.Value)
                        .FirstOrDefault();
                if (sub != default)
                {
                    this.UserId = Guid.Parse(sub);
                }
                if (jti != default)
                {
                    this.SessionId = Guid.Parse(jti);
                }
            }
        }

        public Guid? SessionId { get; private set; }
        public Guid? UserId { get; private set; }
    }
}
```

Já que houve uma mudança no namespace to `LoggedUser`, será preciso atualizar o `Startup.cs` no projeto `QPANC.Api`.

Caso não conheça a anatomia de um token JWT e não faça ideia do que é JTI e SUB, recomendo que leia sobre em [JSON Web Tokens (Part 1)](https://dev.to/iiits-iota/json-web-tokens-part-1-5c83#payload).

Agora iremos criar um Options, ou seria um Middleware? ele será responsável por validar o token, e quando necessário, disponibilizar uma forma alternativa de passar o mesmo para API, como por exemplo, através da querystring, ao invés de um header.

**./QPANC.Api/Options/JwtBearer.js**
```cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using QPANC.Domain;
using QPANC.Services.Abstract;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;

namespace QPANC.Api.Options
{
    public class JwtBearer : IConfigureOptions<JwtBearerOptions>
    {
        private IServiceProvider _provider;
        private IJwtBearer _settings;

        public JwtBearer(IServiceProvider provider, IJwtBearer settings)
        {
            this._provider = provider;
            this._settings = settings;
        }

        private async Task OnTokenValidated(TokenValidatedContext context)
        {
            using (var scope = this._provider.CreateScope())
            {
                var db = scope.ServiceProvider.GetRequiredService<QpancContext>();
                var sessionId = Guid.Parse(context.Principal.Claims
                    .Where(x => x.Type == JwtRegisteredClaimNames.Jti)
                    .Select(x => x.Value)
                    .FirstOrDefault());

                var session = await db.Sessions.FindAsync(sessionId);
                if (session == null)
                {
                    context.Fail("");
                }
                else 
                {
                    context.Success();
                }
            }
        }

        private async Task OnMessageReceived(MessageReceivedContext context)
        {
            await Task.Yield();
            // var urlPath = context.Request.Path.ToString();
            // if (urlPath.StartsWith("/signalr/") && context.Request.Query.ContainsKey("bearer"))
            //     context.Token = context.Request.Query["bearer"];
        }

        public void Configure(JwtBearerOptions options)
        {
            options.RequireHttpsMetadata = false;
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = _settings.ValidIssuer,
                ValidAudience = _settings.ValidAudience,
                IssuerSigningKey = new SymmetricSecurityKey(_settings.IssuerSigningKey),
                TokenDecryptionKey = new SymmetricSecurityKey(_settings.TokenDecryptionKey),
                RequireExpirationTime = false,
                ClockSkew = TimeSpan.Zero
            };

            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = OnMessageReceived,
                OnTokenValidated = OnTokenValidated
            };
        }
    }
}
```

Como mencionado anteriormente, iremos utilizar a tabela `Sessions` para invalidar os tokens após um Logout, para que o token não possa ser usado por terceiros após ao usuário realizar um logout. Isto é feito no `OnTokenValidated`

Por enquanto o `OnMessageReceived` não fará muito, e é bastante provável que não o fará na maioria dos projetos. Mas ele poderá ser útil em alguns casos bem específicos, como autenticação por token em um Hub do `SignalR`.

Agora, que já escrevemos os serviços, options, middlewares, chaves, etc... temos que registrar tudo:

```cs
using QPANC.Api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using System.IdentityModel.Tokens.Jwt;

namespace QPANC.Api
{
    public class Startup
    {
        private IServiceProvider _provider;
    
        public void ConfigureServices(IServiceCollection services)
        {
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(
                authenticationScheme: JwtBearerDefaults.AuthenticationScheme,
                configureOptions: options =>
                {
                    _provider.GetRequiredService<IConfigureOptions<JwtBearerOptions>>().Configure(options);
                });
            services.ConfigureOptions<ConfigureOptions.JwtBearer>();
        }
        
        
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMigrateLegacyService migrateLegacy)
        {
            this._provider = app.ApplicationServices;
        }
    }
}
```

###12.1 Chore - Alteração na Estrutura dos serviços

Antes e continuamos, crie as pasta `Infra` e `Business` dentro do projeto `QPANC.Services` e `QPANC.Servives.Abstract`. Então mova todos os arquivos de ambos os projetos para a pasta `Infra` do respectivo projeto.

###12.2 Sugestão - Tokens de breve duração com renovação automática.

Caso precise, implementar Tokens de breve duração com renovação automática, adicione o campo `TokenId` à tabela `SessionId`.

Na geração do Token, defina o `ExpireAt` para ocorrer em x minutos, o `JTI` será o `TokenId` e adicione uma nova `claim` com o `SessionId`.

Durante a validação do `Token`, verifique se a `Session` está ativa, e se o `JTI` do `token` atual corresponde ao `TokenId`.

E por fim, precisaremos de uma `Action` para renovar o token, o que basicamente irá criar um novo `token` com o mesmo `SessionId`, porém com a data de expiração atualizada, e um novo `TokenId`.

No tocante a aplicação `Web`, será necessário apenas adicionar um `setInterval`, que chame o `endpoint` de renovação a cada y minutos.

##13 Controller para Autenticação

> Vou começar este capítulo com uma nota/desabafo pessoal, eu não sou um grande fã de arquiteturas com excesso de organização, daquelas que ao abrir a solução, você se depara com 937 projetos. Isto ao meu ver, é apenas `Over-Architecting`, ou seja, um design ruim e inchado que é vendido como bom e indispensável para a manutenção do código.

> Porém, vejo grande valor em separar as regras de negocio das peculiaridades técnicas da camada de apresentação, afinal, as regras de negocio devem ser as mesmas, independente do front ser uma aplicação `Xamarim`, `WebAPI`, `Mvc` `WebPages`, `WCF` (R.I.P.) ou `NodeJS` (sim, é possível compartilhar código entre uma aplicação `Node` e outra em `C#`, para saber mais, procure por `EdgeJS`).

> De toda forma, como mencionei durante a Introdução, não se sinta intimidado por causa da minha opinião, caso veja valor neste tipo de arquitetura, siga em frente.

Instale o pacote `Microsoft.`

O primeiro serviço que iremos desenvolver, será responsável pelas rotinas de autenticação, que são: `Login`, `Logout` e `Register`.

O primeiro passo, será criar os Modelos POCO, que serão consumidos pelo Serviço:

A primeira classe, será o `BaseResponse.cs`, que é o objeto que será retornado por todos os métodos dos Serviços com regras de Negocio (Business). Estamos utilizando o `HttpStatusCode` como retorno, por ser um padrão conhecido e de fácil reconhecimento, porém os serviços podem vir a ser consumidos por aplicações fora da web.

**QPANC.Services.Abstract/Models/BaseResponse.cs**
```cs
using System.Collections.Generic;
using System.Net;

namespace QPANC.Services.Abstract
{
    public class BaseResponse
    {
        public BaseResponse() { }
        public BaseResponse(HttpStatusCode statusCode)
        {
            this.StatusCode = statusCode;
        }

        public HttpStatusCode StatusCode { get; set; }
        public Dictionary<string, string> Errors { get; set; }
    }

    public class BaseResponse<T> : BaseResponse
    {
        public BaseResponse() : base() { }
        public BaseResponse(HttpStatusCode statusCode) : base(statusCode) { }

        public T Data { get; set; }
    }
}
```
o Login, irá esperar um objeto do tipo `LoginRequest` e retornar um do tipo `LoginResponse`.

**QPANC.Services.Abstract/Models/LoginRequest.cs**
```cs
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;

namespace QPANC.Services.Abstract
{
    [DataContract]
    public class LoginRequest
    {
        [EmailAddress(ErrorMessage = nameof(Messages.ErrorMessage_Email))]
        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_UserName))]
        [DataMember]
        public string UserName { get; set; }

        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_Password))]
        [DataMember]
        public string Password { get; set; }
    }
}
```
**QPANC.Services.Abstract/Models/LoginRequest.cs**
```cs
using System;
using System.Runtime.Serialization;

namespace QPANC.Services.Abstract
{
    [DataContract]
    public class LoginResponse
    {
        [DataMember]
        public Guid SessionId { get; set; }
        [DataMember]
        public Guid UserId { get; set; }
        [DataMember]
        public string UserName { get; set; }
        [DataMember]
        public DateTimeOffset ExpiresAt { get; set; }
    }
}
```

Note a presença das seguintes declarações: `nameof(Messages.ErrorMessage_Required)`, `nameof(Messages.Field_UserName)`, `nameof(Messages.Field_Password)`. Como discutido no capitulo de regionalização, elas são necessárias para localizar a aplicação.

Agora, crie a interface `IAuthentication.cs` na pasta `Business` e adicionar o método `Login`

**QPANC.Services.Abstract/Business/IAuthentication.cs**
```cs
using System.Threading.Tasks;

namespace QPANC.Services.Abstract
{
    public interface IAuthentication
    {
        Task<BaseResponse<LoginResponse>> Login(LoginRequest login);
    }
}
```

A implementação, será feita no projeto `QPANC.Services`, no arquivo `Authentication.cs` dentro da pasta `Business`

**QPANC.Services.Abstract/Business/IAuthentication.cs**
```cs
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using QPANC.Domain;
using QPANC.Domain.Identity;
using QPANC.Services.Abstract;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;

namespace QPANC.Services
{
    public class Authentication : IAuthentication
    {
        private readonly UserManager<User> _userManager;
        private readonly QpancContext _context;
        private readonly ISGuid _sguid;
        private readonly ILoggedUser _loggedUser;
        private readonly IStringLocalizer _localizer;

        public Authentication(
            UserManager<User> userManager,
            QpancContext context,
            ISGuid sguid,
            ILoggedUser loggedUser,
            IStringLocalizer<Messages> localizer)
        {
            this._userManager = userManager;
            this._context = context;
            this._sguid = sguid;
            this._loggedUser = loggedUser
            this._localizer = localizer;
            
        }

        public async Task<BaseResponse<LoginResponse>> Login(LoginRequest login)
        {
            var incorrectPasswordOrUsername = new BaseResponse<LoginResponse>
            {
                StatusCode = HttpStatusCode.UnprocessableEntity,
                Errors = new Dictionary<string, string>
                {
                    { nameof(login.Password), this._localizer[nameof(Messages.ErrorMessage_IncorrectPasswordOrUsername)] }
                }
            };

            var user = await this._userManager.FindByNameAsync(login.UserName);
            if (user == default)
            {
                return incorrectPasswordOrUsername;
            }

            var isAuthenticated = await this._userManager.CheckPasswordAsync(user, login.Password);
            if (!isAuthenticated)
            {
                return incorrectPasswordOrUsername;
            }

            var expiresAt = DateTimeOffset.Now.AddYears(1);
            var sessionId = this._sguid.NewGuid();
            var session = new Domain.Identity.Session
            {
                SessionId = sessionId,
                UserId = user.Id,
                ExpireAt = expiresAt
            };
            this._context.Sessions.Add(session);
            await this._context.SaveChangesAsync();

            return new BaseResponse<LoginResponse>
            {
                StatusCode = HttpStatusCode.OK,
                Data = new LoginResponse
                { 
                    SessionId = session.SessionId,
                    UserId = session.UserId,
                    UserName = user.UserName,
                    ExpiresAt = session.ExpireAt
                }
            };
        }
    }
}
```

Note, que este método, valida se o usuário existe, se o password está correto, cria a sessão que será utilizada para rastrear/verificar o usuário, porém não cria o `Token`, afinal o uso de `Token JWT` é uma estrategia utilizada por APIs `REST`/`GraphQL`, mas outras interfaces, podem vir a adotar outras estrategias, como `Cookies`, `Windows Login`, `(Azure) Active Directory`, etc.

Falando em `Tokens`, adicione a interface `ITokenGenerator` ao projeto `QPANC.Services` dentro da pasta `Infra`

**QPANC.Services.Abstract/Infra/ITokenGenerator.cs**
```cs
using System.Threading.Tasks;

namespace QPANC.Services.Abstract
{
    public interface ITokenGenerator
    {
        Task<string> Generate(LoginResponse userId);
    }
}
```

E agora a implementação, que será feita no projeto da API.

**QPANC.Api/Services/TokenGenerator.cs**
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using QPANC.Domain;
using QPANC.Services.Abstract;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;

namespace QPANC.Api.Services
{
    public class TokenGenerator : ITokenGenerator
    {
        private QpancContext _context;
        private IJwtBearer _jwtBearer;

        public TokenGenerator(QpancContext context, IJwtBearer jwtBearer)
        {
            this._context = context;
            this._jwtBearer = jwtBearer;
        }

        public async Task<string> Generate(LoginResponse login)
        {
            var roles = await this._context.UserRoles
                .Where(x => x.UserId == login.UserId)
                .Select(x => x.Role.Name)
                .ToListAsync();

            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, login.UserName),
                new Claim(JwtRegisteredClaimNames.Jti, login.SessionId.ToString()),
                new Claim(ClaimTypes.NameIdentifier, login.UserId.ToString())
            };

            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }

            var keySigning = new SymmetricSecurityKey(this._jwtBearer.IssuerSigningKey);
            var signing = new SigningCredentials(keySigning, SecurityAlgorithms.HmacSha256);

            // var keyEncrypting = new SymmetricSecurityKey(this._jwtBearer.TokenDecryptionKey);
            // var encrypting = new EncryptingCredentials(keyEncrypting, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

            var handler = new JwtSecurityTokenHandler();
            handler.OutboundClaimTypeMap.Clear();

            var token = handler.CreateJwtSecurityToken(
                issuer: this._jwtBearer.ValidIssuer,
                audience: this._jwtBearer.ValidAudience,
                subject: new ClaimsIdentity(claims),
                notBefore: DateTime.Now,
                expires: login.ExpiresAt.UtcDateTime,
                issuedAt: DateTime.Now,
                signingCredentials: signing
                // encryptingCredentials: encrypting
            );
            return handler.WriteToken(token);
        }
    }
}
```

Note que o `keyEncrypting` e o `encrypting` estão comentados, ative estas linhas, caso deseje que o `payload` do token seja criptografado, lembrando que ao faze-lo, a aplicação cliente (browser) não será capaz de ler o `payload`.

Antes de criamos a `AuthController`, iremos criar à `ControllerBase`, ela terá os métodos que serão comuns as demais `Controllers`.

**QPANC.Api/Controllers/ControllerBase.cs**
```cs
using Microsoft.AspNetCore.Mvc;
using QPANC.Services.Abstract;

namespace QPANC.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase
    {
        internal IActionResult ParseResult<T>(BaseResponse<T> result)
        {
            if (result.StatusCode == System.Net.HttpStatusCode.OK)
            {
                return Ok(result.Data);
            }
            else if (result.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
            {
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(error.Key, error.Value);
                }
                var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>();
                return options.Value.InvalidModelStateResponseFactory(ControllerContext);
            }
            else
            {
                return StatusCode((int)result.StatusCode);
            }
        }

        internal IActionResult ParseResult(BaseResponse result)
        {
            if (result.StatusCode == System.Net.HttpStatusCode.OK)
            {
                return Ok();
            }
            else if (result.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
            {
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(error.Key, error.Value);
                }
                var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>();
                return options.Value.InvalidModelStateResponseFactory(ControllerContext);
            }
            else
            {
                return StatusCode((int)result.StatusCode);
            }
        }
    }
}
```

e agora, a tão esperada `AuthController`

**QPANC.Api/Controllers/AuthController.cs**
```cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using QPANC.Services.Abstract;
using System.Threading.Tasks;

namespace QPANC.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class AuthController : ControllerBase
    {
        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IAuthentication _authentication;
        private readonly ITokenGenerator _tokenGenerator;

        public AuthController(IAuthentication authentication, ITokenGenerator tokenGenerator, ILogger<WeatherForecastController> logger)
        {
            this._authentication = authentication;
            this._tokenGenerator = tokenGenerator;
            this._logger = logger;
        }

        [HttpPost]
        [Route("[action]")]
        public async Task<IActionResult> Login(LoginRequest model)
        {
            var result = await this._authentication.Login(model);
            if (result.StatusCode == System.Net.HttpStatusCode.OK)
            {
                var token = await this._tokenGenerator.Generate(result.Data);
                return Ok(token);
            }
            return this.ParseResult(result);
        }
    }
}
```

Normalmente as nossas` actions` (métodos das `controllers`) terão apenas duas linhas, o login acima é uma exceção, devido a necessidade de gerar o `Token`.

Antes de podemos testar, teremos de registrar todos os serviços, pois infelizmente eles não se registram sozinhos (ainda).

**QPANC.Api/Startup.cs**
```cs
namespace QPANC.Api
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ITokenGenerator, TokenGenerator>();
            services.AddScoped<IAuthentication, Authentication>();
        }
    }
}
```

Agora podemos executar a aplicação para realizar os nossos testes:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xka9p0tjzg66e1x8wp1i.png)

Primeiro, faça um `POST` para `/Auth/Login` passando o `userName` e/ou o `password` vazios. Como ambos foram especificados como requeridos no modelo, a `API` irá identificar que o modelo está invalido, e irá retornar um Status 422 com os respectivos erros. Vale lembrar, que neste caso, a chamada não chegará a Action.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4pzjadn92awtizc0s58u.png)

Como o meu navegador está em português, as mensagens foram exibidas em português, agora iremos alterar a linguagem, no meu caso para inglês, e tentar novamente:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qbuiylr9une0o07fem6s.png)

No próximo teste, iremos informar um e-mail inexistente ou senha incorreta:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4q9zlkgnl122p977gjof.png)

Desta vez, como o modelo estava valido, a requisição chegou a `Action`, porém ocorreu uma falha durante uma das validações.

Por fim, iremos informar o um `email` existente e a sua respectiva senha, caso não se lembre deles, consulte o serviço `Seeder`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/khmo8ku94dmfsj3omfxq.png)

Finalmente, tivemos um retorno `200 (Success)` e com isto obtivemos o nosso primeiro `token`.:

```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkZXZlbG9wZXJAcXBhbmMuYXBwIiwianRpIjoiMDE3MTMzNjktNTE5Yy00NTkzLWJmM2UtMjc4NDRlNjkwMzNkIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIwMTcxMzM1NC1kZjlkLTRmYzgtYjIxYi1iNDI1ODNkOTNkODkiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJEZXZlbG9wZXIiLCJuYmYiOjE1ODU3MDU0NzIsImV4cCI6MTYxNzI0MTQ3MiwiaWF0IjoxNTg1NzA1NDcyLCJpc3MiOiJodHRwczovL2FwaS5xcGFuYy5hcHAvIiwiYXVkIjoiaHR0cHM6Ly9hcGkucXBhbmMuYXBwLyJ9.8KzqLIOKgv0rZwQQbZqeE9lf96yh18VCbPZ1m5txZ9Q
```

Você poderá decodificar este `token` no site `JWT.io`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ccxaz64m34jw75hy5qtr.png)

Assim como, poderá usar a claim `JTI`, para consultar a sessão associada a este `Token` no Banco de Dados.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/n56f5h06fopokpzaaqok.png)

Agora que conseguimos realizar um login, vamos implementar o logout, primeiramente, adicione o seguinte método a interface `IAuthentication`

**QPANC.Services.Abstract/Business/IAuthentication.cs**
```cs
public interface IAuthentication
{
    Task<BaseResponse> Logout();
}
```

Assim, como a sua respectiva implementação no serviço `Authentication`

**QPANC.Services/Business/Authentication.cs**
```cs
public class Authentication : IAuthentication
{
    public async Task<BaseResponse> Logout()
    {
        var session = await this._context.Sessions.FindAsync(this._loggedUser.SessionId);
        if (session == default)
        {
            return new BaseResponse(statusCode: HttpStatusCode.NotFound);
        }
        this._context.Sessions.Remove(session);
        await this._context.SaveChangesAsync();
        return new BaseResponse(statusCode: HttpStatusCode.OK);
    }
}
```

E por fim, a `Action` Logout na controller `AuthController`

**QPANC.Api/Controllers/AuthController.cs**
```cs
using Microsoft.AspNetCore.Authorization;

namespace QPANC.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class AuthController : ControllerBase
    {
        [Authorize]
        [HttpDelete]
        [Route("[action]")]
        public async Task<IActionResult> Logout()
        {
            var result = await this._authentication.Logout();
            return this.ParseResult(result);
        }
    }
}
```

Note a presença do Atributo `Authorize`, ele é necessário, para garantir que apenas usuários logados possam realizar um Logout, agora vamos aos testes.:

Primeiro, vamos tentar realizar um logout, sem passar o token jwt nos headers:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hifrgxxo4q0x9dfiwvxj.png)

Como esperado, a `API` retornou o status 401, ou seja, não autenticado. Antes de realizar o próximo teste, precisamos nos autenticar, para fazer isto na interface do `Swagger UI`, clique o botão com o rotulo "Authorize", e cole `bearer ${Token}`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/puw9w0964wfujkkigi61.png)

Agora, chame novamente a Action `/Auth/Logout`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0rxo1c54121lnayou30g.png)

Lembrando que, no caso de um `Token JWT`, o Logout não precisa tomar nenhuma ação adicional, mas em outras estrategias, como por exemplo cookies, pode vir a exigir alguns passos extras.

E para finalizamos este capitulo, precisamos criar a opção de registrar um usuário.

como de praxe, iremos iniciar o desenvolvimento desta ação pelo modelo:

**QPANC.Services.Abstract/Models/RegisterRequest.cs**
```cs
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;

namespace QPANC.Services.Abstract
{
    [DataContract]
    public class RegisterRequest
    {
        [EmailAddress(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_UserName))]
        [DataMember]
        public string UserName { get; set; }

        [Compare(nameof(RegisterRequest.UserName), ErrorMessage = nameof(Messages.ErrorMessage_Compare))]
        [EmailAddress(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_ConfirmUserName))]
        [DataMember]
        public string ConfirmUserName { get; set; }

        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_FirstName))]
        [DataMember]
        public string FirstName { get; set; }

        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_LastName))]
        [DataMember]
        public string LastName { get; set; }

        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_Password))]
        [DataMember]
        public string Password { get; set; }

        [Compare(nameof(RegisterRequest.Password), ErrorMessage = nameof(Messages.ErrorMessage_Compare))]
        [Required(ErrorMessage = nameof(Messages.ErrorMessage_Required))]
        [Display(Name = nameof(Messages.Field_ConfirmPassword))]
        [DataMember]
        public string ConfirmPassword { get; set; }
    }
}
```

então, devemos incrementar o nosso serviço de Autenticação:

**QPANC.Services.Abstract/Business/IAuthentication.cs**
```cs
public interface IAuthentication
{
    Task<BaseResponse> Register(RegisterRequest register);
}
```
**QPANC.Services/Business/Authentication.cs**
```cs
public class Authentication
{
    public async Task<BaseResponse> Register(RegisterRequest register)
    {
        var userNameAlreadyTaken = new BaseResponse<LoginResponse>
        {
            StatusCode = HttpStatusCode.UnprocessableEntity,
            Errors = new Dictionary<string, string>
            {
                { nameof(register.UserName), this._localizer[nameof(Messages.ErrorMessage_UserNameAlreadyTaken)] }
            }
        };

        var passwordTooWeak = new BaseResponse<LoginResponse>
        {
            StatusCode = HttpStatusCode.UnprocessableEntity,
            Errors = new Dictionary<string, string>
            {
                { nameof(register.Password), this._localizer[nameof(Messages.ErrorMessage_PasswordTooWeak)] }
            }
        };

        var user = await this._userManager.FindByNameAsync(register.UserName);
        if (user != default)
        {
            return userNameAlreadyTaken;
        }

        user = new User()
        {
            UserName = register.UserName,
            FirstName = register.FirstName,
            LastName = register.LastName
        };
        var result = await this._userManager.CreateAsync(user, register.Password);
        if (result.Succeeded)
        {
            return new BaseResponse(statusCode: HttpStatusCode.OK);
        }
        else
        {
            // email is valid, that isn't take, so if something goes whong, this must be the password =D
            return passwordTooWeak;
        }
    }
}
```

E claro, temos de adicionar uma Action à Controller.

```cs
namespace QPANC.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class AuthController : ControllerBase
    {
        [HttpPost]
        [Route("[action]")]
        public async Task<IActionResult> Register(RegisterRequest model)
        {
            var result = await this._authentication.Register(model);
            return this.ParseResult(result);
        }
    }
}
```

Agora vamos fazer alguns testes, primeiro usando um modelo inconsistente, então um com um email em uso, depois uma senha fraca.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/smsa2fuc1gmqsp9mgr1h.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cehjz04sep5nm06mlvqe.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/75g8mc0dahdyz4z0axlo.png)

E finalmente, um cadastro com modelo valido, email disponível e senha forte.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gwu4uqdta2kdclqfzprd.png)