Cutomizando Claims no ASP.NET

Claims customizados no ASP.NET permitem guardar informações extras sobre o usuário autenticado, além do nome e dos perfis de acesso. Neste artigo, você vai aprender a criar esses valores, ler cada um deles depois e encapsular a leitura em métodos de extensão reutilizáveis.

O conteúdo serve para APIs com JWT, aplicações MVC com cookies, Minimal APIs e também Blazor. Afinal, todos esses modelos compartilham a mesma base: o ClaimsPrincipal do namespace System.Security.Claims.

O que são claims

A palavra claim pode ser traduzida como afirmação. Portanto, no .NET, um claim nada mais é do que uma composição de chave e valor que afirma algo sobre alguém.

Por exemplo, um claim pode afirmar que o e-mail do usuário é xyz@balta.io ou que ele pertence ao perfil student. A partir dessas afirmações, sua aplicação ou API pode tomar decisões de autorização da forma que você desejar.

A documentação oficial de autorização baseada em claims resume bem a ideia. Um claim representa o que o sujeito é, e não o que ele pode fazer.

Em outras palavras, o claim descreve o usuário, enquanto as regras de autorização decidem o que essa descrição libera. Essa separação é importante quando você começa a criar valores próprios.

Claim, ClaimsIdentity e ClaimsPrincipal

Três classes formam a base desse modelo. A primeira é a Claim, que guarda o tipo (Type), o valor (Value) e alguns metadados, como o emissor.

Em seguida vem a ClaimsIdentity, que agrupa vários claims e representa uma identidade do usuário. Por fim, o ClaimsPrincipal reúne uma ou mais identidades e é o objeto que você acessa pela propriedade User.

Na prática, quase toda aplicação trabalha com uma única identidade por usuário. Mesmo assim, vale saber que a propriedade Claims do principal devolve os claims de todas as identidades associadas a ele.

Claims padrões do .NET

O .NET também traz alguns claims padrões dentro da classe estática ClaimTypes. Ela fornece, entre outros, o ClaimTypes.Name, que serve como base para obter o nome do usuário.

// Claim padrão com o nome do usuário
new Claim(ClaimTypes.Name, "xyz@balta.io");

Cada item do ClaimTypes é, na verdade, uma constante do tipo string com uma URI longa. Por isso, usar a constante evita erros de digitação e mantém o código consistente entre os projetos.

Além do nome, existem tipos como ClaimTypes.Email, ClaimTypes.NameIdentifier, ClaimTypes.Role e ClaimTypes.DateOfBirth. Contudo, nenhum deles cobre as regras específicas do seu negócio.

ClaimTypes.Name e ClaimTypes.Role

Dois claims em específico são extremamente úteis no ciclo de vida de qualquer aplicação ASP.NET ou Blazor. Eles são o ClaimTypes.Name e o ClaimTypes.Role.

Sempre que você atribui o ClaimTypes.Name, ganha acesso instantâneo ao valor pela propriedade User.Identity.Name.

// Na geração da identidade
new Claim(ClaimTypes.Name, "andre");

// No controller
public IActionResult GetAsync()
{
    var name = User.Identity?.Name; // andre
    return Ok(name);
}

O mesmo vale para o ClaimTypes.Role, que você pode atribuir várias vezes. Depois, basta verificar o perfil pelo método User.IsInRole ou pelo atributo Authorize.

// Na geração da identidade
new Claim(ClaimTypes.Role, "student");
new Claim(ClaimTypes.Role, "premium");

// No controller
[Authorize(Roles = "student")]
public IActionResult GetAsync()
{
    if (User.IsInRole("premium"))
        return Ok("Premium content");

    return Ok("Standard content");
}

Esse comportamento existe porque a ClaimsIdentity possui as propriedades NameClaimType e RoleClaimType. Assim, o Identity.Name procura o claim configurado como nome, enquanto o IsInRole procura os claims configurados como perfil.

Como criar claims customizados

Mas você sabia que pode criar claims customizados, que não estão definidos no ClaimTypes? Isso abre espaço para guardar o identificador do usuário, o plano contratado ou qualquer informação que a aplicação consulte com frequência.

Como um Claim é apenas uma composição de chave e valor, você pode usar qualquer texto literal como chave. Na verdade, o ClaimTypes é apenas um atalho, que previne o uso de um literal inválido.

Quando vale a pena usar claims customizados

Os claims customizados fazem sentido para dados pequenos, estáveis e consultados em quase toda requisição. O identificador do usuário, o plano de assinatura e o tenant em aplicações multi-tenant são bons exemplos.

