JWT RSA signing with Node.js JOSE

By John Keyes

August 2, 2024 at 14:26

jwt jwt rsa javascript nodejs

Background

If you are unfamiliar with JWTs, please check out A Primer on JSON Web Tokens.

Generating RSA keys using openssh

See RSA key formatting for details on how to create a key pair, and how to transform them into the correct format for node-jose.

These keys can now be used by node-jose to verify the signature.

Signing with the private key

The first thing we need to do is create a keystore and add the private key to it. This is so we can determine the key id (kid):

async function getKid() {
  const fs = require("fs");
  const pubPem = fs.readFileSync("request_key.pub").toString();
  const jose = require("node-jose");
  const keystore = jose.JWK.createKeyStore();
  await keystore.add(pubPem, "pem");
  console.log(keystore.toJSON().keys[0].kid);
}
getKid().then();

This program will output something like:

xKIeQfluQFxYPsAmWKvTH-HwzCYA7lKf6HgXRie8zRU

Using this we can now create a signed JWT:

async function sign() {
  const fs = require("fs");
  const jose = require("node-jose");
  // read private key
  const privPem = fs.readFileSync("request_key.pkcs8").toString();
  // create key store
  const keystore = jose.JWK.createKeyStore();
  // add the private key to the key store
  await keystore.add(privPem, "pem");
  // get the key based on the kid from previous step
  const privKey = keystore.get("xKIeQfluQFxYPsAmWKvTH-HwzCYA7lKf6HgXRie8zRU");

  // create the JWT
  let payload = { d: "e" };
  r = await jose.JWS.createSign(
    { format: "compact", fields: { alg: "RS256" } },
    privKey,
  )
    .update(JSON.stringify(payload))
    .final();
  // print the JWT as a string to the console
  console.log(r);
}
sign().then();

Run this program and observe the JWT being printed:

eyJhbGciOiJSUzI1NiIsImtpZCI6InhLSWVRZmx1UUZ4WVBzQW1XS3ZUSC1Id3pDWUE3bEtmNkhnWFJpZTh6UlUifQ.eyJkIjoiZSJ9.bMLSVkGYeFtLz77-YWKMsPVgoKVOWDkEhrifXGFJt3v5cpC_9_1EsPyBV1kA6Ez411m7vm7s7mGi_DxTO_VMAUvhzUouxNxzSThYZKJz1ESTvBYo9Cvfxl3lGk_tLlT5_6rptET4GC1aUhzBkpwz55VgRiqtJelf9pQe4a8wMAl3pbTVnfpuXvPBCmTxdy6zddC2fs8kmdV_e58Fw5ZXnduRgai5S-JtxQFfH6QF4QjudF1BwQu046DidSz6SZtXgAlltisG6Pw8Vb9yvwLxGe8aSUOXyi83q1nC8Vb1uaqP9v8-GQQcHRm_RbtMA7klXdn-APfS3Du54In59XWzHxs1NOCWxHhY89Y6xaN8i8J0_mI_Dy8CmaBg3TfXXOaBO2K-qxKKQG7JZ0wXAMskiJsL5f121Z3vbFs-hhIqnP-V2DCUbvopqMf11aSSWGMH0NtjIkyldoU2fan3hJNsCR1DQ8BJb9A3Y4zHHPgh-I7Lo7nuCn0__3lt9X_6cbQK

This token is then saved in the token.txt file. Note, when running the signing program you could redirect the output to token.txt e.g.

node sign.js > token.txt

Verifying with the public key

The public key can be used to verify this token was signed by the private key:

async function verify() {
  const fs = require("fs");
  const jose = require("node-jose");
  // read public key
  const pubPem = fs.readFileSync("request_key.pub").toString();
  // create key store
  const keystore = jose.JWK.createKeyStore();
  // add the private key to the key store
  await keystore.add(pubPem, "pem");
  // read the saved token
  const token = fs.readFileSync("token.txt").toString();
  // verify the token
  let r = await jose.JWS.createVerify(keystore).verify(token);
  // print the token payload
  console.log(r.payload.toString());
}
verify().then();

The following is the output when we run this:

{"d":"e"}

If a different key is used for encoding we can see the createVerify call raises an error:

node_modules/node-jose/lib/jws/verify.js:131
              reject(new Error("no key found"));
...

This shows the key that was used for encoding is not in the keystore used for verifying.

Summary

This is a brief intro to RSA signing JWTs in node-jose, which will hopefully shortcut any future work where we are dealing with JWTs in Node.js.

Last updated: August 2, 2024 at 14:26