Skip to content

ge8/docaas-summit

Repository files navigation

SELF-SERVICE LAB: SECURING SAAS APPLICATIONS BUILT ON SERVERLESS MICROSERVICES

In this Lab, you'll crack open the IDE to secure a SaaS platform built on a ReactJS web app and NodeJS serverless microservices. The app uses Amazon API Gateway and Amazon Cognito to simplify the operation and security of the service's API and identity functionality. You'll enforce user isolation and data partitioning with OAuth's JWT tokens and IAM conditional policies. You'll also abstract the security complexity from developers to keep operational burden to a minimum, maximizing developer productivity, and maintaining a great developer experience.

INITIAL SET UP

AWS Account requirements

  • IAM user with admin policy and access keys.
  • A Route53 hosted zone with a domain e.g. docaas.net. Route 53 name servers should be the authoritative name servers for the domain.
  • An AWS ACM certificate in the us-east-1 region for the domain/sudomain name above and *. alias. (The ARN for this ACM Certificate will be later configured as the AcmCertificateArn parameter)
  • A Private S3 bucket for deployment purposes (will be later configured as the SAMBUCKET parameter)

Machine prerequisites (for linux/mac users):

  • VS Code (or IDE of choice) installed
  • (Optional) A REST client like Insomnia [https://insomnia.rest/] to test APIs
  • Install/Update Brew Link
  • Install/Update the AWS CLI with the IAM credentials above and the default region e.g. ap-southeast-2. Link
  • Configure the AWS CLI with the IAM user's access keys, your default region e.g. ap-southeast-2 and json as default output. Link
  • Install/Update the AWS SAM CLI Link
  • Three web browsers installed e.g. Google Chrome, Mozilla Firefox, and Safari or Edge

Lab Setup (for linux/mac/cloud9 users)

  • If you don't have an environment directory at the root, go create one (The Cloud 9 IDE includes this directory, but if you're using other linux or mac, you might need to create it)
cd ~
mkdir environment
  • Navigate to desktop folder and clone repo:
cd ~/environment
git clone https://github.com/ge8/docaas-summit
  • Open the folder ~/environment/docaas-summit in VS Code (or your IDE of choice)
  • Open load-variables.sh and set SAMBUCKET (S3 bucket name for deployment created above), REGION (the same default region configured on the AWS CLI e.g. ap-southeast-2).

  • Open template.yaml found in the backend directory, and set the parameters:
  1. DomainName as an non-existing subdomain for your domain above. You don't need to create a Route 53 record for this subdomain because the setup scripts below will create it for you. Fox example, if your domain is docaas.net, and lab.docaas.net isn't created, you can configure DomainName as lab.docaas.net
  2. AcmCertificateArn as the ARN of the ACM Certificate ARN created above.

  • From the ~/environment/docaas-summit directory, deploy the backend & app. This might take from 10 to 40 mins because Cloudfront takes that much (Go grab a cup of tea/coffee or play a Fortnite game while it deploys)
cd ~/environment/docaas-summit
./deploy-template.sh 
./deploy-app.sh

LABS

Lab 0: Check the app out at the DomainName you configured before

DoCaaS

