QPANC são as iniciais de Quasar PostgreSQL ASP NET Core.
- Source
- Introdução
- Parte I - ASP.NET - Inicializando os Projetos
- Parte 2 - PostgreSQL
- Parte 3 - ASP.NET - Registrando Serviços e Lendo Variáveis de Ambiente
- Parte 4 - ASP.NET - Entity Framework e ASP.NET Core Identity
- Parte 5 - ASP.NET - Documentação Interativa com Swagger
- Parte 6 - ASP.NET - Regionalização
- Parte 7 - ASP.NET - Autenticação e Autorização
- Parte 8 - ASP.NET - CORS
- Parte 9 - Quasar - Criação e Configuração do Projeto
- Parte 10 - Quasar - Configurações e Customizações
- Parte 11 - Quasar - Componentes - Diferença entre SPA e SSR
- Parte 12 - Quasar - Serviços
- Parte 13 - Quasar - Regionalização e Stores
- Parte 14 - Quasar - Consumindo a API
- Parte 15 - Quasar - Login
- Parte 16 - Quasar - Áreas Protegidas
- Parte 17 - Quasar - Registro
- Parte 18 - Docker - Maquina Virtual Linux
- Parte 19 - Docker - Registro e Build
- Parte 20 - Docker - Traefik e Publicação
- Demo Online
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
namespace QPANC.Services.Abstract
{
public interface IJwtBearer
{
byte[] IssuerSigningKey { get; }
byte[] TokenDecryptionKey { get; }
string ValidIssuer { get; }
string ValidAudience { get; }
}
}
QPANC.Services/JwtBearer.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
namespace QPANC.Services.Abstract
{
public interface IAppSettings
{
IConnectionStrings ConnectionString { get; }
IJwtBearer JwtBearer { get; }
}
}
QPANC.Services/AppSettings.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
:
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:
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
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).
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
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:
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.) ouNodeJS
(sim, é possível compartilhar código entre uma aplicaçãoNode
e outra emC#
, para saber mais, procure porEdgeJS
).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
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
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
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
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
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
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
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
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
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 nossasactions
(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
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:
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.
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:
No próximo teste, iremos informar um e-mail inexistente ou senha incorreta:
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
.
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
Assim como, poderá usar a claim JTI
, para consultar a sessão associada a este Token
no Banco de Dados.
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
public interface IAuthentication
{
Task<BaseResponse> Logout();
}
Assim, como a sua respectiva implementação no serviço Authentication
QPANC.Services/Business/Authentication.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
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:
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}
Agora, chame novamente a Action /Auth/Logout
.
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
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
public interface IAuthentication
{
Task<BaseResponse> Register(RegisterRequest register);
}
QPANC.Services/Business/Authentication.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.
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.
E finalmente, um cadastro com modelo valido, email disponível e senha forte.
Top comments (0)