diff --git a/.DS_Store b/.DS_Store index eb4a248..bbe6bc0 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 961b7fc..93fd5c2 100755 --- a/README.md +++ b/README.md @@ -1,446 +1,18 @@ -# Module 5: Enabling 3rd party applications using OAuth 2.0 +# Building ADFS federation for your Web app using Amazon Cognito User Pools -In this module we will turn our Wild Rydes application into a platform, enabling third party developers to build new applications on top of our APIs. Working with third party developers makes it easier for us to open new markets and geographies as well as provide new functionality for our riders. +In this post, we show how to federate identities from Active Directory to authenticate users into your web app by leveraging AWS services. The main AWS service that we will leverage for this purpose is Amazon Cognito user pools. With Amazon Cognito user pools, you can seamlessly add user sign-up and sign-in to your mobile and web apps using a secure and scalable user directory. In addition, you can federate users from a SAML IdP with Amazon Cognito user pools, map these users to a user directory, and get standard authentication tokens from a user pool after the user authenticates with a SAML IdP. -You'll configure your Cognito User Pool from module #2 to enable OAuth 2.0 flows. Using OAuth, third party developers can build new client applications on top of your APIs. We will create a new method in the application's API that allows unicorns to list the rides they have given. This will open a new line of business for us, making it easy for third party developers to build applications that help unicorns manage their time and earnings. First, we will create the new method to list rides. Then, we will enable OAuth flows in our Cognito User Pool and deploy a sample client. +More specifically, we explain how to integrate Amazon Cognito User Pools, together with Active Directory Federation Services, to obtain JWT tokens in your web app that in turn can be used for downstream authentication. To demonstrate the end to end authentication flow we have created a simple REST API built on Amazon API Gateway. The REST API retrieves data from a DynamoDB table with the help of an AWS Lambda function. We will use those JWT tokens vended from user pools to authenticate to the REST API which is hosted on API Gateway. -![OAuth 2.0 3rd party app architecture](../images/oauth-architecture.png) -The diagram above shows how the component of the new third party application interact with our current Wild Rydes architecture. The web application is deployed in an S3 bucket. The application uses the Cognito User Pools built-in UI to start an implicit grant OAuth 2.0 flow and authenticate the user. Once the Unicorn user is authenticated, the client application receives an identity and access token for the Unicorn. Tokens for Unicorns include an additional `Unicorn` claim that gives them access to the new API. In API Gateway, a custom authorizer checks for the `Unicorn` claim in the JWT access token produced by Cognito and passes the unicorn name to the backend Lambda function. The backend Lambda function uses the unicorn name from the access token to query the rides table in DynamoDB. +![Blog architecture](../images/ADFS.png) -### Prerequisites - -This module depends on all of the previous four modules in the Wild Rydes workshop. To make it easier to get started, we have prepared a CloudFormation template that can launch the complete stack for you. If you have skipped the earlier modules, and deploying using the CloudFormation template, clone the aws-serverless-workshop repository to your local working environment. - -If you have previously created resources from modules #1 to #4 in your account, and would still like to start fresh with the CloudFormation template below, make sure you first follow the [cleanup steps](../9_CleanUp/). - -Region| Launch -------|----- -US East (N. Virginia) | [![Launch Modules 1, 2, 3, and 4 in us-east-1](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-us-east-1/WebApplication/5_OAuth/prerequisites.yaml) -US East (Ohio) | [![Launch Modules 1, 2, 3, and 4 in us-east-2](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-us-east-2/WebApplication/5_OAuth/prerequisites.yaml) -US West (Oregon) | [![Launch Modules 1, 2, 3, and 4 in us-west-2](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-us-west-2/WebApplication/5_OAuth/prerequisites.yaml) -EU (Frankfurt) | [![Launch Modules 1, 2, 3, and 4 in eu-central-1](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-eu-central-1/WebApplication/5_OAuth/prerequisites.yaml) -EU (Ireland) | [![Launch Modules 1, 2, 3, and 4 in eu-west-1](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-eu-west-1/WebApplication/5_OAuth/prerequisites.yaml) -EU (London) | [![Launch Modules 1, 2, 3, and 4 in eu-west-2](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-2#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-eu-west-2/WebApplication/5_OAuth/prerequisites.yaml) -Asia Pacific (Tokyo) | [![Launch Modules 1, 2, 3, and 4 in ap-northeast-1](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-ap-northeast-1/WebApplication/5_OAuth/prerequisites.yaml) -Asia Pacific (Seoul) | [![Launch Modules 1, 2, 3, and 4 in ap-northeast-2](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-2#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-ap-northeast-2/WebApplication/5_OAuth/prerequisites.yaml) -Asia Pacific (Sydney) | [![Launch Modules 1, 2, 3, and 4 in ap-southeast-2](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-ap-southeast-2/WebApplication/5_OAuth/prerequisites.yaml) -Asia Pacific (Mumbai) | [![Launch Modules 1, 2, 3, and 4 in ap-south-1](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/images/cloudformation-launch-stack-button.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-south-1#/stacks/new?stackName=wildrydes-webapp&templateURL=https://s3.amazonaws.com/wildrydes-ap-south-1/WebApplication/5_OAuth/prerequisites.yaml) - -The stack creation process will ask you for a **Website Bucket Name**, specify a unique name for your bucket such as **wildrydes-webapp-<username>**. - -#### Populate the rides database -After the stack created successfully, open the **Outputs** tab in the CloudFormation console. Copy the **WebsiteURL** output value and navigate to the page with a browser window. - -On the Wild Rydes website, click the **Giddy Up!** button and register a new user. Once you have received your verification code, navigate to the **verify.html** page of the website to submit your code. From the login page, use your new credentials to log into the website. Use the application to request a few unicorn rides, we will need the rides data later in this module. - - -### 1. Create the new List Rides Lambda function - -#### Background -AWS Lambda runs your code in response to an API request. In this step, you will create a new Lambda functions to answer unicorn requests to the list rides API. In the Wild Rydes application, we have mapped each API method to an independent Lambda function. You also have the option to group multiple API methods in a single Lambda function. To keep writing code with the libraries you are already familiar with, we have created two open source frameworks: [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) and [aws-serverless-java-container](http://github.com/awslabs/aws-serverless-java-container). - -Take a look at the code in the [listUnicornRides.js](./listUnicornRides.js) file. The Lambda function expects the current unicorn name to be present in the authorizer context of the event. Once the event is parsed, the function queries our DynamoDB rides table to extract all of the rows for the current unicorn. The field is set by the custom authorizer you'll create in the next step. - -#### High-Level Instructions -Use the AWS Lambda console to create a new Lambda function called **ListUnicornRides** that will process the API requests. Use the provided [listUnicornRides.js](./listUnicornRides.js?raw=1) example implementation for your function code. Just copy and paste from that file into the AWS Lambda console's editor. - -Make sure to configure your function to use the `WildRydesLambda` IAM role you created in module 2 of this workshop. - -
-Step-by-step instructions (expand for details)

