ASP .NET Core - Segurança com DataProtetorTokenProvider


 Hoje veremos como cifrar parâmetros em uma URL usando o recurso DataProteterTokenProvider.

Atualmente,  a segurança das aplicações na internet e dos dados se tornou uma grande preocupação para a maioria das empresas.

Apesar disso não é difícil você encontrar aplicações web que usam URLs para passar parâmetros que serão usados para identificar usuários e/ou realizar alguma tarefa que exige uma segurança maior.

Exemplo de URL com parâmetro usado em uma aplicação web ingênua:

https://localhost:44347/Customers/Edit/1

É evidente que tal informação poderá ser hackeada por pessoas mal intencionadas causando prejuízos enormes dependendo do tipo de aplicação.

Será que não podemos fazer nada para contornar esse problema ?

Será que não podemos criptografar ou cifrar o valor do parâmetro em um formato não legível e ainda sim usá-lo no formato decifrado internamente para realizar a operação ?

Sim podemos fazer isso, e, nosso objetivo será exibir a URL cifrada de forma a proteger a URL:

https://localhost:44347/Customers/Edit/CfDJ8FCRQuiQpFtEur77mE5kVpwvgzvO1HQEIAeDGkr5I9NG1...

Para isso vamos usar os recursos da classe DataProtectorTokenProvider que fornece proteção e validação de tokens de identidade e também é responsável por criptografar e descriptografar esses tokens.

Este seria um sistema de proteção de dados embutido e disponível por padrão na plataforma .NET.

Esse sistema de proteção de dados é construído usando um provedor de proteção de dados (representado pela interface IDataProtectionProvider), que é usado para criar um protetor de dados (representado pela interface IDataProtector).

O protetor de dados é usado para criptografar e descriptografar dados. Como o sistema de proteção de dados foi adicionado à coleção de serviços do aplicativo por padrão, ele pode ser disponibilizado por meio de injeção de dependência.

Vamos usar os métodos Protect() e Unprotect() da interface IDataProtector para criptografar e descriptografar, respectivamente.

Nota: Você pode ver o código fonte da classe DataProtectorTokenProvider neste link: https://github.com/aspnet/Identity/blob/release/2.2/src/Identity/DataProtectionTokenProvider.cs

A seguir eu vou mostrar como cifrar e decifrar o parâmetro em uma URL em uma aplicação ASP .NET Core MVC que faz o CRUD de Clientes ou Customers.

Para simplificar vou partir da aplicação MVC pronta que gerenciar informações de Customers usando o controlador CustomersController e que esta funcionando e expondo o id do Customer na Url. Para evitar isso vou aplicar os recursos da classe DataProtectorTokenProvider.

Criando e registrando a classe para cifrar e decifrar strings

No projeto MVC crie uma pasta Security e nesta pasta crie a classe DataProtectionStrings :

 public class DataProtectionStrings
 {
        public readonly string CustomerIdValue = "CustomerIdValue";
 }

Esta classe contém chave string exigida para criptografia e descriptografia. No momento, temos apenas uma string definida com o nome de 'CustomerIdValue'.

Agora vamos registrar o serviço para esta classe no método ConfigureServices da classe Startup:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<DataProtectionStrings>();
            services.AddDbContext<AppDbContext>(options =>
              options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddControllersWithViews();
        }

Com isso agora poderemos injetar esta classe no controlador CustomersController do projeto MVC.

Ajustando o modelo de domínio Customer

Em nossa implementação vamos cifrar e decifrar o Id do Customer, e, para isso precisamos armazenar o valor cifrado em uma propriedade que iremos criar na tabela Customer com o nome de EncryptedId:

    public class Customer
    {
        public int Id { get; set; }
        [NotMapped]
        public string EncryptedId { get; set; }
        
        public string Name { get; set; }
        public string Email { get; set; }
    }

Criamos a propriedade EncryptedId para conter o Id do cliente criptografado. O atributo NotMapped especifica que esta propriedade deve ser excluída do mapeamento para uma coluna da tabela do banco de dados pois não queremos armazenar essas informações na tabela do banco de dados.

O atributo NotMapped está no namespace System.ComponentModel.DataAnnotations.Schema.

Ajustando o controlador CustomersController

Agora vamos usar os recursos da classe  DataProtectorTokenProvider onde aplicaremos os métodos Protect e Unprotect da interface IDataProtector.

Para isso vamos injetar uma instância de IDataProtector no construtor da classe e a seguir vamos usar o valor da propriedade  CustomerIdValue definida na classe DataProtectionStrings.

