Os desenvolvedores que criam um provedor OAuth 2.0 ou OpenID Connect em Node.js podem gerar pares de chaves e exportá-los como JSON Web Keys com o recurso integrado crypto módulo: chamada publicKey.export({ format: 'jwk' })conecte um endpoint JWKS e deixe que os servidores de recursos verifiquem os tokens. A exportação contém apenas o material chave (kty mais n e e para RSA, ou crv, x e y para CE). Isso deixa de fora alg, use e kid. A RFC 7517 marca todas as três como opcionais e testamos que uma única chave simples ainda é verificada com a corrente jose e jwks-rsa. Os metadados são importantes quando você publica mais de uma chave, que é o que a rotação de chaves exige: sem um kidos verificadores não conseguem dizer qual chave assinou um token. Este artigo constrói uma /.well-known/jwks.json endpoint usando apenas o nativo do Node.js crypto módulo e Express, cobrindo geração de chaves, enriquecimento de metadados, derivação de impressão digital RFC 7638 e verificação ponta a ponta.
Pré-requisitos: uma versão atual do Node.js LTS (o código aqui foi testado no Node.js 22), familiaridade básica com Express e uma compreensão de conceitos de chave assimétrica, como pares de chaves públicas/privadas e assinaturas digitais. O jose exemplo de verificação usa a sintaxe do módulo ES, por isso precisa "type": "module" em package.json ou um .mjs extensão de arquivo. Atualizado em outubro de 2026: código testado em Express 4, jose 6.2.12 e jwks-rsa 4.1.0.
Índice
O que um endpoint JWKS compatível com especificações deve servir
JWK e JWKS num relance
Uma JSON Web Key (JWK) é uma estrutura JSON que representa uma chave criptográfica, definida em RFC 7517. Um JWK Set (JWKS) envolve um ou mais JWKs em um objeto com um único keys variedade: { "keys": (...) }. Os servidores de autorização publicam documentos JWKS em um URI conhecido (conforme descrito por RFC 8414 e OpenID Connect Discovery) para que as partes confiáveis possam buscar chaves públicas e verificar JWTs assinados sem segredos pré-compartilhados.
Cada JWK inclui kty (tipo de chave) mais os componentes matemáticos da chave: n e e para RSA, ou crv, xe y para CE. Ambos os tipos de chave compartilham os mesmos campos de metadados que os verificadores esperam junto com esses componentes: alg (algoritmo pretendido, por exemplo, "RS256" ou "ES256"), use (finalidade, por exemplo, "sig"), e kid (identificador exclusivo para seleção de chave de um conjunto).
Exportação nativa vs. metadados necessários
A lacuna entre o que o Node.js fornece e o que os verificadores precisam é concreta e previsível:
Propriedades retornadas por KeyObject.export({ format: 'jwk' }) | Propriedades opcionais (RFC 7517) que os verificadores usam |
|---|---|
RSA: kty, n, e | alg — identifica o algoritmo de assinatura (RFC 7517) |
CE: kty, crv, x, y | use — declara o propósito da chave, por exemplo, "sig" (RFC7517) |
kid — identificador de chave exclusivo para seleção de chave (RFC 7517) |
O Node.js exporta apenas o material da chave, então você mesmo adiciona essas propriedades. Por RFC 7517, alg, use e kid são todos opcionais. kid é o que mais importa na prática: é como um verificador escolhe uma chave de um conjunto JWK durante a substituição de chave, e a RFC diz que chaves distintas em um conjunto devem usar kid valores. Testamos isso com jose 6.2.12 e jwks-rsa 4.1.0. Um JWKS com uma chave RSA ou EC simples foi verificado em ambos. Um JWKS com duas chaves RSA e sem kid falhou em jose com ERR_JWKS_MULTIPLE_MATCHING_KEYSe em jwks-rsa com “Nenhum KID especificado e o endpoint JWKS retornou mais de 1 chave”. Adicionando um kid para ambas as chaves corrigiu isso.
Configurando o Projeto
Antes de escrever qualquer código, inicialize um projeto e instale as dependências necessárias:
mkdir jwks-demo && cd jwks-demo
npm init -y
npm install express@4
O jose biblioteca (usada posteriormente para verificação) é o primeiro ESM, então adicione "type": "module" para o seu package.json:
npm pkg set type=module
npm install jose
Todo o código neste artigo pressupõe um único arquivo (server.mjs) salvo indicação em contrário. Usando o .mjs extensão ou "type": "module" mantém os exemplos funcionando como módulos ES.
Gerando um par de chaves RSA com criptografia Node.js
Usos de geração de chaves crypto.generateKeyPairSyncque retorna KeyObject instâncias para as chaves pública e privada:
import crypto from 'node:crypto';
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
});
const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
O modulusLength: 2048 parâmetro produz um tamanho de chave compatível com NIST SP 800-131A Rev. 2 para uso até 2030; usar modulusLength: 3072 para chaves destinadas a permanecer válidas após essa data. O mesmo padrão se aplica às chaves EC usando crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); um exemplo complementar de EC aparece posteriormente neste artigo.
Exportando a chave pública e inspecionando a lacuna
Exportando o público KeyObject no formato JWK revela exatamente o que a API nativa fornece e o que deixa de fora:
const jwk = publicKey.export({ format: 'jwk' });
console.log(JSON.stringify(jwk, null, 2));
A saída para uma chave RSA é semelhante a esta:
{
"kty": "RSA",
"n": "" ,
"e": "AQAB"
}
Não alg. Não use. Não kid. Isso corresponde ao comportamento documentado na referência da API criptográfica do Node.js em https://nodejs.org/api/crypto.html, que especifica que a exportação JWK produz apenas os componentes matemáticos da chave. Três propriedades precisam ser adicionadas antes que este objeto seja útil para qualquer verificador compatível com padrões.
Enriquecendo o objeto JWK
Contexto alg e use
O alg propriedade declara o algoritmo de assinatura para a chave. Para uma chave RSA, os valores válidos incluem RS256, RS384, RS512, PS256, PS384e PS512. Para uma chave EC na curva P-256, o algoritmo correspondente é ES256; P-384 mapeia para ES384e P-521 mapeia para ES512. O use propriedade declara o propósito da chave. Para assinar JWTs, o valor é "sig".
Uma função auxiliar pode anexar essas propriedades enquanto valida se o tipo de chave e o algoritmo são compatíveis:
const RSA_ALGORITHMS = new Set(('RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'));
const EC_CURVE_ALG_MAP = { 'P-256': 'ES256', 'P-384': 'ES384', 'P-521': 'ES512' };
function enrichJwk(jwk, algorithm) {
if (jwk.kty === 'RSA') {
if (!RSA_ALGORITHMS.has(algorithm)) {
const err = new Error(`Algorithm ${algorithm} is not valid for RSA keys`);
err.code = 'ERR_INVALID_ALGORITHM';
throw err;
}
} else if (jwk.kty === 'EC') {
const expectedAlg = EC_CURVE_ALG_MAP(jwk.crv);
if (!expectedAlg || expectedAlg !== algorithm) {
const err = new Error(
`Algorithm ${algorithm} is not valid for EC key with curve ${jwk.crv}; expected ${expectedAlg}`
);
err.code = 'ERR_INVALID_ALGORITHM';
throw err;
}
} else {
const err = new Error(`Unsupported key type: ${jwk.kty}`);
err.code = 'ERR_UNSUPPORTED_KEY_TYPE';
throw err;
}
return {
...jwk,
alg: algorithm,
use: 'sig',
};
}
Esta etapa de validação evita um bug onde, por exemplo, uma chave EC P-256 é acidentalmente rotulada RS256fazendo com que os verificadores falhem com erros crípticos de incompatibilidade de algoritmo.
Derivando kid via impressão digital RFC 7638 JWK
RFC 7638 define um método determinístico para calcular uma impressão digital JWK. O procedimento é simples: construa um objeto JSON canônico contendo apenas os membros necessários para o tipo de chave, em ordem lexicográfica (alfabética), conforme exigido pela RFC 7638 §3, depois calcule o hash SHA-256 dessa string JSON e, finalmente, codifique o hash como base64url.
Para chaves RSA, os membros necessários são e, ktye n (ordem alfabética). Para chaves EC, elas são crv, kty, xe y. O determinismo desta abordagem é a sua principal vantagem sobre UUIDs ou strings aleatórias: o mesmo material de chave sempre produz o mesmo kidtornando a identificação da chave reproduzível em todas as implantações.
O determinismo desta abordagem é a sua principal vantagem sobre UUIDs ou strings aleatórias: o mesmo material de chave sempre produz o mesmo
kidtornando a identificação da chave reproduzível em todas as implantações.
function deriveKid(jwk) {
let canonicalObj;
if (jwk.kty === 'RSA') {
canonicalObj = { e: jwk.e, kty: jwk.kty, n: jwk.n };
} else if (jwk.kty === 'EC') {
canonicalObj = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
} else {
const err = new Error(`Unsupported key type for thumbprint: ${jwk.kty}`);
err.code = 'ERR_UNSUPPORTED_KEY_TYPE';
throw err;
}
const canonicalJson = JSON.stringify(
Object.fromEntries(
Object.entries(canonicalObj).sort(((a), (b)) => (a < b ? -1 : a > b ? 1 : 0))
)
);
const hash = crypto.createHash('sha256').update(canonicalJson).digest();
const base64url = hash
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return base64url;
}
Montando o JWK Final
Combinar os dois auxiliares em uma única função utilitária produz um JWK que você pode publicar imediatamente:
function buildPublicJwk(keyObject, algorithm) {
const rawJwk = keyObject.export({ format: 'jwk' });
const enriched = enrichJwk(rawJwk, algorithm);
const kid = deriveKid(rawJwk);
const finalJwk = {
...enriched,
kid,
};
return finalJwk;
}
const completedJwk = buildPublicJwk(publicKey, 'RS256');
if (process.env.NODE_ENV !== 'production') {
console.log(JSON.stringify(completedJwk, null, 2));
}
A saída registrada agora contém todos os campos esperados: kty, n, e, alg, usee kid. Este objeto está pronto para ser servido a partir de um endpoint JWKS.
Construindo o endpoint Express JWKS
O /.well-known/jwks.json Rota
O código a seguir assume crypto, enrichJwk, deriveKide buildPublicJwk são definidos no mesmo arquivo (ou importados de um módulo compartilhado). Um aplicativo Express pronto para produção pode gerar (ou carregar) um par de chaves na inicialização, construir o JWK e servi-lo com cabeçalhos apropriados:
import express from 'express';
const app = express();
const rsaJwk = buildPublicJwk(publicKey, 'RS256');
let currentKeys = (rsaJwk);
const getJwksDocument = () => JSON.stringify({ keys: currentKeys });
app.get('/.well-known/jwks.json', (req, res) => {
res.set('Content-Type', 'application/json');
res.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=60');
res.set('X-Content-Type-Options', 'nosniff');
res.set('Vary', 'Accept-Encoding');
res.send(getJwksDocument());
});
const server = app.listen(3000, () => {
console.log('JWKS endpoint available at http://localhost:3000/.well-known/jwks.json');
});
server.on('error', (err) => {
console.error('Server failed to start:', err.code, err.message);
process.exit(1);
});
Usando um getJwksDocument() A função em vez de uma constante pré-serializada permite que o endpoint forneça material de chave atualizado após a rotação sem reiniciar o processo. Para a rotação de chaves, a estratégia é pré-publicar a próxima chave, empurrando-a para a frente do currentKeys array, mantendo as chaves antigas até que todos os tokens assinados com eles expirem. Os verificadores selecionam chaves combinando o kid no cabeçalho JWT em relação ao kid valores na resposta JWKS, portanto, manter as chaves antigas e novas durante uma janela de transição evita falhas de verificação.
Adicionando uma chave EC junto com RSA
O suporte a vários algoritmos requer a geração de um par de chaves adicional e a inclusão de ambos os JWKs no keys variedade. Atualizar currentKeys incluir ambos os JWKs em vez de registrar uma segunda rota (o Express não substitui manipuladores de rota – uma duplicata app.get para o mesmo caminho sombrearia silenciosamente o segundo manipulador):
const { publicKey: ecPublicKey, privateKey: ecPrivateKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'P-256',
});
const ecJwk = buildPublicJwk(ecPublicKey, 'ES256');
currentKeys = (rsaJwk, ecJwk);
O deriveKid função lida com ambos os tipos de chave sem modificação porque seleciona os membros canônicos com base em kty. Cada chave recebe um valor único e determinístico kid derivado de seus próprios componentes matemáticos.
Testando e verificando o endpoint
Verificação manual com curl
Uma rápida verificação manual confirma que o endpoint atende JWKS bem formado (requer jq a ser instalado):
curl -s http://localhost:3000/.well-known/jwks.json | jq .
A saída deve mostrar o keys array com cada JWK contendo kty, n, e (ou crv, x, y), alg, usee kid. Para inspeção visual, assine um JWT usando a chave privada e cole-o em jwt.io. Observe que jwt.io não pode buscar JWKS de localhost na maioria dos ambientes de navegador; use a verificação programática abaixo ou exponha o endpoint por meio de um túnel como ngrok.
Verificação programática com jose
Verificação ponta a ponta usando o jose biblioteca prova que um verificador de token do mundo real pode usar as chaves e metadados que você publica. jose v6 é publicado como módulos ES, portanto este arquivo deve usar o .mjs extensão ou seu projeto deve ter "type": "module" em package.json.
O script a seguir assume que o servidor da seção anterior está rodando na porta 3000. Ele precisa de acesso a privateKey e rsaJwk — defina todo o código em um único server.mjs arquivo que também inicia o servidor ou exporte esses valores de um módulo compartilhado e importe-os aqui.
import { SignJWT, createRemoteJWKSet, jwtVerify } from 'jose';
async function verifyRoundTrip() {
const jwt = await new SignJWT({ sub: 'user123' })
.setProtectedHeader({ alg: 'RS256', kid: rsaJwk.kid })
.setIssuedAt()
.setExpirationTime('1h')
.setIssuer('http://localhost:3000')
.sign(privateKey);
console.log('JWT signed, length:', jwt.length);
const JWKS = createRemoteJWKSet(
new URL('http://localhost:3000/.well-known/jwks.json')
);
const { payload, protectedHeader } = await jwtVerify(jwt, JWKS, {
issuer: 'http://localhost:3000',
});
console.log('Verified payload:', payload);
console.log('Protected header:', protectedHeader);
}
verifyRoundTrip().catch((err) => {
console.error('Verification failed:', err.code ?? err.name, err.message);
process.exit(1);
});
Se o kid no cabeçalho JWT não corresponde a nenhum kid na resposta JWKS, jose lança ERR_JWKS_NO_MATCHING_KEY. Se o JWKS contiver várias chaves que possam corresponder e o token não tiver kidvocê consegue ERR_JWKS_MULTIPLE_MATCHING_KEYS em vez de. Se jwtVerify for bem-sucedido, as chaves publicadas e seus kid os valores se alinham com o token.
Considerações de produção
Armazenamento e rotação de chaves
Se você gerar chaves em cada reinicialização, invalidará todos os tokens emitidos anteriormente. Na produção, persista chaves privadas em um arquivo criptografado, um módulo de segurança de hardware ou um gerenciador de segredos de nuvem, como AWS Secrets Manager ou HashiCorp Vault. Carregue-os na inicialização usando crypto.createPrivateKey() com o PEM armazenado. Para rotação, publique a nova chave kid na resposta JWKS antes de assinar qualquer token com ele, dando aos verificadores tempo para armazenar em cache o conjunto de chaves atualizado. Remova a chave antiga somente depois que todos os tokens assinados com ela expirarem.
Cache e desempenho
Contexto Cache-Control: public, max-age=3600 diz aos caches downstream e às bibliotecas verificadoras para reutilizar a resposta por uma hora, em vez de atingir o ponto final em cada verificação de token. A RFC 8414 não exige um valor específico, portanto escolha um TTL que corresponda à rapidez com que você gira as chaves. Usando um getJwksDocument() função em vez de chamar JSON.stringify em cada solicitação no caminho ativo mantém a sobrecarga de serialização mínima e ainda oferece suporte à rotação dinâmica de chaves.
Segurança: TLS em produção
O endpoint JWKS deve ser servido por HTTPS na produção. Todos os exemplos de código neste artigo usam http://localhost para conveniência do desenvolvimento local. Antes de qualquer implantação não local, configure o Node.js integrado https módulo com um certificado TLS ou coloque o aplicativo atrás de um proxy reverso com terminação TLS, como nginx ou Caddy. Servir chaves públicas por meio de HTTP simples permite que invasores intermediários substituam chaves forjadas, permitindo-lhes falsificar tokens que os verificadores aceitarão. O X-Content-Type-Options: nosniff O cabeçalho evita que os navegadores reinterpretem a resposta JSON como um tipo MIME executável, o que é importante se o URL for carregado em um contexto de navegador.
Servir chaves públicas por meio de HTTP simples permite que invasores intermediários substituam chaves forjadas, permitindo-lhes falsificar tokens que os verificadores aceitarão.
Construindo na Fundação
Node.js. crypto O módulo exporta deliberadamente apenas os principais componentes matemáticos de um JWK. O alg, usee kid propriedades são de responsabilidade do desenvolvedor. A RFC 7517 os torna opcionais, mas kid é o que permite que os verificadores escolham a chave certa quando você publica mais de uma. A abordagem abordada aqui aborda todas as três lacunas: alg e use por meio de uma função auxiliar validada, e kid por meio de uma impressão digital RFC 7638 que é determinística, reproduzível e compatível com os tipos de chave RSA e EC. O buildPublicJwk utilitário compõe essas peças em uma única chamada que transforma um simples KeyObject em um JWK compatível com as especificações. Ainda não cobre chaves Ed25519 (OKP); adicionar suporte requer estender deriveKid com os membros canônicos do OKP (crv, kty, x) e mapeamento EdDSA na validação do algoritmo. Trate a impressão digital RFC 7638 como padrão kid estratégia de derivação, a menos que sua infraestrutura exija especificamente uma alternativa.
Source link




