ASP .NET Core Identity -  Como trabalhar com Claims - II


Neste artigo vamos continuar a trabalhar com claims usando a ASP .NET Core Identity.

Continuando a primeira parte do artigo vamos agora criar as políticas para as claims e usar essas políticas na autorização.

Criando uma Policy ou Política

Da mesma forma que na autenticação de usuários com base em funções de identidade, podemos fazer autenticação com base em claims ou declarações. Mas lembre-se de que a autenticação de claims requer policies ou políticas, e, essas políticas são criadas no arquivo Startup.cs.

Vamos abrir o arquivo Startup.cs e no método ConfigureServices criar uma policy incluindo o código a seguir:

public void ConfigureServices(IServiceCollection services)
{
            ...

            services.AddAuthorization(opts => {
                opts.AddPolicy("AspManager", policy => {
                    policy.
RequireRole("Gerente");
                    policy.
RequireClaim("Desenvolvedor", "ASP.NET Core");
                });
            });

            ...
}

Observe que criamos uma policy definida com o nome de AspManager.

No código usamos o método AddPolicy para criar a política ou policy e dentro desse método, usamos os métodos RequireRole e RequireClaim para criar os requisitos da política.

A nossa policy ou política foi criada contendo 2 requisitos:

  1. O usuário deve estar na role Gerente;
  2. O usuário deve ter o tipo de claim definido como 'Desenvolvedor' e com o valor 'ASP.NET Core';

Com essa definição pronta vamos incluir um novo método Action chamado Project no controlador ClaimsController, incluindo o atributo [Authorize] neste método que afirma que o valor da política deve ser AspManager : [Authorize (Policy = "AspManager")]

    [Authorize]
    public class ClaimsController : Controller
    {
        private UserManager<AppUser> userManager;
        public ClaimsController(UserManager<AppUser> userMgr)
        {
            userManager = userMgr;
        }
        public ViewResult Index() => View(User?.Claims);
        [Authorize(Policy = "AspManager")]
        public ViewResult Project() => View("Index", User?.Claims);
                 ....

      }

Agora, o método Action Project só pode ser invocado por usuários que estão na função de gerente e têm uma claim ou declaração chamada 'Desenvolvedor' com o valor 'ASP.NET Core'.

Temos ainda outros métodos importantes para criar uma Policy. A tabela abaixo mostra os principais :

Nome Descrição
RequireUserName(name) Especifica o usuário que é necessário
RequireClaim(type, value) Especifica que o usuário possui uma claim do tipo especificado e com um dos valores do intervalo. Os valores da declaração podem ser expressos como argumentos separados por vírgula ou como um IEnumerable.
RequireRole(roles) Especifica que o usuário deve ser membro de uma role especificada. Várias roles podem ser especificadas como argumentos separados por vírgula ou como um IEnumerable.
AddRequirements(requirement) Especifica um requisito personalizado para a política.

Authorize pode ser aplicado a um controlador e neste caso, apenas as identidades que correspondem à política terão acesso permitido a qualquer método Action no controlador.

Entendendo Claims

Para mostrar como as claims atuam e como funcionam eu vou criar um projeto ASP .NET Core MVC com autenticação individual onde vou implementar o autorização inicial baseada no email e senha do usuário. Esse é o padrão quando você criar um projeto usando o template padrão do Visual Studio 2019.

Vamos criar o projeto chamado AspnIdentityClaims usando o template ASP.NET Core Web Application usando o .NET Core 5.0 e configurar o projeto MVC.

Desta forma, para focar apenas nas claims vou partir do projeto já criado e com a autorização para o usuário com senha e email já implementada.

Vamos apresentar o projeto antes de prosseguir iniciando com o arquivo de projeto .csproj :

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

No arquivo de projeto vemos que estamos no ambiente do .NET 5.0 e os pacotes usados para configurar o Identity.

A seguir temos a estrutura do projeto :

Vamos implementar a autenticação baseada em claims ou declarações que na sua forma mais simples, verifica o valor de uma declaração e permite o acesso a um recurso com base nesse valor.