Por outro lado, dados que mudam com frequência não combinam com claims. Afinal, o valor fica congelado no token até ele expirar, então uma alteração no banco só aparece no próximo login.

Uma boa pergunta para decidir é a seguinte: essa informação ajuda a identificar o usuário ou a decidir o acesso? Se a resposta for sim, ela é candidata a virar claim.

Caso contrário, prefira buscar o dado no banco usando o identificador que já está no token. Dessa forma, você mantém os claims customizados enxutos e sempre confiáveis.

Gerando a identidade com claims customizados

Considere o método que gera os claims de um usuário no momento da autenticação. Esse código é o mesmo usado no artigo sobre autenticação e autorização com JWT e Bearer no ASP.NET 7.

private static ClaimsIdentity GenerateClaims(User user)
{
    var ci = new ClaimsIdentity();
    ci.AddClaim(new Claim(ClaimTypes.Name, user.Email));

    foreach (var role in user.Roles)
        ci.AddClaim(new Claim(ClaimTypes.Role, role));

    return ci;
}

Você pode modificar esse código para adicionar qualquer claim customizado. Para deixar o exemplo bem explícito, vamos usar um claim chamado fruit com o valor banana.

private static ClaimsIdentity GenerateClaims(User user)
{
    var ci = new ClaimsIdentity();
    ci.AddClaim(new Claim(ClaimTypes.Name, user.Email));

    foreach (var role in user.Roles)
        ci.AddClaim(new Claim(ClaimTypes.Role, role));

    // Claim customizado, fora do ClaimTypes
    ci.AddClaim(new Claim("fruit", "banana"));

    return ci;
}

Repare que o método AddClaim recebe sempre uma instância de Claim. Por isso, o claim customizado também é criado com new Claim(tipo, valor), exatamente como os claims padrões.

Cuidados com dados sensíveis

Esse claim também vai para o payload do token quando você trabalha com APIs e JWT. Da mesma forma, ele vai para o cookie de autenticação quando você trabalha com aplicações ASP.NET baseadas em cookies.

No caso do JWT, o payload é apenas codificado em Base64Url, ou seja, qualquer pessoa com o token consegue ler o conteúdo. Se quiser revisar esse formato, o artigo o que é JWT explica cada parte do token.

Portanto, nada de informação sensível nos claims customizados. Senhas, documentos, dados de cartão e informações pessoais delicadas devem ficar no banco de dados, nunca no token.

Além disso, cada claim aumenta o tamanho do token ou do cookie, que trafega em toda requisição. Então, guarde apenas o necessário para identificar o usuário e decidir o acesso.

Boas práticas para nomear claims customizados

Strings soltas espalhadas pelo código são uma fonte comum de bugs. Por exemplo, gravar "Id" em um lugar e ler "id" em outro pode gerar comportamentos difíceis de rastrear.

Uma solução simples é centralizar os nomes em uma classe de constantes, como o próprio .NET faz com o ClaimTypes.

public static class CustomClaimTypes
{
    // Nomes curtos e estáveis, usados na geração e na leitura
    public const string Id = "Id";
    public const string Plan = "plan";
    public const string Fruit = "fruit";
}

Com isso, a geração passa a usar new Claim(CustomClaimTypes.Plan, "premium"). Do mesmo modo, a leitura e as políticas de autorização referenciam a mesma constante, sem risco de divergência.

Como ler claims customizados

Com o claim adicionado, agora você precisa obter essa informação. Isso pode ser feito pela propriedade User.Claims, que contém todos os claims do contexto atual.

// Busca o claim pelo tipo e lê o valor
var fruit = User.Claims.FirstOrDefault(x => x.Type == "fruit")?.Value; // banana

Pronto, agora você tem tanto a criação como a leitura de claims customizados. Assim, consegue armazenar mais informações além do nome do usuário.

Métodos nativos de busca

O ClaimsPrincipal também oferece métodos prontos que deixam a leitura mais expressiva. O FindFirst devolve o primeiro claim de um tipo ou null quando ele não existe.

// Primeiro claim do tipo informado, ou null
var fruit = User.FindFirst("fruit")?.Value;

// Todos os claims de um mesmo tipo
var roles = User.FindAll(ClaimTypes.Role).Select(x => x.Value);

// Verifica tipo e valor de uma só vez
var likesBanana = User.HasClaim("fruit", "banana");

Enquanto o FindAll é útil para claims que se repetem, como os perfis, o HasClaim responde rapidamente se o usuário possui um claim com determinado valor.

Vale lembrar que, segundo a documentação, os valores dos claims são comparados de forma ordinal. Ou seja, Premium e premium são valores diferentes, então mantenha um padrão de escrita.

