Skip to content

Conversation

@phitoduck
Copy link
Collaborator

@phitoduck phitoduck commented May 1, 2022

Custom CloudFormation Resource that puts Cognito JSON Web Keys into SSM Parameter Store

image

Sorry about the size of this PR. Typically PR's should be much smaller than this one.

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:

image

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.

We have a nice example of a rootski JWT token in the docs

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 Pull Request message in PR #52 goes into more depth explaining the problem approached in this PR. This PR specifically addresses the Cognito part of the problem.

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-CognitoJWKsInSSM that
is created using AWS CDK right after the Cognito User Pool.

The job of this custom resource is:

  1. fetch the JWKs for the cognito resource by hitting the HTTP endpoint
  2. save the JWKs into an AWS SSM Parameter called /rootski/cognito/jwks.json

That's what the PR implements! Here's the finished product:

image

Implementation

To create the Custom::Rootski-CognitoJWKsInSSM resource, 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 Create event 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 Delete event to the lambda function. When this finishes, the resource should be fully deleted.

The Create and Delete events are identical except for the RequestType field. Here is an example Create event 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

  • A python package at cognito/resources/jwk_custom_resource_lambda/ which contains the lambda function
  • A CDK Stack called SSMParameterWithCognitoJWKsStack which
    • registers the above lambda function as a custom resource
    • creates an instance of the custom resource

@codecov-commenter
Copy link

Codecov Report

Merging #53 (3875a6c) into trunk (bc345c2) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##            trunk      #53   +/-   ##
=======================================
  Coverage   73.70%   73.70%           
=======================================
  Files          42       42           
  Lines        1289     1289           
  Branches      183      183           
=======================================
  Hits          950      950           
  Misses        317      317           
  Partials       22       22           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bc345c2...3875a6c. Read the comment docs.

@phitoduck
Copy link
Collaborator Author

The docs fail to build for this PR. This is due to a need to bump the version of AWS CDK used in parts of the rootski infrastructure unrelated to this feature. The feature itself works and the tests pass, so we will fix the docs build in a later PR.

@phitoduck phitoduck merged commit 63bb1d2 into trunk May 2, 2022
@phitoduck phitoduck deleted the feature/custom-cfn-resource-for-storing-cognito-jkws-ssm branch May 2, 2022 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants