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);
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);
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
curve1.
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 key2 using the command below:
openssl ec -in privateKey.pem -pubout -out publicKey.pem
Footnotes
-
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".`
Generating a new key pair with the expected curve (
↩︎prime256v1
instead ofsecp521r1
) should resolve the error. -
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);