Extension methods para claims customizados

Aproveitando a oportunidade, esse é um ótimo cenário para criar um extension method que retorna os claims de que você precisa. Assim, você evita possíveis erros na leitura de claims customizados.

O claim pode não existir, ou o valor dele, por ser uma string, pode não ser convertido para o tipo esperado. Então, concentrar essa lógica em um único lugar deixa o restante do código mais limpo.

Abaixo está um exemplo com extensões para ler o Id, o nome e o e-mail do usuário. São três claims usados com frequência em praticamente qualquer aplicação.

using System.Security.Claims;

namespace Balta.Ui.Web.Extensions;

public static class ClaimsPrincipalExtension
{
    public static Guid Id(this ClaimsPrincipal user)
    {
        // TryParse evita exceção quando o claim não existe ou é inválido
        var value = user.FindFirst("Id")?.Value;
        return Guid.TryParse(value, out var id) ? id : Guid.Empty;
    }

    public static string Name(this ClaimsPrincipal user)
        => user.FindFirst(ClaimTypes.Name)?.Value ?? string.Empty;

    public static string Email(this ClaimsPrincipal user)
        => user.FindFirst(ClaimTypes.Email)?.Value ?? string.Empty;
}

No original, cada método usava um bloco try/catch para tratar falhas de conversão. Nesta versão, o Guid.TryParse cumpre o mesmo papel sem lançar exceções, o que deixa o código mais leve.

Para os métodos de texto, o operador ?? já garante uma string vazia quando o claim não existe. Dessa forma, quem chama a extensão nunca precisa lidar com null.

Importando com global using

Para usar as extensões, basta importar o namespace onde você as criou. No meu caso, faço isso com o recurso de global using, disponível a partir do C# 10.

global using Balta.Ui.Web.Extensions;

Com essa linha em um único arquivo do projeto, não há necessidade de importar o namespace em cada arquivo que vai usar as extensões. Afinal, o compilador aplica o using a todo o projeto.

Usando em Razor, MVC e Minimal APIs

A extensão funciona em APIs, MVC, Minimal APIs e até no Blazor. Isso acontece porque todos esses modelos expõem o mesmo ClaimsPrincipal.

// Razor
@User.Id()
@User.Name()
@User.Email()

// MVC
User.Id();
User.Name();
User.Email();

// Minimal APIs
app.MapGet("/me", (ClaimsPrincipal user) => new
{
    Id = user.Id(),
    Name = user.Name(),
    Email = user.Email()
});

Nas Minimal APIs, o framework injeta o ClaimsPrincipal diretamente como parâmetro do handler. Por isso, o exemplo usa user em minúsculo, que é o nome do parâmetro recebido.

Testando a leitura de claims customizados

Outra vantagem das extensões é a facilidade de teste. Como elas dependem só do ClaimsPrincipal, você monta um principal em memória e verifica o resultado, sem subir a aplicação.

[Fact]
public void ShouldReturnEmptyGuidWhenIdClaimIsInvalid()
{
    // Monta um usuário com um Id inválido
    var identity = new ClaimsIdentity(new[] { new Claim("Id", "not-a-guid") }, "Test");
    var user = new ClaimsPrincipal(identity);

    Assert.Equal(Guid.Empty, user.Id());
}

O segundo parâmetro do construtor da ClaimsIdentity informa o tipo de autenticação. Com isso, a propriedade IsAuthenticated passa a retornar true, como aconteceria com um usuário real.

Esse tipo de teste garante que os claims customizados continuam sendo lidos corretamente mesmo depois de uma refatoração. Também documenta o comportamento esperado quando o valor está ausente ou malformado.

Autorização com claims customizados

Ler um claim dentro do método resolve muitos casos. No entanto, o ASP.NET Core oferece uma forma declarativa de exigir claims, por meio de políticas de autorização.

A política mais simples verifica apenas se o claim existe, sem olhar o valor. Também é possível informar uma lista de valores permitidos, como mostra o exemplo abaixo.

builder.Services.AddAuthorizationBuilder()
    // Exige apenas a presença do claim
    .AddPolicy("HasFruit", policy => policy.RequireClaim("fruit"))
    // Exige o claim com um dos valores informados
    .AddPolicy("PremiumOnly", policy => policy.RequireClaim("plan", "premium", "enterprise"));

Depois de registrar as políticas, você as aplica com o atributo Authorize ou com o método RequireAuthorization nas Minimal APIs.

[Authorize(Policy = "PremiumOnly")]
public IActionResult GetPremiumAsync() => Ok("Premium content");

