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.