Vamos abrir o controlador CustomersController e alterar o código conforme abaixo:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Mvc_UrlProtected.Models;
using Mvc_UrlProtected.Security;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Mvc_UrlProtected.Controllers
{
    public class CustomersController : Controller
    {
        private readonly AppDbContext _context;
        private readonly IDataProtector protector;

        public CustomersController(AppDbContext context,
            IDataProtectionProvider dataProtectionProvider,
            DataProtectionStrings dataProtectionStrings

            )
        {
            _context = context;

            this.protector = dataProtectionProvider.CreateProtector(
            dataProtectionStrings.CustomerIdValue);
        }

        public async Task<IActionResult> Index()
        {
            var resultado = await _context.Customers.ToListAsync();

            return View(resultado.Select(e =>
           {
              
// Cifra o valor do Id e armazena na propriedade EncryptedId
               e.EncryptedId = protector.Protect(e.Id.ToString());
               return e;
           }));

        }

        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            int decryptedIntId = DecryptUrlValue(id);

            var customer = await _context.Customers
                .FirstOrDefaultAsync(m => m.Id == decryptedIntId);

            if (customer == null)
            {
                return NotFound();
            }

            return View(customer);
        }

        private int DecryptUrlValue(string id)
        {
            string decryptedId = protector.Unprotect(id);
            int decryptedIntId = Convert.ToInt32(decryptedId);
            return decryptedIntId;
        }

        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Id,Name,Email")] Customer customer)
        {
            if (ModelState.IsValid)
            {
                _context.Add(customer);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(customer);
        }

        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            int decryptedIntId = DecryptUrlValue(id);

            var customer = await _context.Customers.FindAsync(decryptedIntId);

            if (customer == null)
            {
                return NotFound();
            }
            return View(customer);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Email")] Customer customer)
        {
            if (id != customer.Id)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(customer);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!CustomerExists(customer.Id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(customer);
        }

        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            int decryptedIntId = DecryptUrlValue(id);

            var customer = await _context.Customers
                .FirstOrDefaultAsync(m => m.Id == decryptedIntId);

            if (customer == null)
            {
                return NotFound();
            }

            return View(customer);
        }

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]

        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var customer = await _context.Customers.FindAsync(id);
            _context.Customers.Remove(customer);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool CustomerExists(int id)
        {
            return _context.Customers.Any(e => e.Id == id);
        }
    }
}

Neste controlador fizemos as seguintes alterações:

1- No construtor injetamos o serviço do IDataProtector e informamos a string a ser usada para cifrar e decifrar o Id do Customer:

        private readonly AppDbContext _context;
        private readonly IDataProtector protector;

        public CustomersController(AppDbContext context,
            IDataProtectionProvider dataProtectionProvider,
            DataProtectionStrings dataProtectionStrings

            )
        {
            _context = context;

            this.protector = dataProtectionProvider.CreateProtector(
            dataProtectionStrings.CustomerIdValue);
        }

 

2- No método Index ciframos o valor do Id e armazenamos na propriedade EncryptedId:

        public async Task<IActionResult> Index()
        {
            var resultado = await _context.Customers.ToListAsync();

            return View(resultado.Select(e =>
           {
              
// Cifra o valor do Id e armazena na propriedade EncryptedId
               e.EncryptedId = protector.Protect(e.Id.ToString());
               return e;
           }));

        }

3- Criamos o método DecriptUrlValue(string id) que recebe o Id cifrado e decifra o Id e faz a conversão para int.

        private int DecryptUrlValue(string id)
        {
            string decryptedId = protector.Unprotect(id);
            int decryptedIntId = Convert.ToInt32(decryptedId);
            return decryptedIntId;
        }

4- Alteramos os métodos Action Edit,  Details e Delete  onde estamos usando o método DecriptUrlValue a alterando a consulta para localizar o customer usando o valor decifrado do id:

1- Edit

    int decryptedIntId = DecryptUrlValue(id);
    
var customer = await _context.Customers.FindAsync(decryptedIntId);

2- Details

     int decryptedIntId = DecryptUrlValue(id);
     var customer = await _context.Customers.FirstOrDefaultAsync(m => m.Id == decryptedIntId);

3- Delete

     int decryptedIntId = DecryptUrlValue(id);
   
  var customer = await _context.Customers.FirstOrDefaultAsync(m => m.Id == decryptedIntId);

E na View Index do controlador CustomersController vamos alterar o código para atribuir o id cifrado à rota usada na aplicação:

@model IEnumerable<Mvc_UrlProtected.Models.Customer>
<h1>Index</h1>
<p>
  <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Email)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.EncryptedId">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.
EncryptedId">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.
EncryptedId">Delete</a>

            </td>
        </tr>
}
    </tbody>
</table>

Vamos precisar ajustar também a view Details para obter o parâmetro referente ao Id a partir da url usando o código abaixo:

 <a asp-action="Edit" asp-route-id="@ViewContext.RouteData.Values["id"]">Edit</a>

Os demais ajustes são simples e estão no código do projeto.

Agora é só alegria....

Executando o projeto iremos obter o seguinte resultado:

Pegue o projeto aqui : Mvc_UrlProtected.zip  (sem as referências)

"Melhor é o que tarda em irar-se do que o poderoso, e o que controla o seu ânimo do que aquele que toma uma cidade."
Provérbios 16:32

Referências:


José Carlos Macoratti