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


Neste artigo veremos como trabalhar com claims usando a ASP .NET Core Identity.

Eu já publiquei diversos artigos abordando o ASP .NET Core Identity e hoje vou focar na utilização do recurso claims.

O que são e como atuam as Claims

As claims ou declarações são um par nome-valor emitido por uma parte confiável. Por exemplo, a carteira de habilitação de uma pessoa é emitida por uma autoridade de habilitação. Se a 'Data de Nascimento' na carteira de habilitação for 21 de março de 1995. Nesse caso, o nome da claim seria 'Data de Nascimento', o valor da claim seria 21 de março de 1995 e o emissor seria a autoridade da carteira de habilitação.

A autorização baseada em claims ou declarações,  verifica o valor de uma claim/declaração e permite o acesso a um recurso com base no valor e na política de autorização definida, sendo que uma identidade pode conter várias claims/declarações com vários valores e pode conter várias claims/declarações do mesmo tipo.

As verificações de autorização baseadas em claims são declarativas - o desenvolvedor as incorpora em seu código, contra um controlador ou uma Action em um controlador, especificando as claims que o usuário atual deve possuir e, opcionalmente, o valor que a claim deve conter para acessar o recurso solicitado.

O tipo mais simples de política baseada em claim é aquela que apenas procura a presença de uma claim e não verifica o valor.

Assim, a primeira coisa a fazer, é criar e registrar a política baseada em claims e isso pode ocorrer como parte da configuração do serviço de autorização, que normalmente faz parte do método  ConfigureServices() no arquivo Startup.cs.

Exemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("SomenteFuncionario", policy => policy.RequireClaim("FuncionarioNumero"));
    });
}

Nesse caso, a política SomenteFuncionario verifica a presença de uma claim ou declaração FuncionarioNumero na identidade atual. Note que aqui não estamos verificando o valor.

Em seguida, você aplica a política usando a propriedade Policy no atributo Authorize para especificar o nome da política;

[Authorize (Policy = "SomenteFuncionario")]
public IActionResult SaldoFolgasMensais()
{
     return View ();
}

O atributo 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 Desenvolvedor 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.

Em outro artigo veremos como definir políticas para as claims.

Pegue o projeto aqui:   AspnIdentityClaims.zip

"Porque Deus não nos destinou para a ira, mas para a aquisição da salvação, por nosso Senhor Jesus Cristo,
Que morreu por nós, para que, quer vigiemos, quer durmamos, vivamos juntamente com ele."
1 Tessalonicenses 5:9,10

Referências:


José Carlos Macoratti