app.MapGet("/premium", () => "Premium content")
   .RequireAuthorization("PremiumOnly");

Para regras mais flexíveis, o método RequireAssertion recebe uma função que avalia o principal. Por exemplo, ele pode exigir que o claim de e-mail termine com um domínio específico.

Se você ainda está organizando os conceitos, o artigo o que é autenticação e autorização explica a diferença entre as duas etapas.

Adicionando claims depois da autenticação

Nem sempre você controla a geração do token. Quando o usuário chega por um provedor externo, por exemplo, os claims já vêm prontos e você não altera o método que os cria.

Nesse caso, a interface IClaimsTransformation permite acrescentar claims ao principal depois da autenticação. Ela exige um único método, o TransformAsync.

public class FruitClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // O método pode rodar várias vezes, então só adiciona se não existir
        if (principal.HasClaim(x => x.Type == "fruit"))
            return Task.FromResult(principal);

        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim("fruit", "banana"));
        principal.AddIdentity(identity);

        return Task.FromResult(principal);
    }
}

// Program.cs
builder.Services.AddTransient<IClaimsTransformation, FruitClaimsTransformation>();

A documentação alerta que esse método pode ser chamado mais de uma vez por requisição. Por isso, o exemplo verifica a existência do claim antes de adicioná-lo.

Como os valores criados aqui ficam apenas no principal da requisição atual, eles não aumentam o token. Em contrapartida, a transformação roda a cada requisição, então evite consultas pesadas dentro dela.

Mapeamento de nomes no JWT

Ao validar tokens, o ASP.NET Core pode mapear alguns nomes curtos para as URIs longas do ClaimTypes. Os claims customizados, como o fruit, costumam chegar com o mesmo nome em que foram gravados.

Caso prefira manter todos os nomes exatamente como vieram do emissor, a opção MapInboundClaims = false desliga esse mapeamento por esquema. Assim, você lê os claims sem surpresas de nomenclatura.

Conclusão

Trabalhar com claims customizados no ASP.NET e no Blazor é uma tarefa relativamente simples. Basta criar um Claim com o tipo e o valor desejados e adicioná-lo à identidade do usuário.

Na leitura, os métodos FindFirst, FindAll e HasClaim resolvem a maior parte dos casos. Além disso, extension methods com o C# deixam o acesso ainda mais seguro e reutilizável.

Também vale centralizar os nomes em constantes e cobrir as extensões com testes de unidade. Com esses cuidados, os claims customizados deixam de ser strings soltas e passam a fazer parte do contrato da sua aplicação.

Por fim, lembre que tudo o que vai no token pode ser lido por quem o possui. Então, mantenha os claims enxutos, sem dados sensíveis, e use políticas de autorização para transformar essas informações em regras de acesso.

FAQ

Posso criar claims com qualquer nome?

Sim, o tipo de um claim é apenas uma string. Mesmo assim, centralize os nomes em constantes para evitar divergências entre geração e leitura.

Claims customizados ficam visíveis no JWT?

Sim, o payload do JWT é apenas codificado em Base64Url. Qualquer pessoa com o token consegue ler os valores, então não guarde dados sensíveis neles.

Qual a diferença entre FindFirst e Claims.FirstOrDefault?

Os dois retornam o primeiro claim de um tipo ou null. O FindFirst é um atalho nativo do ClaimsPrincipal e deixa o código mais legível.

Como exigir um claim para acessar um endpoint?

Registre uma política com RequireClaim e aplique com o atributo Authorize ou com RequireAuthorization. A política pode exigir apenas a presença do claim ou valores específicos.

Quando usar IClaimsTransformation?

Use quando não controla a geração do token, como em provedores externos. A interface adiciona claims ao principal depois da autenticação, a cada requisição.

Glossário

Termo Significado
Claim Par de tipo e valor que afirma algo sobre o usuário autenticado.
ClaimsIdentity Identidade que agrupa um conjunto de claims.
ClaimsPrincipal Objeto que representa o usuário e reúne uma ou mais identidades.
ClaimTypes Classe do .NET com as constantes dos tipos de claim padrões.
JWT JSON Web Token, formato de token com cabeçalho, payload e assinatura.
Payload Parte do JWT que carrega os claims.
Extension method Método estático que se comporta como método de instância de outro tipo.
Global using Diretiva do C# 10 que importa um namespace para todo o projeto.
Política de autorização Conjunto de requisitos que o usuário precisa atender para acessar um recurso.
IClaimsTransformation Interface que permite alterar o principal depois da autenticação.

Fontes