ASP.net Core APP 04 – Construção de API seguras com JWT

O objetivo deste tutorial é construir uma API RESTFul utilizando o processo de autenticação e autorização através do padrão JWT. Esse fonte foi baseado no tutorial escrito pelo Microsoft MVP Renato Groff em seu blog no Medium. Peço que você acesse o material e deixe um gostei como forma de agradecimento: https://medium.com/@renato.groffe/jwt-asp-net-core-2-2-exemplo-de-implementa%C3%A7%C3%A3o-3e10058c1a73

O primeiro passo que precisamos realizar é construir uma simples API REST utilizando o ASP.net Core. Para isso vamos dentro das pasta controller criar uma nova classe chamada ProductAPIController, para diferenciar dos controladores da API WEB. Observe que basta anotar a classe com o decorator Route indicando o caminho da API, e o método com o decorator [HttpGet]

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using myshop.Models;
using myshop.Services;

namespace myshop.Controllers
{
    [Route("api/product")]
    public class ProductAPIController : Controller
    {
        private IProductService productService;
        public ProductAPIController(IProductService productService)
        {
            this.productService = productService;
        }
        [HttpGet]
        public IEnumerable<Product> Get()
        {
            return productService.GetAll();
        }
    }
}

Execute sua aplicação com o dotnet run ou teclando F5 no Visual Studio Code. E acesse o endereço com seu navegador: https://localhost:5001/api/product O retorno deverá ser idêntico a este:

Agora que temos o início de uma API, devemos protegê-la para que apenas usuários autenticados tenham acesso a sua chamada. Para isso vamos construir uma nova entidade no pacote Model para representar nosso usuário.

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace myshop.Models
{
    public class User
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }
        public string UserName { get; set; }
        public string UserPassword { get; set; }
    }
}

Para que a aplicação possa ter acesso a este novo objeto vamos incluir também no pacote de service, uma nova interface chamada IUserService para permitir a consulta pelo nome do usuário e salvar os dados de um novo usuário

using myshop.Models;

namespace myshop.Services
{
    public interface IUserService
    {
         User getUserByName(string name);
         User save(User user);
    }
}

Devemos em seguida criar a implementação para esta interface, vamos nos basear na implementação do serviço do produto, mas ficamos com algumas pendências neste código pois a conexão com o MongoDB esta fixa no construtor necessitando ser externalizada para o arquivo de configuração da aplicação no futuro.

using System;
using MongoDB.Driver;
using myshop.Models;

namespace myshop.Services
{
    public class UserService : IUserService
    {
        MongoClient con;
        IMongoDatabase db;
        public UserService()
        {
            con = new MongoClient("mongodb://172.18.0.35:27017");
            //con = new MongoClient("mongodb://localhost:27017");
            db = con.GetDatabase("dbwalter");
            if(db.GetCollection<User>("Users") == null)
                db.CreateCollection("Users");
        }
        public User getUserByName(string name)
        {
            var collection = db.GetCollection<User>("Users");
            return collection.Find<User>(p => p.UserName.ToLower().Contains(name.ToLower())).FirstOrDefault();
        }

        public User save(User user)
        {
            var collection = db.GetCollection<User>("Users");
            if(user.Id == null | user.Id == String.Empty)
                collection.InsertOne(user);
            else{
                var filter = Builders<User>.Filter.Eq("Id", user.Id);
                var updateDefinition = Builders<User>.Update
                        .Set(p => p.UserName, user.UserName)
                        .Set(p => p.UserPassword, user.UserPassword);
                collection.UpdateOne(filter,updateDefinition);
            }
            return user;
        }
    }
}

Agora devemos alterar nossa classe Startup.cs para no método de configuração do serviços, o novo serviço IUserService seja registrado. E em seguida na mesma classe no método de configuração da aplicação, alteramos a assinatura do método para receber a instância do IUserService e incluímos o código para verificar se já existe um usuário admin, caso contrário inserimos um usuário admin no banco de dados.

public void ConfigureServices(IServiceCollection services)
        {

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            
            services.AddScoped<IProductService,ProductService>();
            services.AddScoped<IUserService,UserService>();


        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
            IUserService userService)
        {
            if(userService.getUserByName("admin") == null)
            {
                var adminUser = new Models.User(){UserName="admin", UserPassword="admin"};
                userService.save(adminUser); 
            }
            
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

Agora que temos como identificar no sistema os usuários autorizados para acessá-lo, vamos iniciar efetivamente a construção do código necessário para implementar o método JWT de autenticação. Para isso vamos primeiro criar um diretório dentro de nossa aplicação chamado Security, dentro deste pacote devemos criar duas classes a SigningConfigurations e TokenConfigurations .

A primeira classe que vamos criar se chama TokenConfigurations, seu objetivo é armazenar três propriedades que configuram o processo de criação do Token JWT: Audience esse texto armazena o nome de um grupo de usuários que terão acesso ao token; Issuer esse texto armazena o nome de um grupo de usuários que não devem ter acesso ao token gerado e por fim o parâmetro mais importante Seconds que configura o tempo de validade do token.

namespace myshop.Security
{
    public class TokenConfigurations
    {
        public string Audience { get; set; }
        public string Issuer { get; set; }
        public int Seconds { get; set; }
    }
}

Esses três valores serão carregados pelo arquivo startup com base nos valores que forem informados dentro do arquivo appsettings.json. Por isso abra este arquivo e inclua a nova propriedade TokenConfigurations.

{
  "TokenConfigurations": {
    "Audience": "ExemploAudience",
    "Issuer": "ExemploIssuer",
    "Seconds": 3600
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Retornando para a pasta Security, vamos criar a segunda classe chamada SigningConfigurations. Essa classe faz o papel de Object Factory construindo duas subclasses importantes para a construção do token JWT. Definindo qual algoritmo de HASH será utilizado para geração o token, neste caso o RSA SHA 256.

using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

namespace myshop.Security
{
    public class SigningConfigurations
    {
        public SecurityKey Key { get; }
        public SigningCredentials SigningCredentials { get; }

        public SigningConfigurations()
        {
            using (var provider = new RSACryptoServiceProvider(2048))
            {
                Key = new RsaSecurityKey(provider.ExportParameters(true));
            }

            SigningCredentials = new SigningCredentials(
                Key, SecurityAlgorithms.RsaSha256Signature);
        }
    }
}

Em seguida devemos construir a lógica do endpoint para autenticação e geração do token JWT. Para isso vamos criar uma nova classe controller chamada LoginAPIController nela haverá um único método que receberá uma requisição do tipo POST.

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using myshop.Models;
using myshop.Security;
using myshop.Services;

namespace myshop.Controllers
{
    [Route("api/login")]
    public class LoginAPIController : Controller
    {
        [AllowAnonymous]
        [HttpPost]
        public object Post(
            [FromBody] User user,
            [FromServices]IUserService usrService,
            [FromServices]SigningConfigurations signingConfigurations,
            [FromServices]TokenConfigurations tokenConfigurations)
        {
            bool validcredentials = false;
            if (user != null && !String.IsNullOrWhiteSpace(user.UserName))
            {
                var userBase = usrService.getUserByName(user.UserName);
                validcredentials = (userBase != null &&
                    user.UserName == userBase.UserName &&
                    user.UserPassword == userBase.UserPassword);
            }

            if (validcredentials)
            {
                ClaimsIdentity identity = new ClaimsIdentity(
                    new GenericIdentity(user.UserName, "Login"),
                    new[] {
                        new Claim(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
                        new Claim(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.UniqueName, user.UserName)
                    }
                );

                DateTime creationDate = DateTime.Now;
                DateTime expirationDate = creationDate +
                    TimeSpan.FromSeconds(tokenConfigurations.Seconds);

                var handler = new JwtSecurityTokenHandler();
                var securityToken = handler.CreateToken(new SecurityTokenDescriptor
                {
                    Issuer = tokenConfigurations.Issuer,
                    Audience = tokenConfigurations.Audience,
                    SigningCredentials = signingConfigurations.SigningCredentials,
                    Subject = identity,
                    NotBefore = creationDate,
                    Expires = expirationDate
                });
                var token = handler.WriteToken(securityToken);

                return new
                {
                    authenticated = true,
                    created = creationDate.ToString("yyyy-MM-dd HH:mm:ss"),
                    expiration = expirationDate.ToString("yyyy-MM-dd HH:mm:ss"),
                    accessToken = token,
                    message = "OK"
                };
            }
            else
            {
                return new
                {
                    authenticated = false,
                    message = "Falha ao autenticar"
                };
            }
        }
    }
}

O decorator AllowAnonymous permite que qualquer usuário não autenticado faça a chamada do endpoint. Este método receberá uma instância da classe User contendo o usuário e a senha do cliente que deseja se autenticar. A instância do serviço IUserService para verificar a existência do usuário no sistema e se a senha esta correta, e a instância das classes SigningConfigurations e TokenConfigurations. Logo no início do método o código verifica se o usuário existe no banco de dados e se a senha enviada na requisição é a mesma armazenada no banco. IMPORTANTE: este é um segundo ponto de fragilidade do código pois a senha não deveria ter sido armazenada de forma aberta no banco de dados. Precisamos no futuro retornar para reescrever esse código para que o hash da senha seja armazenado no banco.

Caso o usuário e senha estejam corretos, o código cria uma instância da classe ClaimsIdentity, que representa a “afirmação” de que esse usuário é valido, portanto carrega na classe as informações de identificação para geração do token JWT. Em seguida o código calcula o tempo de criação e de expiração do token e chama da classe JwtSecurityTokenHandler o método CreateToken para gerar o token JWT. Esse token então é convertido para String e retornado para o usuário que fez a requisição.

Agora que já temos um endpoint para gerar o token precisamos configurar nossa aplicação para que todas as requisições sejam interceptadas e verifiquem a existência e validade do token JWT. Para isso devemos novamente alterar a classe Startup.cs para incluir o seguinte código o método ConfigureServices:

public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            var signingConfigurations = new SigningConfigurations();
            services.AddSingleton(signingConfigurations);

            var tokenConfigurations = new TokenConfigurations();
            new ConfigureFromConfigurationOptions<TokenConfigurations>(
                Configuration.GetSection("TokenConfigurations"))
                    .Configure(tokenConfigurations);
            services.AddSingleton(tokenConfigurations);

            services.AddAuthentication(authOptions =>
            {
                authOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                authOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(bearerOptions =>
            {
                var paramsValidation = bearerOptions.TokenValidationParameters;
                paramsValidation.IssuerSigningKey = signingConfigurations.Key;
                paramsValidation.ValidAudience = tokenConfigurations.Audience;
                paramsValidation.ValidIssuer = tokenConfigurations.Issuer;

                // Valida a assinatura de um token recebido
                paramsValidation.ValidateIssuerSigningKey = true;

                // Verifica se um token recebido ainda é válido
                paramsValidation.ValidateLifetime = true;

                // Tempo de tolerância para a expiração de um token (utilizado
                // caso haja problemas de sincronismo de horário entre diferentes
                // computadores envolvidos no processo de comunicação)
                paramsValidation.ClockSkew = TimeSpan.Zero;
            });
            services.AddAuthorization(auth =>
            {
                auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                    .RequireAuthenticatedUser().Build());
            });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            
            services.AddScoped<IProductService,ProductService>();
            services.AddScoped<IUserService,UserService>();


        }

O primeiro código do método desliga o envio de cookies para autenticação do usuários. Em seguida a classe SigningConfigurations é instanciada e adicionada na aplicação no formato de um Singleton para que uma unica referência do objeto exista na aplicação. Em seguida a classe TokenConfigurations é instanciada e os dados existentes no arquivo appsettings.json são dinamicamente carregados para ela, sendo que ela também é adicionada na aplicação no formato de Singleton.

Agora chegamos a parte importante do código, ao chamar o método AddAuthentication definimos que a autenticação será realizada pelo método JWT Bearer (portador do token JWT). Em seguida passamos para o método AddJwtBearer uma função anônima que define as configurações do Token, e as validações que ele deve passar para ser considerado válido.

Então definimos a regra de autorização pelo método AddAuthorization, onde definimos que apenas portadores de tokens válidos podem ter acesso a aplicação.

Como último passo devemos decorar nossa classe ProductAPIController com o [Authorize(“Bearer”)], assim apenas portadores do token poderão acessar a aplicação.

[Authorize("Bearer")]

Salve o código e execute sua aplicação novamente, caso voce tente acessar o endpoint da API de produtos receberá o erro 401 de não autorizado.

Precisamos agora fazer a autenticação na aplicação para depois ter acesso a API, para isso vamos utilizar o Postman. Crie uma chamada do tipo POST para o endereço https://localhost:5001/api/login Selecione a opção Body -> raw -> JSON. E cole o exemplo do objeto json contendo o login e a senha. Ao clicar em SEND você deverá receber um retorno status 200 contendo no corpo do response o Token JWT.


{
	"UserName": "admin",
	"UserPassword": "admin"
}

Agora crie uma nova chamada do tipo GET para o endereço https://localhost:5001/api/product clique na opção Authorization, selecione Bearer Token e cole o token apresentado na outra chamada da API. Ao clicar em Send você receberá os dados dos produtos cadastrados no sistema.

Importante agradecer novamente ao Microsoft MVP Renato Groff que construiu esse tutorial.