Deck Of Cards as a Service is an online service that allows users to create virtual decks of cards, shuffle decks, deal 2-card games, etc. We have three user plans:

  1. Login into the app with three different users (gold1, silver1 and bronze1) by going to your DomainName using incognito browser sessions on three different browsers (this prevent caching issues with ReactJS). You will be prompted to change the password for a permanent one e.g. Permanent1!. For example:
  • Chrome, username: gold1, password: Temporary1! (Use Chrome for the gold1 user - you'll need this below)
  • Firefox, username: silver1, password: Temporary1!
  • Safari/Edge, username: bronze1, password: Temporary1!

  1. With some users, create and get a couple of decks. You need to type a deck name or number in the text field e.g. "111".

Note: you might experience slow APIs the first time to hit each API after a new deploy. This is due to Lambda cold starts For more info: jump to the "My deployment seems successful but now the app is slow" FAQ

  1. With some users, play a few games. Note that this 2-card game with perfectly ordered decks, makes no sense.

  1. With some users, shuffle a few decks and then get them and play games with the shuffled deck.

  • Note the Cut service won't work because it's misconfigured and you'll fix it as part of Lab 1.
  1. Using the app browser logged in as the gold user, open Developer Tools. In Chrome, you do this by either going to the Chrome menu > More Tools > Developer Tools (or simply using the keyboard shortcut: command+options+I). Go to the console tab and then try to cut a deck. You'll notice this fails and gives you a CORS error in the console. This is what happens when CORS isn't configured in your API: the browser will prevent you from accessing the API.

  1. At the top of the console logs, you'll see a long string of seemilgly random characters. This is the user's identity token - it's one of the three JWT tokens Link as part of the OAuth standard Link which is used by Open ID Connect Link identity providers like Amazon Cognito for our app.

Let's inspect this JWT token. Copy this token by copying it and pasting it at [https://jwt.io/].

Note that the token's signature is valid.

The payload of the identity token contains the meat of the token. You can add as many claims in payload as you want, such as iss (the identity provider that validated the token), cognito:username, email, etc. For SaaS apps, things like subscription status, or plan can be useful. The way you add more fields with Amazon Cognito is by creating custom attributes like we did with custom:plan.

This flexibility makes Open ID Connect identity providers like Amazon Cognito and SaaS apps are a great match because you get a lightweight and secure way all you microservices can get user context without having to pull information from different places.

On Lab 1, we will use the custom:plan found in the JWT token to control access to API resources.

  1. Open your IDE (AWS Cloud9, VS Code or other) Check out the ReactJS source code found in the frontend directory.

  2. Check out the backend source code found in the backend directory.

  • Note there are 9 AWS Lambda functions written in NodeJS - 7 of those are part of the microservices that serve our app, plus 2 Lambda functions for CORS and Lambda Authorizer (not in use yet - you'll use it in Lab 1)

  • Check out the SAM template called template.yaml found in the backend directory and see all the resources that are part of the CloudFormation stack.

Lab 1: Access Control

In this Lab, you'll improve the Access Control configuration of the application in two areas: CORS (Cross-Origin Resource Sharing) and Access Control to API resources.

CORS

Cross-Origin Resource Sharing Link is a security standard measure that needs to be implemented in some APIs in order to let web browsers access them. The implication of having of this misconfigured can be anywhere from having data stolen to having our entire application compromised. With CORS, browsers send an *options request to the API - and the API responds with permissions.

At the moment our application is proxing these options requests to a CORS-specific Lambda function and the Lambda response is hardcoded with a wildcard for origin that allows any computer in the world to access the APIs. We'll improve this in two ways: 1) Replacing the CORS Lambda function with Amazon API Gateway native support for CORS Link. This way we won't have to have a Lambda function for this. 2) Restricting origin permissions to our Subdomain name. Let's do it!

  1. Check out the CORS Lambda function definition in the SAM template template.yaml found in the backend directory. The syntax used here is part of the Serverless Appication Model (SAM) Link which makes it easier to create, manage and update Serverless resources like AWS Lambda functions, Amazon API Gateway APIs and Amazon DynamoDB tables.

  1. Check out the CORS Lambda function code cors.js found in the backend/src directory. Note that by having a wildcard '*', this API can be accessed by any origin from the interwebs.

Let's replace the CORS Lambda function with Amazon API Gateway native support for CORS.

  1. Open the SAM template template.yaml found in the backend directory. First, hide or remove the CORS Lambda function definition.

Then, reconfigure each of the 4 options methods (Create, Get, Game, Shuffle) found in the API Gateway Resources to use the MOCK type instead of the AWS_PROXY type. You can do this by simply hiding and unhiding the relevant sections of the template. Note that the new mocked CORS responses are only allowing the origin to be our subdomain.

Then, enable the entire options method definition for the Cut resource found last in the API Gateway Resources.

  1. Now you can remove the cors.js file from backend/src

CORS configuration is now properly congifured but before deploying changes, we'll improve the access control to API resources.

Access Control to API resources

API Gateway supports multiple mechanisms for controlling access to your API Link. At the moment we have our Cognito User Pool configured as the Authorizer. Although this is easy and completely managed by Cognito and API Gateway, it only allows a binary check: if the JWT tokens are valid (user is logged in), then it allows access to ALL API resources. A better approach would be to allow granular access to API resources based on the user plans. For example: silver users shouldn’t be able to access the Cut service. While Bronze users should only be able to access Create, Get and Game.

So, we're going to swap the authorizer from Cognito User Pool to a Lambda Authorizer. A Lambda Authorizer is an authorization option for API Gateway that allows us to inspect bearer token authentication methods (such as SAML or OAuth) and make access control decisions based on that.

This is how it works: API Gateway calls the Lambda function and supplies the JWT Tokens. The Lambda Authorizer runs your code (in this case we'll validate the JWT token and make access control decisions based on the custom:plan coming in the token's payload). Tha Lambda Authorizer returns the IAM policies that authorize that specific tokenalong with some context. If the returned policy is invalid or the permissions are denied, the API call does not succeed. For a valid policy, API Gateway caches the returned policy, associated with the incoming token over a configurable TTL. For allowed calls, API gateway embeds the context to downstream services.

Additionally, we'll use the context created by the Lambda Authorizer to embed all user information that our downstream microservices need in order operate without having to validate tokens or pull info from services like Amazon Cognito. This way we're abstracting the security complexity and maintaing a good developer experience from microservices developers.

  1. (Optional) use an REST client like Insomnia [https://insomnia.rest/] to see how the silver1 and the bronze1 users (using custom:plan=silver and custom:plan=bronze respectively) can access the Cut API resouce - which shouldn't be the case. Make sure you use the user's JWT token in the Authorization header

  1. Inspect the Lambda Authorizer code authoriser.js found in backend/src. This function is quite long. Note what we're doing in the three main parts of the handler exports.authorise_request: 1) Validating the JWT token. 2) Constructing the IAM Policy for the JWT token. 3) Constructing the context of the token.

  2. Open the SAM template template.yaml found in the backend directory and let's replace the AWS::ApiGateway::Authorizer type from Cognito User Pools to Token (this is a Lambda Authorizer). You can do this by simply hiding and unhiding the relevant sections of the template.

  1. Define the Lambda Authoriser Lambda function using SAM syntax and its permissions. You can do this by simply hiding and unhiding the relevant sections of the template.

  1. Finally we need to update the AuthorizationType for all 5 of our POST or GET methods so that they stop using Cognito (COGNITO_USER_POOLS) and start using our Lambda Authorizer (CUSTOM)

  1. Now we're ready to deploy all changes! This should take about 1 minute.
cd ~/environment/docaas-summit
./update-template.sh
  1. Check out the app and confirm everything is working. Note: you might experience slow APIs the first time to hit each API after a new deploy. This is due to Lambda cold starts For more info: jump to the "My deployment seems successful but now the app is slow" FAQ

  2. (Optional) use an REST client like Insomnia [https://insomnia.rest/] to see how the silver1 and the bronze1 users (using custom:plan=silver and custom:plan=bronze respectively) are now blocked from accessing the Cut API resouce thanks to the fine grained access control we implemented.

Lab 2: Data Partitioning

Now that Access Control is more secure, we want to secure access to our data. But, where is all our data? We have two independent data stores: one storing all users’ decks and one storing all users’ scores.

When you think about it, anyone with rightful access to these DynamoDB tables, could access anyone’s decks or scores. This is not great. For our use case of decks and scores, this doesn't sound too bad, but for a different use case you can be storing more sensitive information such as health information or financial records. Either way it's a best practice to only allow users access to their own items in DynamoDB and nothing else.

We'll fix this by implementing two things:

  1. A composite-key strategy in our DynamoDB tables by prepending all items partition keys are prepended with the Cognito Identity user ID (userid). This help identify which user owns which item.

  1. A nifty security feature by IAM that allows to have fine-grained access control to DynamoDB tables using conditions on IAM policies based on the key access patterns for DynamoDB. Link. In our case, we'll allow conditional access to items that are prepended with the user's own Cognito Identity ID.

Let's do it!

  1. Go to the AWS Console > DynamoDB > Tables and check out the structure for both the decks-master and scores-master tables. You'll notice on the Items tab that both tables are key value stores where the partition key is called id and is currently the number/name for the decks.

  1. Go to the AWS Console > IAM > Roles and serch for "CognitoAuthorized". You'll find the Cognito Authorized Role. This is the role given to all authorized users. You'll notice this role has a single IAM policy attached granting full access to DynamoDB. Not great.

Let's fix it. First, let's remove the full access to DynamoDB and add our conditional policy to the Cognito Authorized role.

  1. Open the SAM template template.yaml found in the backend directory and let's delete/hide the - "dynamodb:*" line found on the policy of the CognitoAuthorizedRole resource.

  1. On the same CognitoAuthorizedRole resource definition, add the conditional policy discussed before. You can do this by simply unhiding the relevant policy.

We need to make a couple of changes in our Lambda code to ensure the Cognito Identity ID will now be prepended to every read and write to both DynamoDB tables. This functionality is located in two libraries that our datastore microservices lambda use: one for the decks table and one for scores table.

  1. Open the library deck-dataAccess.js found at backend/src/common. Append the tenantID + "-" to both the create and read operations: initDeck() and getDeck(). You can do this by simply hiding and unhiding the relevant sections of the code: hide lines 29 and 45; unhide lines 30 and 46.

  1. Do the same as before with deck-dataAccess.js found at backend/src/common. You can do this by simply hiding and unhiding the relevant sections of the code: hide lines 7 and 33; unhide lines 6 and 32.

Now our Lambdas will be reading and writing items at decks-master and _scores-master- with the Cognito intentity ID prepended to the partition key!

  1. Now we're ready to deploy all changes! This should take about 1 minute.
cd ~/environment/docaas-summit
./update-template.sh
  1. (Optional) Check out what's inside the ./update-template.sh script found in the project root.

You'll notice we're using the SAM CLI Link: a command line tool that operates on an AWS SAM template and application code. With the AWS SAM CLI, you can invoke Lambda functions locally, create a deployment package for your serverless application, deploy your serverless application to the AWS Cloud, and so on. In this case, we're only using it to package and deploy. These two commands combined package all the artifacts for your CloudFormation stack and your Lambda code, uploads them to S3 and then triggers an update to your CloudFormation stack effectively updating both your Stack and your Lambda functions with only 2 commands!

One thing that the SAM CLI doesn't do yet with these two commands (feature request) is updating the API stage, so our ./update-template.sh script does precicely that at the end of the script.

  1. Play with the app in the browser. Make sure to at least create a deck and play a game. Notice the notification after creating the deck that now includes a much longer name. Note: you might experience slow APIs the first time to hit each API after a new deploy. This is due to Lambda cold starts For more info: jump to the "My deployment seems successful but now the app is slow" FAQ

  1. Go to the AWS Console > DynamoDB > Tables and check out the recently created items in both the decks-master and scores-master tables. You'll notice on the partition key id is much longer because it now includes the user's Cognito Identity ID prepended.

  1. Go to the AWS Console > IAM > Roles and serch for "CognitoAuthorized". You'll find the Cognito Authorized Role. This is the role given to all authorized users. You'll notice this role now has a two IAM policies attached. The first one no longer grants access to DynamoDB. The second one is our conditional IAM policy that will only allow users to access their own items on the DynamoDB tables.

Congratulations! You've significantly improved the security of our SaaS app by properly implementing access control and data partitioning. You've also abstracted the security complexity from microservice developers. This way developers can stay focused on shipping great quality software fast. You've now finished the Lab!

FAQS

Lab Solutions

If you get stuck or want to see or deploy the lab answers, we have those pre-configured in separate branches.

To view the soltion to Lab 1 (and discard all your changes):

git reset --hard HEAD && git clean --force -d
git checkout demo1

To view the soltion to Lab 2 (and discard all your changes):

git reset --hard HEAD && git clean --force -d
git checkout demo2

To deploy either of these solutions, simply run the update-template.sh command.

cd ~/environment/docaas-devlab
./update-template.sh

Want to experiment with the react app?

To deploy the app, run the deploy-app.sh command.

cd ~/environment/docaas-devlab
./deploy-app.sh

How to reset the lab

You can reset the lab at any time by running the following command:

cd ~/environment/docaas-devlab
git reset --hard HEAD && git clean --force -d
git checkout master
./reset-lab.sh

My deployment seems successful but now the app is slow

The first time you execute an AWS Lambda function, you may experience a couple of seconds of delay - this is called a "cold start". This only occurs the first time you use a Lambda function after creation, update or after a long period without use. For this app, a single "create" may cold-start up to 3 lambda functions, so you might need to way up to 10 seconds the first time to execute these functions. This lab doesn't intend to resolve cold starts. This is a great advanced re:Invent session that explains cold-starts and how to optimise your set up [https://www.youtube.com/watch?v=oQFORsso2go].

Some of the application APIs hit multiple lambdas in sequence, if they're all cold for example after a new deployment, you might experience up to 12 seconds of delay the first time you hit an API. After that first time, the response should be quick.

How do I log out of the app?

To log out you can close the browser. Make sure you're using incognito browser sessions and that you close all incognito browser sessions from the same browser (Chrome, Firefox). It would be better to have a sign out button but we haven't implemented it yet.

About

Repo for docaas-summit

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published