Gerenciando Claims

Vamos criar um novo controlador na pasta Controllers com o nome ClaimsController e alterar o seu método Action Index para retornar User.Claims conforme mostra o código a seguir:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace AspnIdentityClaims.Controllers
{
    [Authorize]
    public class ClaimsController : Controller
    {
        public ViewResult Index() => View(User?.Claims);
    }
}

Aqui, a propriedade User (também disponível como a propriedade HttpContext.User) retorna um objeto ClaimsPrincipal do usuário conectado.

Estamos assim obtendo todas as claims do usuário (como um objeto IEnumerable) usando a propriedade Claims do objeto ClaimsPrincipal, que estamos retornando para a View como um modelo.

Vamos criar a view Index.cshtml na pasta Views/Claims para exibir os dados das claims : 

@model IEnumerable<System.Security.Claims.Claim>

<h2 class="bg-primary m-1 p-1 text-white">Claims</h2>

<table class="table table-sm table-bordered">
    <tr>
        <th>Subject</th>
        <th>Issuer</th>
        <th>Type</th>
        <th>Value</th>
    </tr>

    @foreach (var claim in Model.OrderBy(x => x.Type))
    {
        <tr>
            <td>@claim.Subject.Name</td>
            <td>@claim.Issuer</td>
            <td>@claim.Type</td>
            <td>@claim.Value</td>

        </tr>
    }

</table>

Neste código a view vai exibir todas as claims ou declarações do usuário conectado em uma tabela HTML.

Para poder exibir o link que acessa esta view vamos alterar o arquivo _Layout.cshtml incluindo esta opção :

...
  <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
      <ul class="navbar-nav flex-grow-1">
           <li class="nav-item">
              <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
           </li>
            <li class="nav-item">
               <a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="Index">Admin</a>
            </li>
            <li class="nav-item">
                  <a class="nav-link text-dark" asp-area="" asp-controller="Claims" asp-action="Index">Claims</a>
            </li>

            </ul>
           <partial name="_LoginPartial" />
    </div>
...

Executando o projeto e fazendo o login poderemos acessar e exibir as claims do usuário logado conforme abaixo:

Vamos agora implementar a criação e a exclusão de claims iniciando com a criação de claims.

Vamos abrir o controlador ClaimsController e definir os métodos Action Create para GET e POST, para isso vamos ter que injetar uma instância da classe UserManager<AppUser> no construtor do controlador:

using AspnIdentityClaims.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using System.Threading.Tasks;
namespace AspnIdentityClaims.Controllers
{
    [Authorize]
    public class ClaimsController : Controller
    {
        private UserManager<AppUser> userManager;
        public ClaimsController(UserManager<AppUser> userMgr)
        {
            userManager = userMgr;
        }
        public ViewResult Index() => View(User?.Claims);
        public ViewResult Create() => View();
        [HttpPost]
        [ActionName("Create")]
        public async Task<IActionResult> Create_Post(string claimType, string claimValue)
        {
            AppUser user = await userManager.GetUserAsync(HttpContext.User);

            Claim claim = new Claim(claimType, claimValue, ClaimValueTypes.String);

            IdentityResult result = await userManager.AddClaimAsync(user, claim);
            if (result.Succeeded)
                return RedirectToAction("Index");
            else
                Errors(result);
            return View();
        }

        void Errors(IdentityResult result)
        {
            foreach (IdentityError error in result.Errors)
                ModelState.AddModelError("", error.Description);
        }
    }
}

No método Action Create, primeiro obtemos o usuário atual conectado no método userManager.GetUserAsync().

Em seguida, adicionamos um novo objeto claim : new Claim(claimType, claimValue, ClaimValueTypes.String);

Por fim, incluímos esse objeto claim para criar uma nova claim para meu usuário. A declaração é criada para um usuário usando o método - userManager.AddClaimAsync()

A seguir vamos criar a view Create.cshtml na pasta Views/Claims :