- -1. Choose on **Services** then select **Lambda** in the Compute section. - -1. Choose **Create function**. - -1. Click the **Author from scratch** button at the top of the blueprint list. - -1. Enter **ListUnicornRides** in the **Name** field. - -1. Select **wildrydes/WildRydesLambda** from the **Existing Role** dropdown. - - ![Define handler and role screenshot](../images/lambda-handler-and-role.png) - -1. Click **Create function**. - -1. Select **Node.js 6.10** for the **Runtime**. - -1. Copy and paste the code from [listUnicornRides.js](./listUnicornRides.js?raw=1) into the code entry area. - - ![Create Lambda function screenshot](../images/create-list-rides-function.png) - -1. Leave the default of **index.handler** for the **Handler** field. - -1. Click **Save** at the top of the page. - -

- -### 2. Create the new custom authorizer Lambda function - -#### Background -Amazon API Gateway can leverage an AWS Lambda function to make authorization decisions. In order to support bearer tokens, such as JWT tokens, you can use custom authorizers. When configured with a custom authorizer, API Gateway invokes a Lambda function with the request token and context. The Lambda custom authorizer must return a policy that API Gateway can use to make the authorization decision for the entire API, not just the specific method that was called. To make the creation of custom authorizers easier, we have created JavaScript and Python blueprints that you can select from the Lambda console. These blueprints contain a utility object that simplifies policy generation. - -You can also return a set of key/value pairs that are appended to the request context values. The code for our custom authorizer is in the `ListUnicornAuthorizer` folder, open the folder and take a look at the `index.js` file to get an idea of how our custom authorizer works. To authorize access to our new list rides API we rely on a custom scope called `UnicornManager/unicorn` - this scope is automatically added to client tokens produced by the Unicorn Manager application. - -#### High-Level Instructions -Use the AWS Lambda console to create a new Lambda function called **ListUnicornAuthorizer** that will process incoming JWT bearer tokens. Upload the provided [ListUnicornAuthorizer.zip](./ListUnicornAuthorizer.zip) as the function code. The authorizer Lambda function relies on an environment variable called **`USER_POOL_ID`**, define this in the Lambda console and set the value of the WildRydes **Pool Id** from the Cognito console. - -Make sure to configure your function to use the **WildRydesLambda** IAM role you created in module 2 of this workshop. - -
-Step-by-step instructions (expand for details)

- -1. Choose on **Services** then select **Lambda** in the Compute section. - -1. Choose **Create function**. - -1. Click the **Author from scratch** button at the top of the blueprint list. - -1. Enter **ListUnicornAuthorizer** in the **Name** field. - -1. Select **wildrydes/WildRydesLambda** from the **Existing Role** dropdown. - -1. Click **Create function**. - -1. Change the **Code entry type** to **Upload a .ZIP file**. - -1. Select **Node.js 6.10** for the **Runtime**. - -1. Leave the default of **index.handler** for the **Handler** field. - -1. Click the **Upload** button and select the [ListUnicornAuthorizer.zip](./ListUnicornAuthorizer.zip) file in the current module folder. - -1. Expand the **Environment variables** section and declare a new variable called **USER_POOL_ID**. The value for the variable is the **Pool Id** for the WildRydes user pool, you can find the value in the Cognito console. - - ![Create Lambda function screenshot](../images/create-list-rides-authorizer-function.png) - - -1. Click **Save** at the top of the page. - -

- -### 3. Configure the new custom authorizer - -#### Background -Amazon API Gateway can leverage AWS Lambda functions to make authorization decision. This enables you to customize the business logic behind the scenes. API Gateway supports two type of custom authorizers: **Token authorizers** and **Request authorizers**. You can use Token authorizers when your authorization decision is purely based on the client's bearer token. Request authorizers give your Lambda function access to all of the request information except for the body. - -API Gateway can also receive context information from the custom authorizer and pass them to the backend service. In our application, the custom authorizer includes the `unicorn` property in the request context if the `UnicornManager` scope [is present in the token](./ListUnicornAuthorizer/index.js#L109). - -#### High-level Instructions -Open the API Gateway console and create a new authorizer in the **WildRydes** API we created in module #4. The authorizer should use the **ListUnicornAuthorizer** function we created in the previous step. You should configure the new authorizer as a **Token authorizer** and the token source should be the **Authorization** header. - -
-Step-by-step instructions (expand for details)

- -1. Open the **Services** menu and select **API Gateway** in the Application Services section. - -1. Open the **WildRydes** API in the left menu and select the **Authorizers** page. - - ![Open custom authorizers](../images/open-wild-rydes-authorizers.png) - -1. Click the button to **Create New Authorizer** at the top of the page. - -1. Enter **ListUnicornAuthorizer** as the **Name** and **Lambda** as the **Type**. - -1. Using the **Lambda Function** field, select your region and enter the **ListUnicornAuthorizer** Lambda function name. - -1. Leave the **Lambda Execution Role** field blank. Configured this way, the API Gateway console automatically sets the permissions on the Lambda function to allow the invocation. The console will ask you to confirm this action as you save the new authorizer settings. - -1. Select **Token** as the **Lambda Event Payload** and enter **Authorization** as the **Token Source**. - -1. Leave the default values in the **Authorization Caching** settings and click **Create** - - ![Create Custom Token Authorizer](../images/create-custom-token-authorizer.png) - -1. The API Gateway console asks you to confirm the new permissions on the Lambda function. Click **Grant & Create**. - -

- -### 4. Create the new API Gateway method - -#### Background -Following REST conventions, you will use a `GET` method on the `/ride` resource to list the rides. In the same fashion, if we wanted to extract data for a specific ride, we would create a new resource called `/ride/{rideId}` and use a `GET` method under this resource to extract the data for a specific ride. Take a look at the [REST Resource Naming Guide](https://restfulapi.net/resource-naming/). - -#### High-Level Instructions -In the API Gateway console, open the `WildRydes` API we created in module #4 and add a new **GET** method to the `/ride` resource. The method integration should be a **Lambda Proxy** integration to the **ListUnicornRides** function we created in step #1 of this module. Configure the new method to use the **ListUnicornAuthorizer** we created in the previous step for authorization. Once you have made the changes to the API resources, deploy the new configuration to the existing **prod** stage. - -
-Step-by-step instructions (expand for details)

- -1. Open the **Services** menu and select **API Gateway** in the Application Services section. - -1. Open the **WildRydes** API and, from the **Resources** page, select the `/ride` resource. - -1. Using the **Actions** dropdown menu in the **Resources** pane, select **Create Method**. - -1. Configure the new method as a **GET** and confirm the settings with the small checkmark button next to the dropdown. - -1. In the method integration settings screen, select **Lambda Function** as the **Integration Type**, check the **Use Lambda Proxy Integration** checkbox, then select your Lambda region and use **ListUnicornRides** (careful: NOT **ListUnicornAuthorizer**) as the function name. - - ![Configure List Rides integration](../images/list-rides-api-integration.png) - -1. Click **Save** and confirm the new permissions on the Lambda function by clicking **Ok** in the modal window. - -1. In the **Method Execution** screen, open the **Method Request** pane. - -1. Click on the pencil icon next to the **Authorization** settings to change the value and select the **ListUnicornAuthorizer** from the dropdown. - - ![Configure Custom Authorizer](../images/select-list-custom-authorizer.png) - -1. Click the checkmark icon next to the dropdown to save your changes. - -1. Using the **Actions** dropdown in the **Resources** pane, select **Deploy API**. - -1. In the deployment modal window, select the **prod** stage from the **Deployment stage** dropdown and then click **Deploy**. - -

- -### 5. Create S3 bucket for static website - -#### Background -Our new partner website, called Unicorn Manager, is also a static application hosted on Amazon S3. You can define who can access the content in your S3 buckets using a bucket policy. Bucket policies are JSON documents that specify what principals are allowed to execute various actions against the objects in your bucket. - -By default objects in an S3 bucket are available via URLs with the structure http://<Regional-S3-prefix>.amazonaws.com//. In order to serve assets from the root URL (e.g. /index.html), you'll need to enable website hosting on the bucket. This will make your objects available at the AWS Region-specific website endpoint of the bucket: .s3-website-.amazonaws.com - -Because our application interacts with Cognito via the OAuth 2.0 implicit flow, which requires a redirect, we need our website to use HTTPS. To have an HTTPS endpoint for an S3 static website, we can use a [CloudFront distribution](https://aws.amazon.com/cloudfront/). - -#### High-Level Instructions -Use the console or AWS CLI to create an Amazon S3 bucket. Keep in mind that your bucket's name must be globally unique across all regions and customers. We recommend using a name like `unicornmanager-firstname-lastname`. If you get an error that your bucket name already exists, try adding additional numbers or characters until you find an unused name. - -You will need to add a bucket policy to your new Amazon S3 bucket to let anonymous users view your site. By default your bucket will only be accessible by authenticated users with access to your AWS account. See [this example](http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-2) of a policy that will grant read only access to anonymous users. This example policy allows anyone on the Internet to view your content. The easiest way to update a bucket policy is to use the console. Select the bucket, choose the permission tab and then select Bucket Policy. - -Using the console, enable static website hosting. You can do this on the Properties tab after you've selected the bucket. Set `index.html` as the index document, and leave the error document blank. See the documentation on [configuring a bucket for static website hosting](https://docs.aws.amazon.com/AmazonS3/latest/dev/HowDoIWebsiteConfiguration.html) for more details. - -Using the CloudFront console, create a new Distribution for web content specifying the S3 static website URL as the origin domain and / as the path. Make sure that the distribution only accepts HTTPS requests and HTTP requests are redirected to the HTTPS url. - -
-Step-by-step instructions (expand for details)

- -1. In the AWS Management Console choose **Services** then select **S3** under Storage. - -1. Choose **+ Create Bucket** - -1. Provide a globally unique name for your bucket such as `unicornmanager-firstname-lastname`. - -1. Select the Region you've chosen to use for this workshop from the dropdown. - -1. Choose **Create** in the lower left of the dialog without selecting a bucket to copy settings from. - - ![Create bucket screenshot](../images/create-unicornmanager-bucket.png) - -1. Open the bucket you just created. - -1. Choose the **Permissions** tab, then click the **Bucket Policy** button. - -1. Enter the following policy document into the bucket policy editor replacing `[YOUR_BUCKET_NAME]` with the name of the bucket you created in section 1: - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::[YOUR_BUCKET_NAME]/*" - } - ] - } - ``` - - ![Update bucket policy screenshot](../images/update-bucket-policy.png) - -1. Choose **Save** to apply the new policy. You will see a warning indicating `This bucket has public access`. This is expected. - -1. Next, choose the **Properties** tab. - -1. Choose the **Static website hosting** card. - -1. Select **Use this bucket to host a website** and enter `index.html` for the Index document. Leave the other fields blank. - -1. Note the **Endpoint** URL at the top of the dialog before choosing **Save**. - -1. Click **Save** to save your changes. - - ![Enable website hosting screenshot](../images/enable-website-hosting-unicornmanager.png) - -1. Next, open the **CloudFront** console under the **Networking & Content Delivery**. - -1. In the **CloudFront Distributions** page, click **Create Distribution**. - -1. For the delivery method, under **Web** section, click **Get Started**. - -1. In the **Origin Domain Name** field, paste the URL for the S3 static website we just created and **/** as the origin path. **Do not select the bucket from dropdown list, paste the full website url including the http:// prefix. The origin type should be `custom`, not `s3`**. - -1. In the **Viewer Protocol Policy** make sure that **Redirect HTTP to HTTPS** is selected. - - ![Create CloudFront distribution](../images/create-cloudfront-distribution.png) - -1. Under **Distrubution Settings** for **Price Class**, select **Use Only US, Canada and Europe**. - -1. Click **Create Distribution** at the bottom of the page. - -1. Creating a global distribution can take some time. Let CloudFront do its work in the background and move on the next step. We will come back to get the distribution endpoint at a later step. - -

- -### 6. Declare a new client application - -#### Background -Amazon Cognito User Pools allows you to declare multiple client applications that can interact with your pool. This includes both applications you own and apps by third party developers. Each application is identified by an application id and client secret. Cognito User Pools also offers a hosted login UI that supports the most common user operations such as registration, login, reset passwords, and MFA. You can also customize the look and feel of the hosted UI. - -#### High-Level Instructions -Using the Cognito console, add a new client application called **UnicornManager**. Because the client application is a static website hosted on S3 and written in JavaScript, we do **not** need a client secret. Next, in the App Integration section of the Cognito console, configure a domain name prefix for your hosted login UI. We called this **WildRydes-<username>**. - -
-Step-by-step instructions (expand for details)

- -1. In the AWS Management Console choose **Services** then select **Cognito** under Mobile. - -1. In the intro page, click **Manage your User Pools** an open the **WildRydes** pool. - -1. Open the **App clients** from the **General settings** menu on the left. - -1. Click **Add another app client**. - -1. Enter **UnicornManager** as the **App client name** and uncheck the **Generate client secret** checkbox. - - ![Create bucket screenshot](../images/create-cognito-app-client.png) - -1. Click **Create app client**. - -1. Open the **Domain name** configuration page. - -1. Specify a unique custom domain name, for example **wildrydes-sapessi**. - -1. Make sure that the domain name is available and then click **Save changes**. -

- -### 7. Create the Unicorns scope in the Cognito User Pool - -#### Background -Amazon Cognito User Pools lets you declare custom resource servers. Custom resource servers have a unique identifier - normally the server uri - and can declare custom scopes. You can allow custom applications to request scopes in your user pools. When users authenticate with these applications, the Cognito hosted UI takes care of authenticating the user and authorizing the action. Custom claims are automatically added to the JWT access token. - -#### High-Level Instructions -Using the Cognito console, open the **WildRydes** User Pool and create a new custom resource server called **UnicornServer**. The **UnicornServer** should use **UnicornManager** as the **Identifier** and allow the **unicorn** scope. - -
-Step-by-step instructions (expand for details)

- -1. Open the **Services** menu and select **Cognito** in the Mobile section. - -1. In the main screen, select **Manage your User Pools**. - -1. Open the **WildRydes** pool and select **Resource Servers** under **App integration**. - - ![Open resource servers](../images/cognito-resource-servers-menu.png) - -1. In the resource servers screen, click **Add a resource server**. - -1. Specify **UnicornServer** as the **Name**. - -1. Use **UnicornManager** as the **Identifier** for the custom resource server. - -1. In the **Scopes** section, declare a new scope called **unicorn**. I've used "**Allow listing of rides for unicorns**" as the description. - - ![Configure Cognito Resource Server](../images/configure-cognito-resource-server.png) - -1. Click **Save changes** to create your new custom resource server. -

- -### 8. Configure the new app client for OAuth - -#### Background -Amazon Cognito User Pools supports the authorization code grant, implicit, and client credentials grants. Third party developers can load the Cognito hosted UI with their application ID and request any of the enabled flows. Cognito User Pools also exposes a set of client and server/admin APIs that you can use to build custom authentication flows. As a result of a successful authentication Cognito produces and OpenID Connect-compatible identity token and a JWT access token. The access token includes the custom scopes you declared for the application. - -In our example, we will use the implicit flow for the sake of simplicity. Implicit grant flows are mostly used by mobile applications. For web applications, you would normally require third party developers to host their own backend service and use the authorization code grant flow. - -#### High-Level Instructions -Open the **App client settings** and configure the **UnicornManager** app to use **Cognito User Pool** as an identity provider and allow the **Implicit grant** flow. Make sure the application has access to the **custom scope** we created in step #7. As a callback URL, use the CloudFront distribution endpoint we created in step #5. The callback url will look like this: `https://xxxxxxxxxxx.cloudfront.net`. - -
-Step-by-step instructions (expand for details)

- -1. In the AWS Management Console choose **Services** then select **Cognito** under Mobile. - -1. In the intro page, click **Manage your User Pools** an open the **WildRydes** pool. - -1. Open the **App clients settings** from the **App integration** menu on the left. This page lists both the app clients declared for your user pool. Make sure you make the following changes only to the **UnicornManager** client app. - -1. Select **Cognito User Pool** as an identity provider for the app client. - -1. Enable the **Implicit grant** OAuth flow and allow the **UnicornManager/unicorn** custom scope. - -1. In the **Callback** and **Signout** URLs, specify the HTTPS CloudFront distribution endpoint adding **https://** at the beginning and **/** at the end: - 1. You can find the distribution endpoint in the **CloudFront** console. - 1. Select the distribution we created in step #5. - 1. In the **General** tab, copy the value for **Domain name**. - - ![Create bucket screenshot](../images/configure-cognito-app-client.png) - -1. Click **Save changes**. - -

- -### 9. Configure and upload the Unicorn Manager application to S3 - -#### Background -The last step is to configure the client code with the new Cognito application id and upload to our S3 bucket. - -#### High-Level Instructions -Open the `config.js` file in the **UnicornManager** folder, replace the `userPoolClientId` with the new UnicornManager application id from Cognito, set the region and the domain prefix we configured in step #6. Finally, copy the **WildRydesApiInvokeUrl** value from the prerequisites CloudFormation stack output into the **invokeUrl** property of the config file. Save and close the file. - -Upload the content of the **UnicornManager** folder to the root of your S3 bucket. You can use the AWS Management Console (requires Google Chrome browser) or the AWS CLI to complete this step. If you already have the AWS CLI installed and configured on your local machine, we recommend using that method. Otherwise, use the console if you have the latest version of Google Chrome installed. - -
-CLI step-by-step instructions (expand for details)

