Verifying an RS256 signed JWT
JSON Web Tokens (JWT) are used as a way to verify the identity of the caller of an API.
The best way to verify a JWT is to use a verification library.
I wanted to have a look at some of what those libraries are doing under the hood by putting together a function that will return if a given token is valid. In this blog I'll go through what I have done to get a validation function working.
To simplify things assume:
- That the signing algorithm is RS256 all others are considered invalid.
- That the public keys are available on a JWKS url provided to the function eg https://klee-test.au.auth0.com/.well-known/jwks.json
- I only want to know if the token was signed by a key available at the above url. I wont be checking if the token has expired, if the scopes or other claims are valid.
Break up the token
A JWT is made up to 3 parts. The first thing to do in validating the token is to break this apart.
- The header - Meta information about the token. This is a JSON string tha has been base 64 encoded.
- The Body - Claims that the token is asserting. This is a JSON string tha has been base 64 encoded.
- The Signature - Used to verify the integrity of the token.
The token string is made up of the three sections concatenated with a .
character. The first thing to do is to split the 3 sections.
const [rawHead, rawBody, signature] = jwt.split(".");
Check the Algorithm
To check the token has been signed with the expected algorithm the head needs to be readable.
Create a way to decode the base 64 encode string into TypeScript objects.
function decodeAndJsonParse<T>(base64: string): T {
// Decode the JSON string from Base 64
const json = new Buffer(base64, "base64").toString("ascii");
// Return the parsed object
return JSON.parse(json);
}
Read the head section of the JWT into a known type { alg: string; kid: string }
.
const parsedHead = decodeAndJsonParse<{ alg: string; kid: string }>(rawHead);
Check that the alg
property is the algorithm that was used to sign the token. This example is only going to support the RS256
signing method. If the algorithm is anything else reject the key.
if (parsedHead.alg !== "RS256") {
return false;
}
Get the key
The next step in validating that the token was signed with a known private key is to fetch the public key. The standard for sharing these are to provide a JSON Web Key Set (JWKS) endpoint. The return value from this endpoint for RS256 keys matches this type definition:
Set up a TypeScript type for the key
type JWKS = {
keys: JWK[];
};
type JWK = {
alg: string;
kty: string;
use: string;
n: string;
e: string;
kid: string;
x5t: string;
x5c: string[];
};
Important properties for this example are:
alg
: The Algorithm that was used to sign the key.
n
: The public key modulus (Base64urlUInt encoded)
e
: The public key exponent. (Base64urlUInt-encoded)
kid
: The key identifier.
The spec that outlines the use of all the parameters can be found in RFC7518;
Fetch the key
Use node-fetch
to get the JWKS keys from the provided endpoint.
// Get the key
const jwksResponse = await fetch(jwksEndpoint);
// Read the JSON response as a JWKS type
const jwks: JWKS = await jwksResponse.json() as JWKS;
Check the key
Check that there is a key in the set with a kid
that matches the kid
from the tokens head.
// Find the key that matches the token
const jwk = jwks.keys.find((key) => key.kid === parsedHead.kid);
// Check that a key was found and that it's the correct algorithm
if (!jwk || jwk.alg !== "RS256") {
return false;
}
Convert the JWK to pem
The JWK is a JSON object that contains the components of the public key. To validate the JWT the components need to be converted into a PEM format key.
To do this pass the n
and e
parameters of the JWK to NodeRSA with the "components-public"
flag. Then export the key into a PEM key string.
// Make an instance of Node RSA from the JWK public key components
const key = new NodeRSA(
{
n: Buffer.from(jwk.n),
e: Buffer.from(jwk.e),
},
"components-public"
);
// Export the key into the desired formats
const pem = key.exportKey("pkcs8-public-pem");
Use Crypto to verify the token
Node provides a built in Crypto library that can be used to validate the token.
Setup the Crypto verify object
Start by creating an instance of the Verify class from the Crypto package by using the createVerify
factory method with the "RSA-SHA256"
to specify the algorithm to use.
// Create a verify object that can be used to verify the token
const verifyObject = Crypto.createVerify("RSA-SHA256");
Add the token head and body to the verify object.
The verify object can be written to as a stream. Write in the head and the body section of the JWT with a "."
in the middle. Effectively taking the JWT without the signature.
// Write the base64 encoded head the . character and the base64 encoded body to the stream.
verifyObject.write(rawHead + "." + rawBody);
// Close the stream
verifyObject.end();
Normalise the base64 signature
This is the tricky part. The signature is Base64URL encoded but it needs to be Base64 encoded. Node is able to take care of this by passing it in and out of a buffer. If this isn't done the signature will always be invalid.
// Important! Normalise the base64 encoding by reading the signature in and writing it out.
const base64Signature = Buffer.from(signature, "base64").toString("base64");
Validate the signature
The final step is to use the verifyObject
to validate that the JWT's signature was created using the private key that is paired with the public key retrieved from the JWKS endpoint.
This is done by passing the PEM formatted public key, the signature and the format into the verify
method on the verifyObject
// validate that the signature is correct.
const signatureIsValid = verifyObject.verify(pem, base64Signature, "base64");
Don't do this
There are lots of great libraries available to validate tokens, take care of formatting issues and they are probably more reliable and robust than what I've put together here. If you're looking for a library to use try the listing on jwt.io.
Code for this blog can be found on my GitHub
Cover image from unsplash
Top comments (2)
I actually made a npm package for this. npmjs.com/package/jwt-quick
Still room to improve, but works with types and supports a couple of algorithms.
Nice. I advocate hard for always using a library to do this kind of thing. I wanted to explore what happens under the hood, but in any production code I'd always leave auth stuff to a library.