<h1 class="bg-info text-white">Create Claim</h1>
<a asp-action="Index" class="btn btn-secondary">Back</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post">
    <div class="form-group">
        <label for="ClaimType">Claim Type:</label>
        <input name="ClaimType" class="form-control" />
    </div>
    <div class="form-group">
        <label for="ClaimValue">Claim Value:</label>
        <input name="ClaimValue" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Create</button>
</form>

Neste código temos 2 controles input que permitirão adicionar o tipo de claim e o seu  valor para um usuário.

A seguir vamos atualizar a View Index.cshtml do do método Index do controlador ClaimsController incluindo um link para invocar a view Create e um formulário que contém um botão para excluir uma claim existente.

@model IEnumerable<System.Security.Claims.Claim>
<h2 class="bg-primary m-1 p-1 text-white">Claims</h2>
<a asp-action="Create" class="btn btn-secondary">Create a Claim</a>
<table class="table table-sm table-bordered">
    <tr>
        <th>Subject</th>
        <th>Issuer</th>
        <th>Type</th>
        <th>Value</th>
    </tr>
    @foreach (var claim in Model.OrderBy(x => x.Type))
    {
        <tr>
            <td>@claim.Subject.Name</td>
            <td>@claim.Issuer</td>
            <td>@claim.Type</td>
            <td>@claim.Value</td>
            <td>
                <form asp-action="Delete" method="post">
                    <input type="hidden" name="claimValues" value="@claim.Type;@claim.Value;@claim.Issuer" />
                    <button type="submit" class="btn btn-sm btn-danger"
                       onclick="return confirm('Are you sure you want to delete this?')">
                        Delete
                    </button>
                </form>
            </td>
        </tr>
    }
</table>

A seguir vamos completar o código do controlador ClaimsController incluindo o método Action Delete:

...
        [HttpPost]
        public async Task<IActionResult> Delete(string claimValues)

        {
            AppUser user = await userManager.GetUserAsync(HttpContext.User);
            string[] claimValuesArray = claimValues.Split(";");

            string claimType = claimValuesArray[0], 
                claimValue = claimValuesArray[1], claimIssuer = claimValuesArray[2];
            Claim claim = User.Claims.Where(x => x.Type == claimType
                                  && x.Value == claimValue && x.Issuer == claimIssuer)
                                  .FirstOrDefault();
            IdentityResult result = await userManager.RemoveClaimAsync(user, claim);
            if (result.Succeeded)
                return RedirectToAction("Index");
            else
                Errors(result);
            return View("Index");
        }

...

O método Delete obtemos os valores da claim, que são separados por ponto e vírgula (;), em seu parâmetro.

Em seguida, com o método de split, extraiamos o tipo da claim, o seu valor e os valores do emissor da claim;

As claims selecionadas são localizadas usando a consulta LINQ:

Claim claim = User.Claims.Where(x => x.Type == claimType && x.Value == claimValue 
                        && x.Issuer == claimIssuer).FirstOrDefault();

A exclusão da claim do usuário é feita usando o método userManager.RemoveClaimAsync().

Vamos testar esses recursos, executando o projeto e fazendo o login com o usuário macoratti.

A seguir vamos criar uma claim definindo o tipo da claim como e o valor como ASP.NET Core.

Lembre-se de que, uma vez que uma claim é criada, você precisa fazer o login mais uma vez, a fim de ver a sua claim criada. Então, faça login na conta mais uma vez.

Em seguida acione o link para exibir as claims e você verá a claim criada:



Agora exclua sua claim recém-adicionada clicando no botão Delete ao lado dela.

Depois que a reivindicação for excluída, você precisará fazer login novamente e, em seguida, acessar o link das claims novamente:

Para poder realizar a autenticação dos usuários usando as claims precisamos definir as políticas.

Na próxima parte do artigo veremos como definir as políticas para as claims.

Pegue o projeto aqui:   AspnIdentityClaims.zip

"Então os justos resplandecerão como o sol, no reino de seu Pai. Quem tem ouvidos para ouvir, ouça."
Mateus 13:43

Referências:


José Carlos Macoratti