Using Key Pairs with JWTs
Tags: javascript, jwt
Published: 2023-03-26 20:20:58

---
published: true
title: "Using Key Pairs with JWTs"
tags: ["javascript", "jwt"]
description: "JWTs are a format for representing claims between two parties. JWTs can be signed to increase confidence in their security. This signing, and subsequent verification, can be done using a secret or a key pair"
canonical_url: https://odongo.pl/jwt-asymmeytric-key-pair/
---

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.

```js
// 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);
```

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.

```js
// 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);
```

## 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<sup>[[1](#footnote-1)]</sup>.

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

```
openssl ecparam -name secp521r1 -genkey -out privateKey.pem
```

The private key is then used to generate the public key<sup>[[2](#footnote-2)]</sup> using the command below:

```
openssl ec -in privateKey.pem -pubout -out publicKey.pem
```

<a name="footnotes"></a>

## Footnotes

<a name="footnote-1">[1]</a>: 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.

```js
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".`
```

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

<a name="footnote-2">[2]</a>: 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:

```js
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);
```