Feature/custom cfn resource for storing cognito jkws ssm #53
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Custom CloudFormation Resource that puts Cognito JSON Web Keys into SSM Parameter Store
Context
What is AWS Cognito? How does rootski use it?
Rootski personalizes the experience for users by having users sign up and log in.
Rather than write a user registration system ourselves, we use a service in AWS called
AWS Cognito. Cognito takes care of sign up, sign in, password recovery, deregistering users,
and enforcing strong passwords. Cognito even exposes a nice UI to do all these things:
When users successfully sign into rootski in the browser,
AWS Cognito grants the user an authorization token (a JWT token) which the browser
places in the headers of HTTP requests sent to the rootski backend.
When the rootski backend API receives an incoming request, it checks the JWT token in
the headers to verify that the request is authorized. To do this, rootski downloads a key
called a JSON Web Key (JWK) which it uses to decrypt the token and make sure that
the token came from the rootski AWS Cognito instance--not some hacker on the internet.
The JSON Web Keys for a Cognito User Pool can be fetched using this endpoint:
"https://cognito-idp.{cognito_aws_region}.amazonaws.com/{cognito_user_pool_id}/.well-known/jwks.json"This is what the JWKs look like.
{ "keys": [ { "alg": "RS256", "e": "AQAB", "kid": "vBU9jC18VYmhB09UOHVOChs9A15t/8+2TvAJkR6+gjk=", "kty": "RSA", "n": "sURVgjgib4xdf2b-bIDwotk3Qvph32D447PeDri5GtRBIxUBcMG_ODHqnBlq61adRptop1XlPAcYCjD6sWzpLbiCibrtuli0ZB2OyOU4ZTNnDPBlLavd17dxWqW7QN0lh1zBYaNvLiBbkVGIfwxsRcMrWiel3rTGQAYTIIuytuPvWqMdat0J86auN-vqkG7MoM460U9HsfwSfKt_GDmSgE8soO6BM7K2a80lww2qtTz-R--blwoTmB3nHLKL6X125OMWdELX6d7xrlRkXGItnRexgDsJKBpsxDTNPieD42mo26Qo3vZA77myaevg_YM53XuX8buwsMwxN5eUkdAUKQ", "use": "sig" }, { "alg": "RS256", "e": "AQAB", "kid": "lTFNZ2kMaKhtTYe+PyzGAnF3U1TcW0VKJtybTxkTwmE=", "kty": "RSA", "n": "1k3quCmSUFGJHR5IcuDoHv155h4yuWVe35J4PTDguL65yeSIVUrmcyc4S4pYcOiShLQn2kUuUgxqywCVa6aRZVMOXLct2Rcn8yd-jhaa_Tz21JHsODeRefbTHc9L39YebTCCydgN9EC4QNqnDC6xC4h7gqenSC9MU45aO1Sl74hwBMaW6QINcE5tSM671UWoRUXR8yjM6SBoX3qbJwITInEYKlFiKWywx7addhMw6ZyNaPpsfnM5StKIOwg2n-suhmDpq9jAkHe3MBuBL0s-W1nhamuDVsiBYNBvbc24XBStTPDjJRHpnM-_WdWuhxb0R7tJAB5mcSDCIucIOfGgPw", "use": "sig" } ] }What is the problem this PR is solving?
The backend rootski API runs in an isolated VPC network without internet
access. Due to this, the backend is not able to hit the public AWS endpoint
to fetch the JSON Web Keys used to validate JWT tokens issued by the rootski
Cognito User Pool.
However, the rootski API is able to access AWS SSM, including AWS SSM Parameter Store.
This lambda function defines a custom resource that performs an HTTP request to
the cognito URL to fetch the JWKs and then stores those in an SSM parameter.
Thanks to this, the rootski API code running in AWS Lambda can fetch the JWKs from SSM.
What is the solution proposed in this PR?
The rootski AWS Cognito User Pool is created using AWS CDK code. This code existed prior to this PR.
This PR creates a "custom CloudFormation resource" called
Custom::Rootski-CognitoJWKsInSSMthatis created using AWS CDK right after the Cognito User Pool.
The job of this custom resource is:
/rootski/cognito/jwks.jsonThat's what the PR implements! Here's the finished product:
Implementation
To create the
Custom::Rootski-CognitoJWKsInSSMresource, we needed to write an AWS Lambda Function that is invoked by CloudFormation.The concept is pretty straightforward: when you create your stack, CloudFormation sends a
Createevent to your Lambda function. The expectation is that when the Lambda function finishes running, the custom resource AKA our SSM Parameter containing JWKs will exist.When the CDK/CloudFormation stack is destroyed, CloudFormation sends a
Deleteevent to the lambda function. When this finishes, the resource should be fully deleted.The
CreateandDeleteevents are identical except for theRequestTypefield. Here is an exampleCreateevent payload that came up while testing the custom resource lambda for this PR:{ "RequestType": "Create", "ServiceToken": "arn:aws:lambda:us-west-2:091910621680:function:Cognito-JWKs-In-SSM-Param-SSMParameterWithCognitoJ-GGOkyUUvA1f4", "ResponseURL": "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-west-2%3A091910621680%3Astack/Cognito-JWKs-In-SSM-Parameter-Custom-Resource-CF/36444d20-c8e3-11ec-81f0-06fa347afb33%7CSSMParameterWithCognitoJWKs%7C471031d4-5160-496c-9f00-6126aa96ee3d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220501T001223Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7200&X-Amz-Credential=AKIA54RCMT6SJTABWA2S%2F20220501%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Signature=7b01a55a52ea74a68a3bd1075cfc25804bead538503a4f05c844005546325451", "StackId": "arn:aws:cloudformation:us-west-2:091910621680:stack/Cognito-JWKs-In-SSM-Parameter-Custom-Resource-CF/36444d20-c8e3-11ec-81f0-06fa347afb33", "RequestId": "471031d4-5160-496c-9f00-6126aa96ee3d", "LogicalResourceId": "SSMParameterWithCognitoJWKs", "ResourceType": "Custom::Rootski-CognitoJWKsInSSM", "ResourceProperties": { "ServiceToken": "arn:aws:lambda:us-west-2:091910621680:function:Cognito-JWKs-In-SSM-Param-SSMParameterWithCognitoJ-GGOkyUUvA1f4", "SSMParameterPath": "/rootski/cognito/jwks.json", "CognitoUserPoolId": "us-west-2_NMATFlcVJ", "CognitoUserPoolRegion": "us-west-2" } }High-level Table of Contents for the PR
cognito/resources/jwk_custom_resource_lambda/which contains the lambda functionSSMParameterWithCognitoJWKsStackwhich