DEV Community

Emmanuel Odongo
Emmanuel Odongo

Posted on • Edited on • Originally published at odongo.pl

Using Key Pairs with JWTs

One common way of handling authentication and authorisation in web-based systems is to have a client send their login credentials to the backend, which generates and returns a signed JWT linked to an identity. The client can then access or modify protected resources by attaching the JWT to the request. Before handling the request, the backend verifies the JWT's authenticity.

Signing and Verifying JWTs

JWTs can be signed and verified using a secret. In this case, the same secret is used for signing and verifying. This is a reasonable approach in a monolithic architecture, since only one program has access to the secret.

// GENERATING A JWT USING A SECRET
import { randomUUID } from "crypto";
import * as jwt from "jsonwebtoken";

const SECRET = "123";

const user = {
  id: randomUUID(),
};

const claimSet = {
  aud: "Audience",
  iss: "Issuer",
  jti: randomUUID(),
  sub: user.id,
};

const token = jwt.sign(
  claimSet,
  SECRET,
  {
    algorithm: "HS256",
    expiresIn: "20 minutes",
  }
);

console.log(token); // => eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdWRpZW5jZSIsImlzcyI6Iklzc3VlciIsImp0aSI6ImY1NGEzOGVmLTQ4NzctNGJmYy05N2RmLWFkYzFiNjQxNzU5YiIsInN1YiI6IjRlNzQ5ZTAwLTE1NWItNGNlNi1iYWQyLWExOTE5MWM0MmQ2NyIsImlhdCI6MTY3OTc3OTUwOSwiZXhwIjoxNjc5NzgwNzA5fQ.X94g8OkecnaOYLMuVFmy_hcjJ7nvBMhDEvrUpTvvxQE


// VERIFYING A JWT USING A SECRET
import { verify } from "jsonwebtoken";

const SECRET = "123";

verify(token, SECRET);
Enter fullscreen mode Exit fullscreen mode

An alternative way of signing and verifying JWTs is by using key pairs. This involves signing the JWT using a private key and subsequently verifying it using the corresponding public key.

In a service-oriented architecture, the borders between services are generally drawn in a way that separates concerns. That separation should go hand in hand with the principle of least privilege. From the point of view of a service, it should have the least permissions needed for it to perform its duties.

More concretely, only the service responsible for generating JWTs should have access to the private key. This means that other services are unable to generate valid JWTs; all they can do is use the public key to verify a JWT they have received.

// GENERATING A JWT USING A PRIVATE KEY
import { randomUUID } from "crypto";
import { readFileSync } from "fs";
import { sign } from "jsonwebtoken";

const PRIVATE_KEY = readFileSync("./privateKey.pem");

const user = {
  id: randomUUID(),
};

const claimSet = {
  aud: "Audience",
  iss: "Issuer",
  jti: randomUUID(),
  sub: user.id,
};

const token = sign(
  claimSet,
  PRIVATE_KEY,
  {
    algorithm: "ES512",
    expiresIn: "20 minutes",
  }
);

console.log(token); // => eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdWRpZW5jZSIsImlzcyI6Iklzc3VlciIsImp0aSI6ImE0NzVhYTU5LTIwMGQtNDlkOS1iODVmLTJkZmExM2Q3NTMyMSIsInN1YiI6ImE2YWFkNWY0LTE3NjctNDUwYy04MWNjLTIyMmI3OWI1NzNiYSIsImlhdCI6MTY3OTc4MDI3NiwiZXhwIjoxNjc5NzgxNDc2fQ.AIuJlLZCvpSWLh_ez6pBVX4lcrVbOiUc2NuwCNiw5ms4ELAZRvQFT5-UlKC-PBWXWzWpHh7eO-WWmfOgRnObk_vpAYAo5Wu8Wu-YaL2lBLvaQp2oG5YnXJ9S1kCKGF9i0UloUeYCK6-bdhRvh-rrOqpCOPepWEiQDiWgEzAdPOl75pY4


// VERIFYING A JWT USING A PUBLIC KEY
import { readFileSync } from "fs";
import { verify } from "jsonwebtoken";

const PUBLIC_KEY = readFileSync("./publicKey.pem");

verify(token, PUBLIC_KEY);
Enter fullscreen mode Exit fullscreen mode

Generating Key Pairs

A key pair can be generated from the terminal using openssl.

Before we start generating a key pair, we need to know which curve openssl should use. The ES512 algorithm used in the previous code snippet corresponds to the secp512r1 curve[1].

We can generate the private key by running the following command:

openssl ecparam -name secp521r1 -genkey -out privateKey.pem
Enter fullscreen mode Exit fullscreen mode

The private key is then used to generate the public key[2] using the command below:

openssl ec -in privateKey.pem -pubout -out publicKey.pem
Enter fullscreen mode Exit fullscreen mode

Footnotes

[1]: If you wanted to use a different algorithm, say ES256, but provided the key generated above, jsonwebtoken would throw a helpful error message specifying which curve it expects.

import { randomUUID } from "crypto";
import { readFileSync } from "fs";
import { sign } from "jsonwebtoken";

const PRIVATE_KEY = readFileSync("./privateKey.pem");

const user = {
  id: randomUUID(),
};

const claimSet = {
  aud: "Audience",
  iss: "Issuer",
  jti: randomUUID(),
  sub: user.id,
};

const token = sign(
  claimSet,
  PRIVATE_KEY,
  {
    algorithm: "ES256",
    expiresIn: "20 minutes",
  }
); // => throws `"alg" parameter "ES256" requires curve "prime256v1".`
Enter fullscreen mode Exit fullscreen mode

Generating a new key pair with the expected curve (prime256v1 instead of secp521r1) should resolve the error.

[2]: The node crypto module can generate a public key based off of the private key. So if the service issuing tokens needed to verify them too, we would only need to configure the private key:

import { createPublicKey } from "crypto";
import { readFileSync } from "fs";
import { verify } from "jsonwebtoken";

const PRIVATE_KEY = readFileSync("./privateKey.pem");

const PUBLIC_KEY = createPublicKey(PRIVATE_KEY)
  .export({ format: "pem", type: "spki" });

verify(token, PUBLIC_KEY);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)