JWT Parsing with Javascript
The previous two articles, showed how to create a login page using AWS Cognito, and how to break down the Json Web Token it produces. This article follows on from both of these, and shows how we can programmatically parse the JWT using Javascript.
Parsing the JWT with Javascript
I want to be able to validate the JWT and get its content in a variety of ways
(for example, in server side code, and in an AWS Lambda). I am therefore
going to use Javascript as my programming language. In this example, we will
need to make http requests. I will use the axios
package for this. To
manipulate JWTs, we will use the jsonwebtoken
package. Finally, we need to
convert between different certificate standards, and I will use the
jwk-to-pem
package for these conversions. So, we start off our code by
including all these modules:
// Libraries we need
const axios = require("axios");
const jwkToPem = require("jwk-to-pem");
const jwt = require("jsonwebtoken");
Next, since we are working with AWS, we need to set up some variables in order
to access resources. These are the region
, the poolId
(from our cognito
user pool), and the URL for the issuer
of
our JWT (again you can see this in the previous article):
// User pool information
const region = "us-east-1";
const poolId = "us-east-1_MsHScNijB";
const issuer = "https://cognito-idp." + region + ".amazonaws.com/" + poolId;
In order to verify the JWT we receive, we need to specify the hashing algorithm (just like we did earlier in the online JWT debugger), and also specify the issuer we are expecting (from the variable we’ve just defined):
// When verifying, we expect to use RS256, and that our issuer is correct
const verificationOptions = {
algorithms: ["RS256"],
issuer
};
Eventually, we will pass our JWT in as a parameter, but for now we will just set it in a variable (truncated in this example):
// Get our token
const token = "eyJraWQiOi...";
To validate the JWT we received, we run through a number of steps:
- Get the public keys for our user pool
- Find the specific key used for this JWT
- Verify the token (does the signature match, and is the issuer correct)
- Check that the token is of the correct type (we want an access token)
If we pass all the above steps, we know our JWT is good, and we can let the user do something. If we wished to, we could add additional checks to provide finer-grained access, but that can wait for another day.
The Javascript code to achieve this, is structured using promises. This gives us readable code, which mirrors the above list:
// The validation process
function validate(token, doSomething) {
getKeys(region, poolId)
.then(indexKeys)
.then(findVerificationKey(token))
.then(verifyToken(token))
.then(checkTokenUse)
.then(doSomething)
.catch(handleError);
}
For the purposes of this example, our do something is just printing out the result. We also handle errors by just printing out the error message:
const doSomething = console.info;
// Error handling
function handleError(err) {
console.error(err);
}
So, we now have the structure of our validator. We will define these functions as we progress through this article, but first a brief interlude.
Retrieving the Public Keys
As just mentioned, the public keys we need are available from our issuer URL.
The path we need in the URL is /.well-known/jwks.json
. So, we just construct
the appropriate URL and use axios to retrieve the content. Because we are
chaining promises together, the keys we get back from axios need to be
wrapped in a new promise (ready for the next step to .then()
the result):
// Get our keys
function getKeys(region, poolId) {
let jwksUrl = issuer + "/.well-known/jwks.json";
return axios.get(jwksUrl).then(response => Promise.resolve(response.data.keys));
}
This returns an array of keys associated with our Cognito user pool:
[ { alg: 'RS256',
e: 'AQAB',
kid: 'cOC7t2DqhGQ6nW0C6PLUJmFjbJxKdfvTYDaNtrXKVvw=',
kty: 'RSA',
n: 'onEegrGePE6RXVwyr4QE...',
use: 'sig' },
{ alg: 'RS256',
e: 'AQAB',
kid: 'oasMMVu5r1YbNMG+sI0/LgSTTG283WYO0vSQjl6gMVs=',
kty: 'RSA',
n: 'kOVH_KT2QChe6pKxPHMF...',
use: 'sig' } ]
One of the challenges with working with keys and certificates, is that there
are multiple file formats for storing them. The JWT validation library we are
using wants the keys in a format known as PEM. So, we are going to take each
of the keys in the array, and convert them to PEM format. If you think back
to when we used the jwt.io debugger to look at our JWT, you will recall that
there was a kid value in the JWT header. These are the kid fields in the
above array of keys. So, at the same time as we convert to PEM format, we will
also convert the structure from an array to an object, indexed on the kid.
We can use Javascript’s reduce()
function to achieve this:
// Index keys by "kid", and convert to PEM
function indexKeys(keyList) {
let result = keyList.reduce((keys, jwk) => {
keys[jwk.kid] = jwkToPem(jwk);
return keys;
}, {});
return Promise.resolve(result);
}
This gives us an object of this format:
{
'cOC7t2DqhGQ6nW0C6PLUJmFjbJxKdfvTYDaNtrXKVvw=':
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonEegrGePE6RXVwyr4QE
...
aSa0u4UBk81FjlRCMFbqDRwJ+Cu41a35cLbt7D28TYGX7LGiGAgBIzTqXXWwnWAe
DQIDAQAB
-----END PUBLIC KEY-----',
'oasMMVu5r1YbNMG+sI0/LgSTTG283WYO0vSQjl6gMVs=':
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkOVH/KT2QChe6pKxPHMF
...
IeiEH+8Q8Wev2gWfqdDCFMW4G3NoiDedaPCwIL9swj43X6oaJQ3QR0bk8ZIa7rGZ
XQIDAQAB
-----END PUBLIC KEY-----'
}
Verify Access from the JWT
Now we’ve prepped for verification, we can move on to the actual verification step itself. The first thing we need to do, is identify the specific PEM used in the token. If you recall, this is in the JWT header, so we simply decode the JWT and look for the kid field in the header. If we think back to how we’ve constructed this as a chain of promises, the parameter for this function needs to be the list of PEMs from the previous step. We also want to pass in our JWT (so we can get the kid), so we need to use a curried function to achieve this.
What we pass back from the function is the specific PEM we’ve found (wrapped in a promise):
// Now we need to decode our token, to find the verification key
function findVerificationKey(token) {
return (pemList) => {
let decoded = jwt.decode(token, {complete: true});
return Promise.resolve(pemList[decoded.header.kid]);
};
}
This gives us something like:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkOVH/KT2QChe6pKxPHMF
y/EmVjjBAZq9hdnvIDzbepFF0SXRfvcpvYp/eHF1wBXO8pZvsFC9PNpdYgAo/1jb
P2lPkqvkz3rysvwjMCwbMySfctCSFIH3qeE3awWTBp5+vOsmfrlQqCV/lIbsqp9d
uDcECEnD1Ow/KD/wxSRmDfdlBQqCCqve8YYQZX9RCDuj6PbwQINhqgxN4Whrj9XY
7bYGIln9K/uZK/Osc9fee+PgC17ElHpxsWmaIhWz9Iutc0cRLe10D7feazJwN+Ge
IeiEH+8Q8Wev2gWfqdDCFMW4G3NoiDedaPCwIL9swj43X6oaJQ3QR0bk8ZIa7rGZ
XQIDAQAB
-----END PUBLIC KEY-----
Now we’ve got our PEM, we can validate the token. This will check the signature, the timestamp, and the issuer:
// Verify our token
function verifyToken(token) {
return (pem) => {
let verified = jwt.verify(token, pem, verificationOptions);
return Promise.resolve(verified);
};
}
This function then passes out the payload from the JWT, and we know that if we get here, it has been verified:
{
sub: 'f283baed-e6e8-4723-ac0f-69443f8cf08c',
event_id: 'c0b91359-6688-11e8-be39-ed012137f487',
token_use: 'access',
scope: 'openid',
auth_time: 1527959817,
iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_MsHScNijB',
exp: 1527963417,
iat: 1527959817,
version: 2,
jti: '7e0aca62-0d7b-483a-ac23-2d597cb1349d',
client_id: '4ep3ec3eat8jq0qeb1bf2ftt7v',
username: 'ian'
}
If we want additional validation, we can apply that ourselves. Here we are checking that we have been given an access token, but if we had other properties in here, we could check them similarly:
// Check that we are using the token to establish access
function checkTokenUse(verifiedToken) {
if (verifiedToken.token_use === "access") {
return Promise.resolve(verifiedToken);
}
throw new Error("Expected access token, got: " + verifiedToken.token_use);
}
That’s all the pieces in place, and we can just invoke our top-level function:
// Run it!
validate(token, doSomething);
To make this useful externally, we need to export our validate()
function,
instead of calling it (so we can require
it when we want to use it).
// Make validation available externally
module.exports = validate;
If you want to take a look at the code discussed here, it’s in my Github repo:
ianfinch/jwt-parsing-example/tree/4cff3a4384223782d565d475dd5f72ec0708e011
The next article will demonstrate how we can combine the Cognito User Pool login with this validation code, in a NodeJS API server.