Gerenciamento de chaves JWT no ASP.NET Core
Você configurou JWT Bearer Authentication no ASP.NET Core. Endpoint de login gera o token, `[Authorize]` protege a controller, `AddJwtBearer` valida a assinatura. Funciona. Cola no `Program.cs`, roda, token sai no response. Bonito. Agora me responde: como você gerencia a chave de assinatura?
Se a resposta envolve Encoding.UTF8.GetBytes("minha-chave-secreta"), isso nem é uma chave, é uma senha disfarçada e é digno de um tapa na cara.
Você tem uma chave simétrica. HMAC-SHA256. O mesmo segredo precisa existir em todo lugar que valida o token. Se sua API roda em três instâncias atrás de um load balancer, as três precisam da mesma chave. Se você tem uma API de identidade e três APIs consumidoras, todas compartilham o mesmo segredo. Se esse segredo vaza, tudo está comprometido. E rotação? Troca a chave e todo token emitido antes morre na hora. Sem transição, sem graceful rotation.

Na prática, HMAC funciona pra cenários simples. Uma API, um servidor, um ambiente de dev. Mas quando o sistema cresce — e cresce — você precisa de chaves assimétricas, rotação automática e um jeito de distribuir a chave pública sem expor a privada. É aí que entra o NetDevPack.Security.Jwt.
O que o componente resolve
NetDevPack.Security.Jwt é um gerenciador de chaves criptográficas para JWT no .NET. Ele faz o que você teria que implementar na mão:
- Gera chaves RSA ou ECDsa automaticamente — algoritmos assimétricos, conforme recomendação da RFC 7518
- Rotaciona a chave a cada 90 dias, seguindo as práticas do NIST para rotação de chaves públicas
- Remove chaves privadas antigas após a rotação
- Expõe um endpoint
jwks_uricom a chave pública no formato JWKS — para que outras APIs validem tokens sem precisar da chave privada - Persiste as chaves no mesmo local que o ASP.NET Core DataProtection (ou em banco, ou em filesystem)
Você para de gerenciar chaves e passa a só gerar tokens.
Se sua API roda em Kubernetes, Docker Swarm, ou qualquer cenário com múltiplas instâncias, pense nele como o DataProtection Key do ASP.NET Core, só que para JWT. Se você já configurou DataProtection pra funcionar com Redis ou SQL Server em cluster, sabe a dor. Mesma ideia, aplicada à assinatura de tokens.
Pré-requisitos
- .NET 8+ (o componente suporta a versão mais recente)
- Um projeto ASP.NET Core Web API
- Familiaridade básica com JWT Bearer Authentication (
JwtSecurityTokenHandler,SecurityTokenDescriptor,AddJwtBearer)
Instalação
Dois pacotes. O core e o de AspNetCore:
dotnet add package NetDevPack.Security.Jwt
dotnet add package NetDevPack.Security.Jwt.AspNetCore
O primeiro tem a lógica de gerenciamento de chaves. O segundo adiciona extensions para ASP.NET Core — o endpoint JWKS e a integração com o middleware de validação.
Configuração básica — uma API que emite e valida
Vamos começar pelo cenário mais comum: uma API que gera tokens JWT e ela mesma valida.
Na maioria dos tutoriais, o Program.cs tem uma chave simétrica registrada manualmente, o AddJwtBearer com IssuerSigningKey explícita, e o endpoint de login montando o token na mão. Com o NetDevPack.Security.Jwt, a configuração muda: você não gerencia mais a chave, e a validação usa o JWKS manager.
// Program.cs — .NET 8+
var builder = WebApplication.CreateBuilder(args);
// Configura autenticação JWT Bearer
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"]
};
});
// Registra o gerenciador de chaves JWT e integra validação
builder.Services.AddJwksManager().UseJwtValidation();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Expõe o endpoint JWKS com a chave pública
app.UseJwksDiscovery();
Perceba o que não existe aqui: não tem SymmetricSecurityKey, não tem RandomNumberGenerator.GetBytes(32), não tem IssuerSigningKey no TokenValidationParameters. O AddJwksManager() registra o serviço IJwtService no container de DI. O UseJwtValidation() conecta a validação de tokens ao gerenciador — ele sabe qual chave está ativa. E o UseJwksDiscovery() expõe https://sua-api/jwks com a chave pública no formato JWKS.
Por padrão, o componente gera chaves ECDsa com P-256 e SHA-256. Curva elíptica. Mais seguro que RSA pra tamanhos de chave equivalentes, e mais rápido pra assinar e verificar. Se você precisa de RSA (compatibilidade, por exemplo), muda na configuração — mostro mais pra frente.
Gerando tokens com IJwtService
Agora o endpoint de login. Em vez de criar uma SigningCredentials na mão, você injeta IJwtService e pede a credencial atual:
app.MapPost("/auth/login", async (
LoginRequest request,
IConfiguration config,
IJwtService jwtService) =>
{
// Validação real aqui — banco, Identity, o que for
if (request.Username != "bruno" || request.Password != "senha-segura")
return Results.Unauthorized();
var tokenHandler = new JwtSecurityTokenHandler();
var key = await jwtService.GetCurrentSigningCredentials();
var token = tokenHandler.CreateToken(new SecurityTokenDescriptor
{
Issuer = config["Jwt:Issuer"],
Audience = config["Jwt:Audience"],
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "Admin"),
new Claim("department", "Engineering")
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = key
});
return Results.Ok(new
{
token = tokenHandler.WriteToken(token)
});
});
No código a diferença parece pouca. Por baixo, muda tudo. jwtService.GetCurrentSigningCredentials() retorna a chave ativa no momento — RSA ou ECDsa, gerada automaticamente. Essa chave rotaciona a cada 90 dias. Quando a rotação acontece, tokens antigos continuam válidos porque o endpoint JWKS expõe tanto a chave atual quanto as anteriores (enquanto não expiram). Tokens novos usam a chave nova. Nenhum token válido morre no processo.
Você vai dizer: "mas e o JwtSecurityTokenHandler? Não muda nada?" Não. O handler é o mesmo. O que muda é de onde vem a SigningCredentials. Em vez de criar uma SymmetricSecurityKey na mão e rezar pra ninguém perder a chave, o componente gera, rotaciona e persiste automaticamente — usando algoritmos assimétricos e removendo chaves privadas antigas.
O endpoint JWKS — distribuindo a chave pública
Quando você chama app.UseJwksDiscovery(), a API expõe um endpoint em /jwks (configurável). Esse endpoint retorna algo assim:
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "abc123...",
"crv": "P-256",
"x": "...",
"y": "...",
"alg": "ES256"
}
]
}
Isso é um JSON Web Key Set — a coleção de chaves públicas que qualquer API pode consumir pra validar tokens. A chave privada nunca sai do servidor que emite o token. Quem valida só precisa da pública.
Na prática, a diferença entre compartilhar uma chave simétrica (HMAC-SHA256) e expor um JWKS é a mesma diferença entre dar a chave da sua casa pra todo mundo ou instalar um interfone. Com HMAC, se uma API consumidora é comprometida, a chave de assinatura vaza — e qualquer um pode forjar tokens. Com chaves assimétricas e JWKS, a API consumidora só tem a chave pública. Pode verificar, mas não pode criar. Se for comprometida, o atacante não assina nada.
Múltiplas APIs — o cenário real
Até aqui, uma API que emite e valida. Mas na prática, o cenário mais comum é: uma API de identidade emitindo tokens e várias APIs consumidoras validando.
API de identidade (quem emite o token)
Instala NetDevPack.Security.Jwt.AspNetCore:
// Program.cs da API de Identidade
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddJwksManager();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseJwksDiscovery(); // Expõe /jwks
// Endpoint de login gerando token com IJwtService
app.MapPost("/auth/login", async (
LoginRequest request,
IJwtService jwtService) =>
{
// Validação...
var tokenHandler = new JwtSecurityTokenHandler();
var currentIssuer = "https://identity.suaempresa.com";
var key = await jwtService.GetCurrentSigningCredentials();
var token = tokenHandler.CreateToken(new SecurityTokenDescriptor
{
Issuer = currentIssuer,
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "Admin")
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = key
});
return Results.Ok(new { token = tokenHandler.WriteToken(token) });
});
API consumidora (quem valida o token)
Na API que recebe requests autenticados, instala NetDevPack.Security.JwtExtensions:
dotnet add package NetDevPack.Security.JwtExtensions
// Program.cs da API consumidora
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.IncludeErrorDetails = true;
options.SetJwksOptions(
new JwkOptions("https://identity.suaempresa.com/jwks"));
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/dados", () => "dados protegidos")
.RequireAuthorization();
O SetJwksOptions faz o trabalho pesado: a API consumidora busca as chaves públicas do endpoint JWKS da API de identidade, cacheia por 10 minutos, e usa pra validar os tokens. Você não copia chave nenhuma entre projetos e não precisa de variável de ambiente com segredo compartilhado.
Quando a API de identidade rotaciona a chave, o JWKS atualiza. Na próxima vez que o cache expira, a API consumidora pega a chave nova. Você nem percebe.
Persistência — onde as chaves ficam
Por padrão, NetDevPack.Security.Jwt guarda as chaves no mesmo lugar que o ASP.NET Core DataProtection — o IXmlRepository. Se você já configurou DataProtection pra um cenário distribuído (Redis, Azure Blob Storage, SQL Server), as chaves JWT seguem o mesmo caminho.
Mas se você precisa de controle mais direto, tem duas opções extras:
Banco de dados com Entity Framework Core
dotnet add package NetDevPack.Security.Jwt.Store.EntityFrameworkCore
// Seu DbContext
public class AppDbContext : DbContext, ISecurityKeyContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<SecurityKeyWithPrivate> SecurityKeys { get; set; }
}
// No Program.cs
builder.Services.AddJwksManager()
.PersistKeysToDatabaseStore<AppDbContext>();
As chaves vão pra uma tabela no banco. Todas as instâncias da API leem a mesma chave. Rode o migration e pronto.
Filesystem
dotnet add package NetDevPack.Security.Jwt.Store.FileSystem
builder.Services.AddJwksManager()
.PersistKeysToFileSystem(new DirectoryInfo(@"/var/keys/jwt"));
Funciona pra cenários onde você tem um volume compartilhado (NFS, Azure Files, EFS). Simples, mas depende do filesystem estar acessível por todas as instâncias.
Trocando o algoritmo
O padrão é ECDsa com P-256 e SHA-256 (ES256). Se precisa de RSA — talvez por compatibilidade com um sistema legado, ou porque seu API gateway só suporta RSA — muda na configuração:
builder.Services.AddJwksManager(options =>
{
options.Jws = Algorithm.Create(DigitalSignaturesAlgorithm.RsaSha256);
});
Tem opções pra todos os algoritmos da RFC 7518: RSA (RS256, RS384, RS512), RSA-PSS (PS256, PS384, PS512), ECDsa (ES256, ES384, ES512), e até HMAC (HS256, HS384, HS512) — embora HMAC com esse componente não faça muito sentido, já que o benefício principal é criptografia assimétrica.
Pra JWE (tokens encriptados, não só assinados):
builder.Services.AddJwksManager(options =>
{
options.Jws = Algorithm.Create(DigitalSignaturesAlgorithm.RsaSsaPssSha256);
options.Jwe = Algorithm.Create(EncryptionAlgorithmKey.RsaOAEP)
.WithContentEncryption(EncryptionAlgorithmContent.Aes128CbcHmacSha256);
});
O que muda na prática
Comparando com a abordagem manual que todo tutorial ensina:
| Aspecto | Chave simétrica manual | Com NetDevPack.Security.Jwt |
|---|---|---|
| Tipo de chave | Simétrica (HMAC-SHA256) | Assimétrica (ECDsa ou RSA) |
| Geração | RandomNumberGenerator.GetBytes(32) na mão |
Automática via componente |
| Rotação | Não existe — troca a chave, tokens morrem | A cada 90 dias, com transição graceful |
| Distribuição | Todas as APIs compartilham o segredo | Só a chave pública é exposta via JWKS |
| Load balancer | Precisa sincronizar a chave entre instâncias | Centralizado no store (DB, filesystem, DataProtection) |
| Segurança se comprometido | Atacante pode validar E criar tokens | Atacante só pode validar, não criar |
| Configuração | ~20 linhas de setup manual | AddJwksManager() + UseJwtValidation() |
A abordagem manual funciona. E entender o que acontece por baixo é importante — saber como uma SigningCredentials funciona, o que é uma SecurityKey, por que a entropia da chave importa. Mas em produção, gerenciamento manual de chaves criptográficas é uma responsabilidade que você não quer carregar.
Quando não usar
Depende do contexto, como sempre.
Se sua aplicação é um monolito server-rendered com Cookie Authentication, você não precisa disso. Cookies não dependem de chaves JWT — o ASP.NET Core DataProtection já cuida da criptografia dos cookies.
Se você está em um cenário de desenvolvimento local, uma API, sem load balancer, sem múltiplos consumidores — uma chave simétrica no appsettings.json resolve. Não over-engineer.
Se você usa um identity provider externo (Keycloak, Auth0, Azure AD), ele já gerencia as chaves e expõe o JWKS dele. Você só consome. O NetDevPack.Security.Jwt é pra quando a sua API é o identity provider.
Conclusão
A maioria dos tutoriais de JWT para em "gera o token, valida, funciona, tchau". O gerenciamento de chaves fica como exercício pro leitor. Só que gerenciamento de chaves não é detalhe de infraestrutura. Chave vazada permite forjar tokens, chave que não rotaciona acumula exposição, e quando o segredo é simétrico e compartilhado entre serviços, qualquer brecha compromete tudo junto.
Se sua API emite tokens JWT, pare de gerenciar chaves na mão. NetDevPack.Security.Jwt resolve essa parte. Instala, configura, e segue trabalhando no que importa.
Próximo passo: troque a chave simétrica do seu projeto pelo IJwtService, adicione o endpoint JWKS, e acesse /jwks no browser.