Verify and Decode Cognito JWT Tokens
Intro
Previously we have covered the process of retrieving JWT Tokens from the Cognito Token Endpoint.
📘 ncoughlin: AWS Cognito Notes
Next we need to decode the tokens to get the information inside, and then verify the signature of the tokens to ensure they are legitimate. There are libraries that exist just for this purpose, and because we are using Node as our environment we need to find a node library.
Resources
AWS Docs: Verifying a JSON Web Token
Github:auth0/node-jsonwebtoken
Understanding the JWK
Before we go forward with a complete example let's quickly make sure that we understand the theory behind verifying the signature of a JWT. As we know the JWT contains three sections.
- Header
- Body
- Signature
The signature is what we check to make sure that the token actually came from Cognito and not a malicious 3rd party conducting a man in the middle attack (MIM). We verify the signature by using a public encryption key that Cognito creates and provides for us (this is described in more detail in the docs linked above). One of the tricky things to understand about this however is that the public key is publicly available. Anyone with our user pool id could retrieve this key. So if anyone can retrieve our public key how do we know that a MIM attack isn't just using that key to create a fake signature on the JWT?
This is answered with a fundamental understanding of the difference between private and public keys.
- Public Keys: Can decode a signature
- Private Keys: Can decode and encode a signature
These keys are created mathematically in such a way that only the private key can encode something, while the public key can decode it. Therefore we can use the public key to verify that signature was created with the private, which only Cognito has access to.
So in short, there is no way for a MIM to fake a signature. A MIM could intercept and decode our signature, or even steal the credentials and log in to our API, but they could not fake a signature from Cognito.
The ability to possibly intercept our tokens is why 0Auth2 puts a time limit on access tokens, and why using secure connections is so important.
Decode the JWT Token
The node-jsonwebtoken library linked above has the ability to decode and verify the JWT token all in one method. That method takes the following format.
jwt.verify(token, secretOrPublicKey, [options, callback])
At this point in the process we have the token but we have not yet retrieved our Public Key.
Retrieve the Public Key
To retrieve the key we construct a URL with the following format:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Filling in the variables for your region and user pool id. Then to retrieve the keys we can simply make a Postman GET request to that address and response we get back will be two key objects in JSON format.
Only one of these keys is the one we want to use, so we need to find out which one matches the kid in the header of our token. We could use the node library to do this programmatically but we just need to do this once so you can also do this with jwt.io.
And we can see that it was the first key that we wanted. So I can go ahead and add that to my code and give it a variable.
// key to verify signature of JWT Tokens
const jwkPublicKey = {
alg: "RS256",
e: "AQAB",
kid: "tOicxu1dhFVKydyt+fjmZ0Iqu4tIkVTXBy1dRFtOVu8=",
kty: "RSA",
n: "lU6YHKuCXRTm1_WAlCMgP5g1WdiV3u8OKU6kNPZRKvWo11RVE46tDrMZncWC4iwj_GNoYvwJdvgVZYR9ka7erlfwlPI5sykQLMaYsRZjgznpMKuQqRzz_b4g0Ytffnv04UTYMckVlF-MR7b1P6X0m9CJGQK5QW3UctSJQb6-CYUptfaNip_hDjBFiTjqZqp8A9fbNG3mfn0u4bcgSiqRpWRsPC9vjKoYDCjxY_OGZINEKkaGIx9aSw8cCWGhQgCsLUPHrbiYRkEk55GLWXnMjluH-EHjSUejoB3XjUYsTBMzy1xGYw_pHUD5ZDxcv6TnMJDXJj1PjdFmwB0EO0F5jw",
use: "sig",
};
All this was a point of confusion for me before. When AWS docs said that I needed to retrieve the public key one time, I wasn't sure if that meant getting it with API requests once per session etc. But no, they do in fact mean ONE TIME.
If you are using Express or another framework that doesn't output a static SPA you would probably want to put this key in environment variables or something else. This project will need environment variables eventually just for development purposes but we aren't there yet. Therefore a variable is fine for now.
So at this point we have the two pieces that we need for the Github:auth0/node-jsonwebtoken library to decode and verify our JWT Token, so let's go ahead and do that.
jwt.verify()
Now let us construct a function that takes the JWT Tokens as an argument and then decodes and verifies the id_token.
var jwt = require("jsonwebtoken");
var jwkToPem = require("jwk-to-pem");
// verify signature and decode id_token
const verifyIDToken = (tokens) => {
//convert JWK keys to PEM format
var pem = jwkToPem(jwkPublicKey);
// verify id_token
var verified = jwt.verify(tokens.id_token, pem, { algorithms: ["RS256"] });
console.log("VERIFIED ID TOKEN");
console.log(verified);
};
You'll notice that we also had to use another small library there to convert the Token to PEM format, as the AWS Docs recommended, and we also had to specify the algorithm. However we can see that this worked.
There is however one small modification that we need to make to this. Currently the verify method is just using the signature to decode the contents and then checking to make sure that the token is not expired. We actually need to verify the claims inside of the contents. AWS instructs us to verify the audience, issuer and token use claims. To do that we add those items into the options of the verify method like so.
var jwt = require("jsonwebtoken");
var jwkToPem = require("jwk-to-pem");
// verify signature, decode id_token and verify claims
const verifyIDToken = (tokens) => {
//convert JWK keys to PEM format
var pem = jwkToPem(jwkPublicKey);
// verify id_token signature
var verified = jwt.verify(tokens.id_token, pem, {
algorithms: ["RS256"],
aud: "1q5o88b6enkv30u6gbvaueumb8",
iss: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_W1XNLNHNX",
token_use: "id",
});
return verified;
};
And we have safely retrieved our verified user information. Now because we are doing this with React-Redux we can save these objects in the store and then pass them down as props to our components to display user information. In addition we can save all of the Tokens in the store, as we will need their information to access our API.
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.