Como criar e verificar tokens JWT via endpoint JWK para seus microserviços em node js
Olá ! Neste tutorial veremos como podemos criar nosso próprio endpoint JWK público para verificar nosso token JWT. JWT tem sido amplamente utilizado no design de microsserviços modernos. Isso nos facilita a transferência de carga via assinatura segura. Um dos métodos amplamente adotados de assinar jwt é o RSA, que usa chaves públicas e privadas para assinar e verificar o token, respectivamente. Essas teclas podem ser representadas em um arquivo pem ou uma string ou também JWK.
Vamos começar…
JWK significa JSON Web Key. Um JWK típico para chave privada seria como:
{
"keys": [
{
"kty": "RSA",
"kid": "pos6sKqyyi1LmTP1-hjHpPR7-1nrqUhgk6CPhry-YvY",
"n": "2pDe9zBdvpmtPrqg-7RMldTKT6FMdw1YhleyhsoqY0dUEVk08kgkilVisvsKr5cExDSHDCwyz9edj9DF2Bm7YQ",
"e": "AQAB",
"d": "FkZDYt-7_gu9WzI768sBLxfjkl_24f8rMW3IlPIPhdRz0L9YoMhfMkT_7ClWkZwwWraPJMf2MNCN6UX8SE7WTQ",
"p": "-zYkOgqXYcR_n4j2X543oyRSgleiu0aqcoKQ52SOaAM",
"q": "3rttConSkWFd8DHTzxlkhNB8y_cSfpjTESXcWGc8a8s",
"dp": "aMFmD_IUuIdZdOyHWM5Agz6FTac_y_qm30OFK4jqPYM",
"dq": "JevXNtIcTbA8JCb3nuz91jcA6GEafv9aADNn_o0lFl0",
"qi": "oEQGatYl7VazeAPRxK6h53iHecYTYQjYVkY4TRK0MiE"
},
{
"kty": "RSA",
"kid": "ZHuDkZMFZHh7rd0sA1jUnmoqMxElow8i5n4S7UX7jwM",
"n": "z08t46yQghDYY9dln4ZSHdBTDrX3M-uK2yvHDEfsDfzR5xES-qNJXZ3vLb2Vsom9GzY-4dS1abG6dplhaAkK1Q",
"e": "AQAB",
"d": "w6ZLfdLPsyDoyBlx_EMNXrvMl1aeje6fZseDHJEINA2R6LgqHwIAANw5P-HR6dQDFQdhDmGWFnW0giOItAjJoQ",
"p": "9X8yzDW33gs7qY3fprMI7Bh22fHHxkdQg-ww8oDZWek",
"q": "2C27W0XjwxblxFKEdC7_j4YM-qZTCcfl-70jpN99ag0",
"dp": "kU32PRRWfeBcMeE9TSeO0l8wiZMn0V4Ic-zqk75b53E",
"dq": "zlNnnIeqCMtT5Pq0_IbW188jmB8i5hTqRkiROo0sEAk",
"qi": "U3gO2J5P4QaaxpgjU7738wkQIHbNn6pFsOJu0MvSPKQ"
}
]
}
Para entender isso, devemos saber um pouco sobre padrões de criptografia de chave pública (PKCS). A chave pública consiste em dois parâmetros “n” e “e” onde:
n: o módulo RSA, um inteiro positivo
e: o expoente público RSA, um inteiro positivo
[ref:https://tools.ietf.org/html/rfc3447#section-3.1]
Da mesma forma, a chave privada consiste em
n: o módulo RSA, um número inteiro positivo
d: o expoente privado RSA, um número inteiro positivo
p: o primeiro fator, um número inteiro positivo
q: o segundo fator, um número inteiro positivo
dP: o expoente CRT do primeiro fator, um número inteiro positivo
dQ: o expoente CRT do segundo fator, um número inteiro positivo
qInv: o (primeiro) coeficiente CRT, um número inteiro positivo
r_i: o i-ésimo fator, um número inteiro positivo
d_i: o expoente CRT do i-ésimo fator, um número inteiro positivo
t_i: o coeficiente CRT do i-ésimo fator, um número inteiro positivo
[ref:https://tools.ietf.org/html/rfc3447#section-3.2]
Você pode cavar em cada um desses você auto mas o importante para entender aqui é que o inteiro aqui é representado usando a codificação base64url. Assim, a chave pública para o JWK acima da chave privada será como:
{
"keys": [
{
"kty": "RSA",
"kid": "pos6sKqyyi1LmTP1-hjHpPR7-1nrqUhgk6CPhry-YvY",
"n": "2pDe9zBdvpmtPrqg-7RMldTKT6FMdw1YhleyhsoqY0dUEVk08kgkilVisvsKr5cExDSHDCwyz9edj9DF2Bm7YQ",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "ZHuDkZMFZHh7rd0sA1jUnmoqMxElow8i5n4S7UX7jwM",
"n": "z08t46yQghDYY9dln4ZSHdBTDrX3M-uK2yvHDEfsDfzR5xES-qNJXZ3vLb2Vsom9GzY-4dS1abG6dplhaAkK1Q",
"e": "AQAB"
}
]
}
Agora, se você olhar mais de perto, veremos que há matriz de chaves no objeto chaves. Essas chaves têm um garoto que identifica com exclusividade cada chave. Vamos engradar um token apenas em um momento, mas vamos ver um JWT criado assinado a partir da chave privada acima:
eyJhbGciOiJSUzI1NiIsImtpZCI6InBvczZzS3F5eWkxTG1UUDEtaGpIcFBSNy0xbnJxVWhnazZDUGhyeS1ZdlkifQ.eyJrZXkiOiJobHcifQ.fnu05rRspMJiD2KBpsD9g2SGUD3gbuqN1qC_lfIz4OJvWQ2J2fMe--QxN09oAomkiWjRzxU3JHOvO45gbfl2Lw
Este é apenas um texto base64encode então se você decodificá-lo em alguns sites populares de decodificação JWT você verá o campo de cabeçalho como:
{
"alg": "RS256",
"kid": "pos6sKqyyi1LmTP1-hjHpPR7-1nrqUhgk6CPhry-YvY"
}
Podemos ver que o campo de cabeçalho do JWT tem o garoto da chave que assinou. Assim, qualquer biblioteca de verificação JWT verificará este token pesquisando o garoto a partir das chaves do JWK público.
Antes de começarmos
Antes de começarmos, vamos criar uma chave pública e privada via openssl. Criar chave privada:
openssl genrsa -out k1.private 1024
openssl genrsa -out k2.private 1024
Você pode querer criar uma chave pública também, mas isso não é necessário, pois a chave tem partes públicas e privadas. Para este tutorial usaremos a biblioteca node-jose. Vamos criar um arquivo como jwtHelper.js
const jose = require('node-jose');
const fs = require('fs');
const path = require('path');
const keystore = jose.JWK.createKeyStore();
const opts = {
algorithms: ["RS256"],
};
const privkeys = [
fs.readFileSync(path.join("k1.private")),
fs.readFileSync(path.join("k2.private")),
];
async function creteKeystore() {
await keystore.add(privkeys[1], 'pem');
await keystore.add(privkeys[0], 'pem');
}
async function createPublicJWK() {
const jwk = keystore.toJSON();
return jwk;
}
async function createPrivateJWK() {
// giving true as argument will give private JWK
const jwk = keystore.toJSON(true);
return jwk;
}
async function createJWT(payload) {
let token = await jose.JWS
.createSign({ format: 'compact' }, keystore.all()[0])
.update(JSON.stringify(payload))
.final();
return token;
}
async function verifyJWT(token, jwk) {
const keystore = await jose.JWK.createKeyStore();
for (const key of jwk.keys) {
await keystore.add(key);
}
const result = await jose.JWS
.createVerify(keystore, opts)
.verify(token);
return result;
}
exports.creteKeystore = creteKeystore;
exports.createPublicJWK = createPublicJWK;
exports.createPrivateJWK = createPrivateJWK;
exports.createJWT = createJWT;
exports.verifyJWT = verifyJWT;
Criando JWT e Verificando via JWK
Usaremos nosso módulo jwtHelper para criar o JWT e também verificaremos o token. Vamos criar um arquivo .js índice:
const jwtHeler = require("./jwtHelper");
async function init() {
const payload = {
msg: "Hello!"
};
await jwtHeler.creteKeystore();
const publicJWK = await jwtHeler.createPublicJWK();
const privateJWK = await jwtHeler.createPrivateJWK();
const token = await jwtHeler.createJWT(payload);
const result = await jwtHeler.verifyJWT(token, publicJWK);
console.log("public jwk :: ", JSON.stringify(publicJWK));
console.log("\n");
console.log("private jwk :: ", JSON.stringify(privateJWK));
console.log("\n");
console.log("token :: ", token);
console.log("\n");
console.log("payload :: ", result.payload.toString());
}
init();
Aqui nós criamos o JWK público. Depois disso, criamos um token JWT do JWK privado. Este token é finalmente verificado através do JWK público.
Conclusão
Espero que goste do tutorial. Além disso, você também pode adicionar um ponto final http e fornecer JWT público para outras partes. A partir deste ponto final, eles podem verificar os tokens fornecidos. Mas fazê-lo NÃO TORNE O PRIVADO JWK visível para qualquer um. O código fonte pode ser encontrado aqui.
Referência:
- RSA: https://en.wikipedia.org/wiki/RSA_(criptosystem)
- JWK: https://tools.ietf.org/html/rfc7517
- PKCS: https://tools.ietf.org/html/rfc3447