-1. With a file manager, navigate to the folder where the lab content is located and open the **UnicornManager** directory from the **WebApplication/5_OAuth/** folder. - -1. Open a terminal window and navigate to the folder where the material for this workshop is located. Navigate to the `WebApplication/5_OAuth/UnicornManager` folder. - -1. Open the **js** folder. - -1. Using your preferred text editor, open the **config.js** file. - -1. From the Cognito User Pools console, copy the client app id for the **UnicornManager** application as the value of the **userPoolClientId** property. You can find the application id in the **App clients** menu of the Cognito console. - -1. Change the value of the **region** property to the region you are using for this workshop. For example, I'm using **us-east-2**. - -1. Still in the Cognito User Pools console, open the **Domain name** page and copy the custom prefix in the value for the **authDomainPrefix** property. In our sample, this was `wildrydes-sapessi`. - -1. Finally, open the CloudFormation console and select the pre-requisites stack we created at the beginning of this lab. With the stack selected, use the bottom section of the window to open the **Outputs** tab. Copy the value of the **WildRydesApiInvokeUrl** output variable to the **invokeUrl** property - this value should look like this: `https://xxxxxxxxx.execute-api.xx-xxxxx-x.amazonaws.com/prod` - -1. Next, we need to copy the files we just modified to the S3 bucket that hosts our static website. We created the bucket in step #5 of this lab and it should be called **unicornmanager-<username>**. You can use the AWS CLI or the management console with a compatible browser to upload the files. -##### AWS CLI - -1. With a terminal, navigate to the **UnicornManager** directory in the lab material folder. - -1. Run the following command: - - ``` - aws s3 sync . s3://YOUR_BUCKET_NAME --region YOUR_BUCKET_REGION - ``` -##### AWS Console - -1. Open the **S3** console and select the Unicorn Manager bucket. - -1. In the **Overview** tab, click the **Upload** button. - -1. From a file browser window, select all of the files in the **UnicornManager** folder and drag them to S3's upload window. -

- -### Testing the application -Before we open the web page for the new Unicorn Manager application, we need to create a user for our unicorn. Using the **DynamoDB** console, open the **Tables** page and select the **Rides** table. In the **Items** tab, refresh the list of rides. Take the most common unicorn name from the **UnicornName** field and copy the value. - -Next, open the unicorn manager application by navigating to the CloudFront distribution domain we created in step #5 - the domain should look like this: **xxxxxxxxxxxx.cloudfront.net**. The application detects that we are not logged in an automatically redirects us to the Cognito hosted login page. On the login page, use the **Sign up** link at the bottom of the form. - -In the Sign up page, use the **UnicornName** value we copied from the DynamoDB table as the username, a valid email address, and create a password for the user. With most email addresses you can use a suffix preceded by **+** to create custom addresses. For example, you could sign up with **youremail+unicorn@emaildomain.com**. - -![Sign up unicorn screenshot](../images/user-pool-unicorn-signup.png) - -Click **Sign up** to create the unicorn account. The hosted registration ui will ask you for the verification code, you should have received this code via email. Paste the verification code in the form and click **Confirm account**. - -Once the account is confirmed, the application will redirect you to the main web page of the Unicorn manager. Use the **Refresh** button on the top right to load a list of the rides for the unicorn you registered. - -We have now turned **Wild Rydes** into a platform. Third party developers can now ask us for a new client app id, use our hosted UI to authenticate and register new users. This will allow us to grow our customer base and toolkit beyond what our team can produce by itself, **UnicornManager** is just the first step. +The details of the flow above are as follows: +1. The app starts the sign-up and sign-in process by directing your user to the UI hosted by AWS. A mobile app can use web view to show the pages hosted by AWS. +2. User Pool determines the appropriate IdP based on your configuration. For ADFS the IdP is determined by the metadata file or metadata endpoint URL from your SAML IdP. For example, if you use Microsoft Active Directory Federation Service (AD FS), the metadata URL looks like: https:///FederationMetadata/2007-06/FederationMetadata.xml +3. Your user is redirected to the identity provider. +4. The IdP authenticates the user if necessary. If the IdP recognizes that the user has an active session, the IdP skips the authentication to provide a single sign-in (SSO) experience. +5. The IdP POSTs the SAML assertion to the Amazon Cognito service. +6. The user's profile is created within Amazon Cognito User Pools. +7. After verifying the SAML assertion and collecting the user attributes (claims) from the assertion, Amazon Cognito returns OIDC tokens (id, access and refresh tokens) to the app for the now signed-in user. +8. We make a GET request to the API Gateway. In the Authorization header of the GET request we use the id token. On the API Gateway side we have a Cognito Authorizer that will validate the id JWT token. diff --git a/images/ADFS.png b/images/ADFS.png new file mode 100644 index 0000000..59a8b59 Binary files /dev/null and b/images/ADFS.png differ diff --git a/prerequisites.yaml b/prerequisites.yaml deleted file mode 100755 index fb4bdbc..0000000 --- a/prerequisites.yaml +++ /dev/null @@ -1,695 +0,0 @@ ---- -AWSTemplateFormatVersion: "2010-09-09" - -Description: - Deploys the Wild Rydes workshop - -Parameters: - BucketName: - Type: String - Description: The name for the bucket hosting your website, e.g. 'wildrydes-yourname' - - CodeBucket: - Type: String - Default: wildrydes-us-east-1 - Description: S3 bucket containing the code deployed by this template - -Metadata: - AWS::CloudFormation::Interface: - ParameterGroups: - - - Label: - default: "Website Configuration" - Parameters: - - BucketName - - - Label: - default: "Advanced Configuration" - Parameters: - - CodeBucket - ParameterLabels: - BucketName: - default: "Website Bucket Name" - -Resources: - WebsiteBucket: - Properties: - BucketName: !Ref BucketName - WebsiteConfiguration: - IndexDocument: index.html - Type: "AWS::S3::Bucket" - - WebsiteBucketPolicy: - Properties: - Bucket: !Ref WebsiteBucket - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: "*" - Action: s3:GetObject - Resource: !Sub "arn:aws:s3:::${WebsiteBucket}/*" - Type: "AWS::S3::BucketPolicy" - - WebsiteContent: - Properties: - ServiceToken: !GetAtt CopyS3ObjectsFunction.Arn - SourceBucket: !Ref CodeBucket - SourcePrefix: "WebApplication/1_StaticWebHosting/website/" - Bucket: !Ref WebsiteBucket - Type: "Custom::S3Objects" - - S3CopyRole: - Type: AWS::IAM::Role - Properties: - - Path: /wildrydes/ - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - Policies: - - - PolicyName: S3Access - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: AllowLogging - Effect: Allow - Action: - - "logs:CreateLogGroup" - - "logs:CreateLogStream" - - "logs:PutLogEvents" - Resource: "*" - - - Sid: SourceBucketReadAccess - Effect: Allow - Action: - - "s3:ListBucket" - - "s3:GetObject" - Resource: - - !Sub "arn:aws:s3:::${CodeBucket}" - - !Sub "arn:aws:s3:::${CodeBucket}/WebApplication/1_StaticWebHosting/*" - - - Sid: DestBucketWriteAccess - Effect: Allow - Action: - - "s3:ListBucket" - - "s3:GetObject" - - "s3:PutObject" - - "s3:PutObjectAcl" - - "s3:PutObjectVersionAcl" - - "s3:DeleteObject" - - "s3:DeleteObjectVersion" - - "s3:CopyObject" - Resource: - - !Sub "arn:aws:s3:::${WebsiteBucket}" - - !Sub "arn:aws:s3:::${WebsiteBucket}/*" - - CopyS3ObjectsFunction: - Properties: - Description: Copies objects from a source S3 bucket to a destination - Handler: index.handler - Runtime: python2.7 - Role: !GetAtt S3CopyRole.Arn - Timeout: 120 - Code: - ZipFile: | - import os - import json - import cfnresponse - - import boto3 - from botocore.exceptions import ClientError - client = boto3.client('s3') - - import logging - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - def handler(event, context): - logger.info("Received event: %s" % json.dumps(event)) - source_bucket = event['ResourceProperties']['SourceBucket'] - source_prefix = event['ResourceProperties'].get('SourcePrefix') or '' - bucket = event['ResourceProperties']['Bucket'] - prefix = event['ResourceProperties'].get('Prefix') or '' - - result = cfnresponse.SUCCESS - - try: - if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': - result = copy_objects(source_bucket, source_prefix, bucket, prefix) - elif event['RequestType'] == 'Delete': - result = delete_objects(bucket, prefix) - except ClientError as e: - logger.error('Error: %s', e) - result = cfnresponse.FAILED - - cfnresponse.send(event, context, result, {}) - - - def copy_objects(source_bucket, source_prefix, bucket, prefix): - paginator = client.get_paginator('list_objects_v2') - page_iterator = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix) - for key in {x['Key'] for page in page_iterator for x in page['Contents']}: - dest_key = os.path.join(prefix, os.path.relpath(key, source_prefix)) - if not key.endswith('/'): - print 'copy {} to {}'.format(key, dest_key) - client.copy_object(CopySource={'Bucket': source_bucket, 'Key': key}, Bucket=bucket, Key = dest_key) - return cfnresponse.SUCCESS - - def delete_objects(bucket, prefix): - paginator = client.get_paginator('list_objects_v2') - page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix) - objects = [{'Key': x['Key']} for page in page_iterator for x in page['Contents']] - client.delete_objects(Bucket=bucket, Delete={'Objects': objects}) - return cfnresponse.SUCCESS - - - Type: AWS::Lambda::Function - - UserPool: - Type: AWS::Cognito::UserPool - Properties: - UserPoolName: WildRydes - AliasAttributes: - - email - AutoVerifiedAttributes: - - email - Schema: - - AttributeDataType: String - Name: email - Required: true - - UserPoolClient: - Type: AWS::Cognito::UserPoolClient - Properties: - ClientName: WildRydesWeb - UserPoolId: !Ref UserPool - GenerateSecret: false - - UpdateUserPoolConfig: - Properties: - ServiceToken: !GetAtt UpdateCognitoConfigFunction.Arn - UserPool: !Ref UserPool - Client: !Ref UserPoolClient - Region: !Ref "AWS::Region" - Bucket: !Ref WebsiteBucket - Type: "Custom::CognitoConfigFile" - - UpdateConfigRole: - Type: AWS::IAM::Role - Properties: - - Path: /wildrydes/ - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - Policies: - - - PolicyName: CognitoConfig - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Sid: Logging - Effect: Allow - Action: - - "logs:CreateLogGroup" - - "logs:CreateLogStream" - - "logs:PutLogEvents" - Resource: "*" - - - Sid: Cognito - Effect: Allow - Action: - - "cognito-idp:CreateUserPool" - - "cognito-idp:DeleteUserPool" - - "cognito-idp:CreateUserPoolClient" - - "cognito-idp:DeleteUserPoolClient" - Resource: "*" - - - Sid: ConfigBucketWriteAccess - Effect: Allow - Action: - - "s3:PutObject" - - "s3:PutObjectAcl" - - "s3:PutObjectVersionAcl" - Resource: - - !Sub "arn:aws:s3:::${WebsiteBucket}/*" - - UpdateCognitoConfigFunction: - Properties: - Description: Copies objects from a source S3 bucket to a destination - Handler: index.handler - Runtime: python2.7 - Role: !GetAtt UpdateConfigRole.Arn - Timeout: 120 - Code: - ZipFile: | - import json - import boto3 - import cfnresponse - - s3 = boto3.resource('s3') - - def create(properties, physical_id): - userPoolId = properties['UserPool'] - clientId = properties['Client'] - region = properties['Region'] - bucket = properties['Bucket'] - - object = s3.Object(bucket, 'js/config.js') - config_content = """ - var _config = { - cognito: { - userPoolId: '%s', // e.g. us-east-2_uXboG5pAb - userPoolClientId: '%s', // e.g. 25ddkmj4v6hfsfvruhpfi7n4hv - region: '%s', // e.g. us-east-2 - }, - api: { - invokeUrl: 'Base URL of your API including the stage', // e.g. https://rc7nyt4tql.execute-api.us-west-2.amazonaws.com/prod' - } - }; - """ - config_content = config_content % (userPoolId, clientId, region) - print "Writing config content: %s" % config_content - print "Writing to bucket: %s" % bucket - config = s3.Object(bucket,'js/config.js') - config.put(Body=config_content) - return cfnresponse.SUCCESS, None - - def update(properties, physical_id): - return create(properties, physical_id) - - def delete(properties, physical_id): - return cfnresponse.SUCCESS, physical_id - - def handler(event, context): - print "Received event: %s" % json.dumps(event) - - status = cfnresponse.FAILED - new_physical_id = None - - try: - properties = event.get('ResourceProperties') - physical_id = event.get('PhysicalResourceId') - - status, new_physical_id = { - 'Create': create, - 'Update': update, - 'Delete': delete - }.get(event['RequestType'], lambda x, y: (cfnresponse.FAILED, None))(properties, physical_id) - except Exception as e: - print "Exception: %s" % e - status = cfnresponse.FAILED - finally: - cfnresponse.send(event, context, status, {}, new_physical_id) - - Type: AWS::Lambda::Function - DependsOn: WebsiteContent - - RidesTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: Rides - AttributeDefinitions: - - - AttributeName: RideId - AttributeType: S - KeySchema: - - - AttributeName: RideId - KeyType: HASH - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - - RequestUnicornExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: WildRydesLambda - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - "sts:AssumeRole" - Path: "/wildrydes/" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - - PolicyName: PutRidePolicy - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Action: - - dynamodb:PutItem - - dynamodb:Scan - Resource: !GetAtt RidesTable.Arn - - RequestUnicornFunction: - Type: AWS::Lambda::Function - Properties: - FunctionName: RequestUnicorn - Runtime: nodejs6.10 - Role: !GetAtt RequestUnicornExecutionRole.Arn - Timeout: 5 - MemorySize: 128 - Handler: index.handler - Code: - ZipFile: > - const randomBytes = require('crypto').randomBytes; - - const AWS = require('aws-sdk'); - - const ddb = new AWS.DynamoDB.DocumentClient(); - - const fleet = [ - { - Name: 'Bucephalus', - Color: 'Golden', - Gender: 'Male', - }, - { - Name: 'Shadowfax', - Color: 'White', - Gender: 'Male', - }, - { - Name: 'Rocinante', - Color: 'Yellow', - Gender: 'Female', - }, - ]; - - exports.handler = (event, context, callback) => { - if (!event.requestContext.authorizer) { - errorResponse('Authorization not configured', context.awsRequestId, callback); - return; - } - - const rideId = toUrlString(randomBytes(16)); - console.log('Received event (', rideId, '): ', event); - - // Because we're using a Cognito User Pools authorizer, all of the claims - // included in the authentication token are provided in the request context. - // This includes the username as well as other attributes. - const username = event.requestContext.authorizer.claims['cognito:username']; - - // The body field of the event in a proxy integration is a raw string. - // In order to extract meaningful values, we need to first parse this string - // into an object. A more robust implementation might inspect the Content-Type - // header first and use a different parsing strategy based on that value. - const requestBody = JSON.parse(event.body); - - const pickupLocation = requestBody.PickupLocation; - - const unicorn = findUnicorn(pickupLocation); - - recordRide(rideId, username, unicorn).then(() => { - // You can use the callback function to provide a return value from your Node.js - // Lambda functions. The first parameter is used for failed invocations. The - // second parameter specifies the result data of the invocation. - - // Because this Lambda function is called by an API Gateway proxy integration - // the result object must use the following structure. - callback(null, { - statusCode: 201, - body: JSON.stringify({ - RideId: rideId, - Unicorn: unicorn, - UnicornName: unicorn.Name, - Eta: '30 seconds', - Rider: username, - }), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - }).catch((err) => { - console.error(err); - - // If there is an error during processing, catch it and return - // from the Lambda function successfully. Specify a 500 HTTP status - // code and provide an error message in the body. This will provide a - // more meaningful error response to the end client. - errorResponse(err.message, context.awsRequestId, callback) - }); - }; - - // This is where you would implement logic to find the optimal unicorn for - // this ride (possibly invoking another Lambda function as a microservice.) - // For simplicity, we'll just pick a unicorn at random. - - function findUnicorn(pickupLocation) { - console.log('Finding unicorn for ', pickupLocation.Latitude, ', ', pickupLocation.Longitude); - return fleet[Math.floor(Math.random() * fleet.length)]; - } - - function recordRide(rideId, username, unicorn) { - return ddb.put({ - TableName: 'Rides', - Item: { - RideId: rideId, - User: username, - Unicorn: unicorn, - UnicornName: unicorn.Name, - RequestTime: new Date().toISOString(), - }, - }).promise(); - } - - function toUrlString(buffer) { - return buffer.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - } - - function errorResponse(errorMessage, awsRequestId, callback) { - callback(null, { - statusCode: 500, - body: JSON.stringify({ - Error: errorMessage, - Reference: awsRequestId, - }), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - } - - WildRydesApi: - Type: AWS::ApiGateway::RestApi - Properties: - Name: WildRydes - Body: - swagger: 2.0 - info: - version: 1.0.0 - title: WildRydes - paths: - /ride: - post: - description: Requests a new ride - consumes: - - application/json - produces: - - application/json - security: - - CognitoAuthorizer: [] - responses: - "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - type: "string" - x-amazon-apigateway-integration: - responses: - default: - statusCode: 200 - responseParameters: - method.response.header.Access-Control-Allow-Origin: "'*'" - uri: - Fn::Join: - - "" - - - "arn:aws:apigateway:" - - !Ref AWS::Region - - ":lambda:path/2015-03-31/functions/" - - !GetAtt RequestUnicornFunction.Arn - - "/invocations" - passthroughBehavior: "when_no_match" - httpMethod: "POST" - contentHandling: "CONVERT_TO_TEXT" - type: "aws_proxy" - options: - responses: - "200": - description: "200 response" - schema: - $ref: "#/definitions/Empty" - headers: - Access-Control-Allow-Origin: - type: "string" - Access-Control-Allow-Methods: - type: "string" - Access-Control-Allow-Headers: - type: "string" - x-amazon-apigateway-integration: - responses: - default: - statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'" - method.response.header.Access-Control-Allow-Origin: "'*'" - requestTemplates: - application/json: "{\"statusCode\": 200}" - passthroughBehavior: "when_no_match" - type: "mock" - securityDefinitions: - CognitoAuthorizer: - type: "apiKey" - name: Authorization - in: header - x-amazon-apigateway-authtype: cognito_user_pools - x-amazon-apigateway-authorizer: - providerARNs: - - Fn::Join: - - "" - - - "arn:aws:cognito-idp:" - - !Ref AWS::Region - - ":" - - !Ref AWS::AccountId - - ":userpool/" - - !Ref UserPool - type: "cognito_user_pools" - - - WildRydesApiDeployment: - Type: AWS::ApiGateway::Deployment - Properties: - Description: Prod deployment for wild Rydes API - RestApiId: !Ref WildRydesApi - StageName: prod - - WildRydesFunctionPermissions: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref RequestUnicornFunction - Principal: apigateway.amazonaws.com - SourceArn: - Fn::Join: - - "" - - - "arn:aws:execute-api:" - - !Ref AWS::Region - - ":" - - !Ref AWS::AccountId - - ":" - - !Ref WildRydesApi - - "/*" - - UpdateApiConfig: - Type: "Custom::ApiConfigFile" - Properties: - ServiceToken: !GetAtt UpdateApiConfigFunction.Arn - Bucket: !Ref WebsiteBucket - InvokeUrl: - Fn::Join: - - "" - - - "https://" - - !Ref WildRydesApi - - ".execute-api." - - !Ref AWS::Region - - ".amazonaws.com/prod" - - UpdateApiConfigFunction: - Type: AWS::Lambda::Function - DependsOn: UpdateCognitoConfigFunction - Properties: - Description: Adds the API endpoint to the config.js file - Handler: index.handler - Runtime: python2.7 - Role: !GetAtt UpdateConfigRole.Arn - Timeout: 120 - Code: - ZipFile: | - import json - import boto3 - import cfnresponse - - s3 = boto3.resource('s3') - - def create(properties, physical_id): - bucket = properties['Bucket'] - config_object = s3.Object(bucket, 'js/config.js').get() - config_data = config_object["Body"].read() - print "Current config: %s" % config_data - config_data = config_data.replace("Base URL of your API including the stage", properties["InvokeUrl"]) - print "Modified config: %s" % config_data - config = s3.Object(bucket,'js/config.js') - config.put(Body=config_data) - return cfnresponse.SUCCESS, None - - def update(properties, physical_id): - return create(properties, physical_id) - - def delete(properties, physical_id): - return cfnresponse.SUCCESS, physical_id - - def handler(event, context): - print "Received event: %s" % json.dumps(event) - - status = cfnresponse.FAILED - new_physical_id = None - - try: - properties = event.get('ResourceProperties') - physical_id = event.get('PhysicalResourceId') - - status, new_physical_id = { - 'Create': create, - 'Update': update, - 'Delete': delete - }.get(event['RequestType'], lambda x, y: (cfnresponse.FAILED, None))(properties, physical_id) - except Exception as e: - print "Exception: %s" % e - status = cfnresponse.FAILED - finally: - cfnresponse.send(event, context, status, {}, new_physical_id) - -Outputs: - WebsiteURL: - Value: !GetAtt WebsiteBucket.WebsiteURL - WildRydesApiInvokeUrl: - Description: URL for the deployed wild rydes API - Value: - Fn::Join: - - "" - - - "https://" - - !Ref WildRydesApi - - ".execute-api." - - !Ref AWS::Region - - ".amazonaws.com/prod" - Export: - Name: WildRydesApiUrl diff --git a/prerequisitesv2.yaml b/prerequisitesv2.yaml deleted file mode 100644 index 9b4f4a2..0000000 --- a/prerequisitesv2.yaml +++ /dev/null @@ -1,182 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Creates an S3 bucket and CloudFormation configured for hosting a static website - -Parameters: - BucketName: - Type: String - Description: The name for the bucket hosting your website, e.g. 'datamanager-yourname' - -Resources: - WebsiteBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Ref BucketName - AccessControl: PublicRead - WebsiteConfiguration: - IndexDocument: index.html - DeletionPolicy: Retain - - WebsiteBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref 'WebsiteBucket' - PolicyDocument: - Statement: - - Sid: PublicReadForGetBucketObjects - Effect: Allow - Principal: '*' - Action: s3:GetObject - Resource: !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', /*]] - - WebsiteCloudfront: - Type: AWS::CloudFront::Distribution - DependsOn: - - WebsiteBucket - Properties: - DistributionConfig: - Comment: Cloudfront Distribution pointing to S3 bucket - Origins: - - DomainName: !Select [2, !Split ["/", !GetAtt WebsiteBucket.WebsiteURL]] - Id: S3Origin - CustomOriginConfig: - HTTPPort: '80' - HTTPSPort: '443' - OriginProtocolPolicy: http-only - Enabled: true - HttpVersion: 'http2' - DefaultRootObject: index.html - DefaultCacheBehavior: - AllowedMethods: - - GET - - HEAD - Compress: true - TargetOriginId: S3Origin - ForwardedValues: - QueryString: true - Cookies: - Forward: none - ViewerProtocolPolicy: redirect-to-https - PriceClass: PriceClass_All - - SecretsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: ADFSSecretData - AttributeDefinitions: - - - AttributeName: email - AttributeType: S - KeySchema: - - - AttributeName: email - KeyType: HASH - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - - RequestSecretDataExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: SecretDataLambda - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - "sts:AssumeRole" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - - PolicyName: SecretDataPolicy - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Action: - - dynamodb:Scan - Resource: !GetAtt SecretsTable.Arn - - RequestSecretDataFunction: - Type: AWS::Lambda::Function - Properties: - FunctionName: ListSecretData - Runtime: nodejs6.10 - Role: !GetAtt RequestSecretDataExecutionRole.Arn - Timeout: 5 - MemorySize: 128 - Handler: index.handler - Code: - ZipFile: > - const AWS = require('aws-sdk'); - - const ddb = new AWS.DynamoDB.DocumentClient(); - - exports.handler = (event, context, callback) => { - if (!event.requestContext.authorizer) { - errorResponse('Authorization not configured', context.awsRequestId, callback); - return; - } - - console.log('Received event: ', event); - - listSecrets(event.requestContext.authorizer.claims.email).then((data) => { - // You can use the callback function to provide a return value from your Node.js - // Lambda functions. The first parameter is used for failed invocations. The - // second parameter specifies the result data of the invocation. - - // Because this Lambda function is called by an API Gateway proxy integration - // the result object must use the following structure. - callback(null, { - statusCode: 200, - body: JSON.stringify(data.Items), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - }).catch((err) => { - console.error(err); - - // If there is an error during processing, catch it and return - // from the Lambda function successfully. Specify a 500 HTTP status - // code and provide an error message in the body. This will provide a - // more meaningful error response to the end client. - errorResponse(err.message, context.awsRequestId, callback) - }); - }; - - function listSecrets(username) { - return ddb.scan({ - ExpressionAttributeValues: { - ":u": username - }, - FilterExpression: 'email = :u', - TableName: 'ADFSSecretData', - }).promise(); - } - - function errorResponse(errorMessage, awsRequestId, callback) { - callback(null, { - statusCode: 500, - body: JSON.stringify({ - Error: errorMessage, - Reference: awsRequestId, - }), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - } - -Outputs: - BucketName: - Value: !Ref 'WebsiteBucket' - Description: Name of S3 bucket to hold website content - CloudfrontEndpoint: - Value: !GetAtt [WebsiteCloudfront, DomainName] - Description: Endpoint for Cloudfront distribution diff --git a/prerequisitesv3.yaml b/prerequisitesv3.yaml deleted file mode 100644 index 029ed0f..0000000 --- a/prerequisitesv3.yaml +++ /dev/null @@ -1,273 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: Creates an S3 bucket and CloudFormation configured for hosting a static website - -Parameters: - BucketName: - Type: String - Description: The name for the bucket hosting your website, e.g. 'datamanager-yourname' - -Resources: - WebsiteBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Ref BucketName - AccessControl: PublicRead - WebsiteConfiguration: - IndexDocument: index.html - DeletionPolicy: Retain - - WebsiteBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref 'WebsiteBucket' - PolicyDocument: - Statement: - - Sid: PublicReadForGetBucketObjects - Effect: Allow - Principal: '*' - Action: s3:GetObject - Resource: !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', /*]] - - WebsiteCloudfront: - Type: AWS::CloudFront::Distribution - DependsOn: - - WebsiteBucket - Properties: - DistributionConfig: - Comment: Cloudfront Distribution pointing to S3 bucket - Origins: - - DomainName: !Select [2, !Split ["/", !GetAtt WebsiteBucket.WebsiteURL]] - Id: S3Origin - CustomOriginConfig: - HTTPPort: '80' - HTTPSPort: '443' - OriginProtocolPolicy: http-only - Enabled: true - HttpVersion: 'http2' - DefaultRootObject: index.html - DefaultCacheBehavior: - AllowedMethods: - - GET - - HEAD - Compress: true - TargetOriginId: S3Origin - ForwardedValues: - QueryString: true - Cookies: - Forward: none - ViewerProtocolPolicy: redirect-to-https - PriceClass: PriceClass_All - - SecretsTable: - Type: AWS::DynamoDB::Table - Properties: - TableName: ADFSSecretData - AttributeDefinitions: - - - AttributeName: email - AttributeType: S - KeySchema: - - - AttributeName: email - KeyType: HASH - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - - RequestSecretDataExecutionRole: - Type: AWS::IAM::Role - Properties: - RoleName: SecretDataLambda - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com - Action: - - "sts:AssumeRole" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - - PolicyName: SecretDataPolicy - PolicyDocument: - Version: 2012-10-17 - Statement: - - - Effect: Allow - Action: - - dynamodb:Scan - Resource: !GetAtt SecretsTable.Arn - - RequestSecretDataFunction: - Type: AWS::Lambda::Function - Properties: - FunctionName: ListSecretData - Runtime: nodejs6.10 - Role: !GetAtt RequestSecretDataExecutionRole.Arn - Timeout: 5 - MemorySize: 128 - Handler: index.handler - Code: - ZipFile: > - const AWS = require('aws-sdk'); - - const ddb = new AWS.DynamoDB.DocumentClient(); - - exports.handler = (event, context, callback) => { - if (!event.requestContext.authorizer) { - errorResponse('Authorization not configured', context.awsRequestId, callback); - return; - } - - console.log('Received event: ', event); - - listSecrets(event.requestContext.authorizer.claims.email).then((data) => { - // You can use the callback function to provide a return value from your Node.js - // Lambda functions. The first parameter is used for failed invocations. The - // second parameter specifies the result data of the invocation. - - // Because this Lambda function is called by an API Gateway proxy integration - // the result object must use the following structure. - callback(null, { - statusCode: 200, - body: JSON.stringify(data.Items), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - }).catch((err) => { - console.error(err); - - // If there is an error during processing, catch it and return - // from the Lambda function successfully. Specify a 500 HTTP status - // code and provide an error message in the body. This will provide a - // more meaningful error response to the end client. - errorResponse(err.message, context.awsRequestId, callback) - }); - }; - - function listSecrets(username) { - return ddb.scan({ - ExpressionAttributeValues: { - ":u": username - }, - FilterExpression: 'email = :u', - TableName: 'ADFSSecretData', - }).promise(); - } - - function errorResponse(errorMessage, awsRequestId, callback) { - callback(null, { - statusCode: 500, - body: JSON.stringify({ - Error: errorMessage, - Reference: awsRequestId, - }), - headers: { - 'Access-Control-Allow-Origin': '*', - }, - }); - } - - WildRydesApi: - Type: AWS::ApiGateway::RestApi - Properties: - Name: WildRydes - Body: - swagger: 2.0 - info: - version: 1.0.0 - title: WildRydes - paths: - /ride: - post: - description: Requests a new ride - consumes: - - application/json - produces: - - application/json - responses: - "200": - description: "200 response" - headers: - Access-Control-Allow-Origin: - type: "string" - x-amazon-apigateway-integration: - responses: - default: - statusCode: 200 - responseParameters: - method.response.header.Access-Control-Allow-Origin: "'*'" - uri: - Fn::Join: - - "" - - - "arn:aws:apigateway:" - - !Ref AWS::Region - - ":lambda:path/2015-03-31/functions/" - - !GetAtt RequestSecretDataFunction.Arn - - "/invocations" - passthroughBehavior: "when_no_match" - httpMethod: "POST" - contentHandling: "CONVERT_TO_TEXT" - type: "aws_proxy" - options: - responses: - "200": - description: "200 response" - schema: - $ref: "#/definitions/Empty" - headers: - Access-Control-Allow-Origin: - type: "string" - Access-Control-Allow-Methods: - type: "string" - Access-Control-Allow-Headers: - type: "string" - x-amazon-apigateway-integration: - responses: - default: - statusCode: "200" - responseParameters: - method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST'" - method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'" - method.response.header.Access-Control-Allow-Origin: "'*'" - requestTemplates: - application/json: "{\"statusCode\": 200}" - passthroughBehavior: "when_no_match" - type: "mock" - - WildRydesApiDeployment: - Type: AWS::ApiGateway::Deployment - Properties: - Description: Prod deployment for wild Rydes API - RestApiId: !Ref WildRydesApi - StageName: prod - - WildRydesFunctionPermissions: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref RequestSecretDataFunction - Principal: apigateway.amazonaws.com - SourceArn: - Fn::Join: - - "" - - - "arn:aws:execute-api:" - - !Ref AWS::Region - - ":" - - !Ref AWS::AccountId - - ":" - - !Ref WildRydesApi - - "/*" - -Outputs: - BucketName: - Value: !Ref 'WebsiteBucket' - Description: Name of S3 bucket to hold website content - CloudfrontEndpoint: - Value: !GetAtt [WebsiteCloudfront, DomainName] - Description: Endpoint for Cloudfront distribution