Recently working with a client we had a new HPC grid being developed internally and authentication of the front end became an important subject; this particular client has multiple patterns defined within their enterprise architecture all with particular requirements that didn’t necessarily fit our design. The hurdles to implement the enterprise solutions would have significantly delayed our development timeline, and so the decision was made to do a custom authentication using JWT which followed enterprise patterns and left us able implement this without the enterprise product.
This article assumes you have some familiarity with JWT, Python, AWS, Lambda, Cloudformation, and API Gateway.
The project follows a sample blueprint hosted on GitHub: https://github.com/Eascen/api-gateway-jwt
Overview:
Our solution includes four components that make up the solution, with the blueprint available on GitHub:
- API Gateway: The AWS API Gateway hosting the solution
- Login: A lambda that generates the JWT using KMS and PyJWT
- Authorizer: The lambda the API Gateway calls when receiving a request from a protected lambda
- ExampleFunction: A simple Lambda function whose purposes is to expose an endpoint from which to test the Authorizer and Login functions
Breaking it down: Login
JWT works using cryptographic principals. In this particular example we will be using unencrypted AWS KMS Data Keys (envelope encryption) to sign the payload using HMAC-SHA256, passing the encrypted key with our payload. This provides us two major features:
- Our shared secret (Data Key) is unique to each login
- We offload encryption/decryption operations to AWS KMS
When a client hits a login endpoint it will need to provide the JWT authorization token which then the API Gateway passes to our custom Lambda to verify.
First thing we do is generate a new data key, and encode the encrypted data key to base64 to store with our JWT, while also storing the plaintext key to sign the JWT with:
Then, we set the expiration and sign the key using the un-encrypted data key:
Breaking it down 2: Authorization
When a client hits a login endpoint it will need to provide the JWT authorization token which then the API Gateway passes to our custom Lambda to verify.
Verification happens in two steps:
1. We have to extract the JWT Header which contains the encrypted Data Key, which will need to be decrypted with KMS before we can verify the JWT
2. Once the secret has been decrypted, we can then use it to verify that the JWT was signed with the key:
Any error with the key verification, or if it’s past it’s expiration results in an error we can handle by creating an deny policy for the API Gateway.
Putting it all together: Testing and verification
The sample project contains testing instructions using curl, following them we can successfully verify that a JWT is returned, and that we are able to access our generic Lambda:
And using an online JWT Decode available at jwt.io we can verify the key structure is as expected: