Gerenciamento de chaves JWT no ASP.NET Core

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?

24 de Abril de 2026

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_uri com 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.

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente