├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── audit-fix.sh ├── audit.sh ├── build-ui.sh ├── build.sh ├── cdk-cleanup.sh ├── cdk ├── .gitignore ├── .npmignore ├── README.md ├── cdk.json ├── package-lock.json ├── package.json ├── src │ ├── cdk.ts │ ├── generateConfig.ts │ └── utils.ts ├── tsconfig.json └── tslint.yaml ├── clean.sh ├── deploy.sh ├── diff.sh ├── docs ├── OktaInstructions.md └── images │ ├── add-app2.png │ ├── add-applications.png │ ├── app-config.png │ ├── app-integration.png │ ├── dev-classicUI.png │ ├── gen-settings.png │ └── saml-settings.png ├── env.sh.template ├── install.sh ├── lambda ├── api │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── expressApp.ts │ │ ├── index.ts │ │ ├── local.ts │ │ ├── models │ │ │ └── pet.ts │ │ └── services │ │ │ ├── authorizationMiddleware.ts │ │ │ ├── dynamoDBForcedSignoutHandler.ts │ │ │ ├── dynamoDBStorageService.ts │ │ │ └── storageService.ts │ ├── tests │ │ └── app.test.ts │ ├── tsconfig.json │ ├── tslint.yaml │ ├── update-lambda-code.sh │ └── webpack.config.js └── pretokengeneration │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── helpers.ts │ └── index.ts │ ├── tests │ └── index.test.ts │ ├── tsconfig.json │ └── tslint.yaml ├── resize.sh ├── synth.sh ├── test.sh ├── ui-react ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── components │ │ ├── App.css │ │ ├── App.test.tsx │ │ └── App.tsx │ ├── config │ │ └── amplifyConfig.ts │ ├── index.css │ ├── index.tsx │ ├── model │ │ ├── pet.ts │ │ ├── user.test.ts │ │ └── user.ts │ ├── service │ │ └── APIService.ts │ └── serviceWorker.ts └── tsconfig.json └── workshop.sh /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '42 10 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/.idea 3 | **/*.iml 4 | **/*.js 5 | **/*.d.ts 6 | **/node_modules 7 | !webpack.config.js 8 | **/.nyc_output 9 | **/coverage 10 | **/dist 11 | local-env.sh 12 | /sdkPlayground/ 13 | !/lambda/api/src/types/aws-serverless-express/index.d.ts 14 | /ui-react/src/config/autoGenConfig.* 15 | env.sh 16 | metadata.xml -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/amazon-cognito-example-for-external-idp/issues), or [recently closed](https://github.com/aws-samples/amazon-cognito-example-for-external-idp/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/amazon-cognito-example-for-external-idp/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/amazon-cognito-example-for-external-idp/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Identity and Access Control for Custom Enterprise Applications 2 | 3 | ## Overview 4 | 5 | This example can be used as a starting point for using Amazon Cognito together with an external IdP 6 | (e.g. a SAML 2.0/OIDC provider or a social login provider). 7 | It shows how to use triggers in order to map IdP attributes 8 | (e.g. LDAP group membership passed on the SAML response as an attribute) 9 | to Amazon Cognito User Pools Groups and optionally also to IAM roles. 10 | 11 | It contains all that is needed in order to create a serverless web application with 12 | Amazon Cognito, Amazon API Gateway, AWS Lambda and Amazon DynamoDB (with optionally an external IdP). 13 | 14 | It handles fine-grained role-based access control and demonstrates how to associate users to roles/groups based 15 | on mapped attributes from an external IdP or social login provider. 16 | 17 | It is using TypeScript for frontend, backend and infrastructure. (Using [AWS CDK](https://github.com/awslabs/aws-cdk)) 18 | 19 | ## Modules 20 | 21 | The example contains the following modules within these sub-folders: 22 | 23 | ### /cdk 24 | 25 | This module is using [AWS CDK](https://docs.aws.amazon.com/cdk/api/latest/) 26 | 27 | CDK is a software development framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation. 28 | 29 | It defines all the resources needed in order to create the sample application 30 | 31 | It defines the following resources 32 | 33 | - **Amazon API Gateway**: Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. 34 | Combined with [Amazon Cognito User Pools Authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html) - it handles validation of the user's tokens. 35 | - **AWS Lambda**: AWS Lambda lets you run code without provisioning or managing servers. You pay only for the compute time you consume - there is no charge when your code is not running. 36 | This is the serverless compute service that runs the backend of our app (behind Amazon API Gateway). 37 | requests are only forwarded if the user is authenticated and has a valid JWT token. 38 | - **Amazon Cognito User Pools**: Amazon Cognito lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily. 39 | Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0. 40 | - **Amazon DynamoDB**: Amazon DynamoDB is a serverless key-value and document database that delivers single-digit millisecond performance at any scale. 41 | It is used as the persistence storage layer for our example application. 42 | 43 | ### /lambda/api 44 | 45 | The backend of the example. This is a standard AWS Lambda application written as a node.js (express.js) application 46 | 47 | In order to allow express.js to run in an AWS Lambda runtime, we include https://github.com/awslabs/aws-serverless-express 48 | 49 | (see more examples [here](https://github.com/awslabs/aws-serverless-express/tree/master/examples/basic-starter)) 50 | 51 | **Some notable files** 52 | 53 | - **index.ts**: this is a regular [lambda handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html) 54 | that uses aws-serverless-express to map between a [AWS Lambda Proxy Integration 55 | request/response structure](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html) 56 | and the express.js app 57 | - **app.ts**: this is the actual express.js app 58 | 59 | - **local.ts**: this can be used to launch the app locally as a regular express.js app. 60 | 61 | - **services/authorizationMiddleware.ts**: this is an example express.js middleware that does the following: 62 | 63 | 1. Adds type information to the request for simple auto-completion of available request information passed from Amazon API Gateway to the lambda function 64 | 65 | 2. A convenient / syntactic sugar that makes the claims and Amazon Cognito User Pool group available on the request object. 66 | e.g. `req.groups.has("admin-role")` will return true if the user is authenticated and is a member of group "admin-role" 67 | and `const email = req.claims ? req.claims.email : null;` will get the user's email if the user is logged in and has an email claim in the JWT 68 | 69 | ### /lambda/pretokengeneration 70 | 71 | This is an [Amazon Cognito User Pools Trigger](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html) 72 | that allows to add/remove claims from the [JWT ID token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-user-pools-using-the-id-token) before giving it to the user. 73 | 74 | It is using a trigger named [Pre Token Generation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html). 75 | It allows to do the following: 76 | 77 | 1. Add or remove claims (claims are user built in or custom attributes, e.g. `email` or `custom:customAttributeName`) 78 | 2. Create or remove Groups (a special claim under the name `cognito:groups`) 79 | 3. Add `roles` and `preferred_role` [mapping](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_RoleMapping.html). 80 | These mappings are [similar to assigning a role to a group in the AWS console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html#assigning-iam-roles-to-groups). 81 | This can be used to later give users fine grained, temporary AWS credentials based on their group. 82 | (e.g. letting mobile app users upload a file directly to s3 to their user's folder) 83 | 84 | In this example we simply map from a custom attribute (that is mapped from an IdP attribute, e.g. a SAML attribute that represents for example the user's group memberships in the corporate directory) 85 | into a group claim in the token. Group claims are visible in both the id token and the access token generated by Amazon Cognito. 86 | 87 | ### /ui-react 88 | 89 | A simple React frontend that connects to the backend API. 90 | 91 | It is using [AWS Amplify](https://aws-amplify.github.io/), that provides, among others [react components](https://aws-amplify.github.io/docs/js/start?platform=react) for simpler 92 | integration with various AWS services from web and mobile applications. 93 | 94 | AWS Amplify can manage all aspects of a project, but since we used AWS CDK, we followed the [manual setup](https://aws-amplify.github.io/docs/js/authentication#manual-setup) 95 | 96 | **Some notable files** 97 | 98 | - **user.ts**: provide an example of how to get the token information (e.g. group membership) on the client side. 99 | group membership information can be used for example for hiding/graying out sections that the user has no permission for. 100 | This is not used for enforcing authorization or validation of the token, but it provides a nicer user experience where actions that the user will not be permitted to perform are not visible / grayed out for them. 101 | 102 | ## Notes 103 | 104 | - Do not add the `aws.cognito.signin.user.admin` scope, (not added by default) 105 | this will allow users to modify their own attributes directly with the access token. 106 | Since the IdP is the source of truth, and we don't want users to change attributes 107 | (especially those used for authorization) on their own, this scope should not be added. 108 | 109 | - Do not enable any non-OAuth 2.0 auth flows other than `ALLOW_REFRESH_TOKEN_AUTH` (`explicitAuthFlows` in cdk.ts) to ensure users can only use the OAuth 2.0 flows. 110 | 111 | # Getting Started - Mac / Linux 112 | 113 | ## Pre-requisites 114 | 115 | 1. An AWS account https://aws.amazon.com/resources/create-account/ 116 | 2. AWS CLI https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html 117 | 3. Configure AWS CLI https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html 118 | 4. Ensure you have the latest node and npm installed https://nodejs.org/en/download/ 119 | 120 | ## Installation 121 | 122 | 1. Clone or fork this repo (e.g. `git clone git@github.com:aws-samples/amazon-cognito-example-for-external-idp.git`) 123 | 2. Copy `env.sh.template` to `env.sh` (not recommended to be pushed to your git repo, it's in .gitignore as a protection) 124 | 3. Edit `env.sh` and set the values there based on your environment 125 | 4. Run `./install.sh` which does the following: 126 | - Installs all node dependencies (it runs `npm install` in all relevant sub-folders) 127 | - Builds the project (runs `tsc -b` in each relevant sub-folder - tsc is the TypeScript compiler) 128 | - Runs `cdk bootstrap` - which creates a stack named CDKToolkit (if it was not created already) that helps simplify managing assets. 129 | For more information about assets see [here](https://docs.aws.amazon.com/cdk/latest/guide/assets.html) 130 | 131 | NOTES: 132 | 133 | - If you are using a profile, either run `export AWS_PROFILE=` before the above commands, or `AWS_PROFILE= ./.sh` 134 | - CDK is installed locally to ensure the right version is used. In order to install it globally for use in other projects, run: `$ npm i -g aws-cdk` (see [here](https://github.com/awslabs/aws-cdk#getting-started) for more details) 135 | 136 | ## Deploying / Updating the Backend Stack 137 | 138 | - After installing. Run `./deploy.sh` to deploy the backend stack. (For the first time as well as after making changes) 139 | 140 | ## Launching the UI 141 | 142 | ### React 143 | 144 | - `cd ui-react && npm start` to run the UI in http://localhost:3000 145 | 146 | ## Other Commands 147 | 148 | - Run `./diff.sh` to compare deployed stack with current state 149 | - Run `./synth.sh` to display the generated CloudFormation script from the CDK code 150 | 151 | - Run `./test.sh` to run all tests 152 | - Run `./build.sh` to compile all packages 153 | - Run `./clean.sh` to clean compiled packages 154 | 155 | ## IdP Configuration Instructions 156 | 157 | - **Okta**: 158 | 159 | - https://aws.amazon.com/premiumsupport/knowledge-center/cognito-okta-saml-identity-provider/ 160 | NOTE: to avoid a circular "chicken and egg" dependency, create the Okta Application with placeholder values just to get the metadata XML, then after deploying, update in Okta for the correct values for the user pool. 161 | 162 | - **ADFS**: 163 | - https://aws.amazon.com/blogs/mobile/building-adfs-federation-for-your-web-app-using-amazon-cognito-user-pools/ 164 | - https://aws.amazon.com/premiumsupport/knowledge-center/cognito-ad-fs-saml/ 165 | 166 | ## Related Resources 167 | 168 | - AWS Security Blog Post: [Role-based access control using Amazon Cognito and an external identity provider](https://aws.amazon.com/blogs/security/role-based-access-control-using-amazon-cognito-and-an-external-identity-provider/) 169 | 170 | - AWS re:Inforce 2019: Identity and Access Control for Custom Enterprise Applications (SDD412) [Video](https://www.youtube.com/watch?v=VZzx15IEj7Y) | [Slides](https://www.slideshare.net/AmazonWebServices/identity-and-access-control-for-custom-enterprise-applications-sdd412-aws-reinforce-2019) 171 | 172 | ## Previous Versions 173 | Please note, Angular support was removed from this repo to simplify maintenance activities, currently only the react UI is supported. You can view the old Angular UI on the [Angular Archive branch](https://github.com/aws-samples/amazon-cognito-example-for-external-idp/tree/angular-archive) (this is static and receives no security updates) 174 | 175 | ## License Summary 176 | 177 | This sample code is made available under the [MIT-0 license](https://github.com/aws/mit-0). See the LICENSE file. 178 | -------------------------------------------------------------------------------- /audit-fix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | echo "this will perform npm audit --fix on all packages" 7 | 8 | echo "folder: lambda/api" 9 | cd lambda 10 | cd api 11 | npm audit fix 12 | echo "folder: lambda/pretokengeneration" 13 | cd .. 14 | cd pretokengeneration 15 | npm audit fix 16 | cd ../.. 17 | echo "folder: cdk" 18 | cd cdk 19 | npm audit fix 20 | cd .. 21 | echo "folder: ui-react" 22 | cd ui-react 23 | npm audit fix 24 | cd .. -------------------------------------------------------------------------------- /audit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | echo "this will perform npm audit --production on all packages" 7 | 8 | echo "folder: lambda/api" 9 | cd lambda 10 | cd api 11 | npm audit 12 | echo "folder: lambda/pretokengeneration" 13 | cd .. 14 | cd pretokengeneration 15 | npm audit 16 | cd ../.. 17 | echo "folder: cdk" 18 | cd cdk 19 | npm audit 20 | cd .. 21 | echo "folder: ui-react" 22 | cd ui-react 23 | npm audit 24 | cd .. -------------------------------------------------------------------------------- /build-ui.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | 7 | echo "Generating config for UI based on stack outputs" 8 | cd cdk 9 | npm run generate-config -- "${STACK_NAME}" "${STACK_REGION}" ../ui-react/src/config/autoGenConfig.ts 10 | cd .. 11 | echo "Building UIs" 12 | 13 | cd ui-react 14 | npm run compile-config 15 | npm run build 16 | cd .. -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | cd lambda 7 | cd api 8 | npm run build 9 | cd .. 10 | cd pretokengeneration 11 | npm run build 12 | cd ../.. 13 | cd cdk 14 | npm run build 15 | cd .. 16 | echo "Build successful" 17 | -------------------------------------------------------------------------------- /cdk-cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | echo "Destroying CDK Cloudformation Stack -- Some manual removal required" 7 | cd cdk 8 | npm run cdk-destroy 9 | cd .. -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | *.js.map 4 | node_modules 5 | /.cdk.staging/ 6 | /cdk.out/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Useful commands 2 | 3 | * `npm run build` compile typescript to js 4 | * `npm run watch` watch for changes and compile 5 | * `cdk deploy` deploy this stack to your default AWS account/region 6 | * `cdk diff` compare deployed stack with current state 7 | * `cdk synth` emits the synthesized CloudFormation template 8 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node src/cdk.js" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cognito-example-for-external-idp-cdk", 3 | "version": "0.1.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "amazon-cognito-example-for-external-idp-cdk", 9 | "version": "0.1.2", 10 | "license": "MIT-0", 11 | "dependencies": { 12 | "aws-cdk": "^2.164.1", 13 | "aws-cdk-lib": "^2.164.1", 14 | "aws-sdk": "^2.1354.0", 15 | "source-map-support": "^0.5.19" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.51", 19 | "@types/node": "^14.0.1", 20 | "ts-node": "^8.10.1", 21 | "typescript": "^3.9.2" 22 | } 23 | }, 24 | "node_modules/@aws-cdk/asset-awscli-v1": { 25 | "version": "2.2.209", 26 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.209.tgz", 27 | "integrity": "sha512-tL7aBDzx/QBuZoQso9OST2BMCoev89v01iQZicOKlR0J6vWQLPiqZfn4vd9nissFbM4X+xIwi3UKasPBTQL0WQ==", 28 | "license": "Apache-2.0" 29 | }, 30 | "node_modules/@aws-cdk/asset-kubectl-v20": { 31 | "version": "2.1.3", 32 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", 33 | "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", 34 | "license": "Apache-2.0" 35 | }, 36 | "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { 37 | "version": "2.1.0", 38 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", 39 | "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", 40 | "license": "Apache-2.0" 41 | }, 42 | "node_modules/@aws-cdk/cloud-assembly-schema": { 43 | "version": "38.0.1", 44 | "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", 45 | "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", 46 | "bundleDependencies": [ 47 | "jsonschema", 48 | "semver" 49 | ], 50 | "license": "Apache-2.0", 51 | "dependencies": { 52 | "jsonschema": "^1.4.1", 53 | "semver": "^7.6.3" 54 | } 55 | }, 56 | "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { 57 | "version": "1.4.1", 58 | "inBundle": true, 59 | "license": "MIT", 60 | "engines": { 61 | "node": "*" 62 | } 63 | }, 64 | "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { 65 | "version": "7.6.3", 66 | "inBundle": true, 67 | "license": "ISC", 68 | "bin": { 69 | "semver": "bin/semver.js" 70 | }, 71 | "engines": { 72 | "node": ">=10" 73 | } 74 | }, 75 | "node_modules/@types/aws-lambda": { 76 | "version": "8.10.51", 77 | "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.51.tgz", 78 | "integrity": "sha512-XK7RerpXj4r+IO0r7qIeNqUSU6L4qhPMwNhISxozJJiUX/jdXj9WYzTShRVisEcUQHXgJ4TTBqTArM8f9Mjb8g==", 79 | "dev": true 80 | }, 81 | "node_modules/@types/node": { 82 | "version": "14.0.1", 83 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz", 84 | "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==", 85 | "dev": true 86 | }, 87 | "node_modules/arg": { 88 | "version": "4.1.3", 89 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 90 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 91 | "dev": true 92 | }, 93 | "node_modules/available-typed-arrays": { 94 | "version": "1.0.5", 95 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", 96 | "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", 97 | "engines": { 98 | "node": ">= 0.4" 99 | }, 100 | "funding": { 101 | "url": "https://github.com/sponsors/ljharb" 102 | } 103 | }, 104 | "node_modules/aws-cdk": { 105 | "version": "2.164.1", 106 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.164.1.tgz", 107 | "integrity": "sha512-dWRViQgHLe7GHkPIQGA+8EQSm8TBcxemyCC3HHW3wbLMWUDbspio9Dktmw5EmWxlFjjWh86Dk1JWf1zKQo8C5g==", 108 | "license": "Apache-2.0", 109 | "bin": { 110 | "cdk": "bin/cdk" 111 | }, 112 | "engines": { 113 | "node": ">= 14.15.0" 114 | }, 115 | "optionalDependencies": { 116 | "fsevents": "2.3.2" 117 | } 118 | }, 119 | "node_modules/aws-cdk-lib": { 120 | "version": "2.164.1", 121 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.164.1.tgz", 122 | "integrity": "sha512-jNvVmfZJbZoAYU94b5dzTlF2z6JXJ204NgcYY5haOa6mq3m2bzdYPXnPtB5kpAX3oBi++yoRdmLhqgckdEhUZA==", 123 | "bundleDependencies": [ 124 | "@balena/dockerignore", 125 | "case", 126 | "fs-extra", 127 | "ignore", 128 | "jsonschema", 129 | "minimatch", 130 | "punycode", 131 | "semver", 132 | "table", 133 | "yaml", 134 | "mime-types" 135 | ], 136 | "license": "Apache-2.0", 137 | "dependencies": { 138 | "@aws-cdk/asset-awscli-v1": "^2.2.202", 139 | "@aws-cdk/asset-kubectl-v20": "^2.1.2", 140 | "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", 141 | "@aws-cdk/cloud-assembly-schema": "^38.0.0", 142 | "@balena/dockerignore": "^1.0.2", 143 | "case": "1.6.3", 144 | "fs-extra": "^11.2.0", 145 | "ignore": "^5.3.2", 146 | "jsonschema": "^1.4.1", 147 | "mime-types": "^2.1.35", 148 | "minimatch": "^3.1.2", 149 | "punycode": "^2.3.1", 150 | "semver": "^7.6.3", 151 | "table": "^6.8.2", 152 | "yaml": "1.10.2" 153 | }, 154 | "engines": { 155 | "node": ">= 14.15.0" 156 | }, 157 | "peerDependencies": { 158 | "constructs": "^10.0.0" 159 | } 160 | }, 161 | "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { 162 | "version": "1.0.2", 163 | "inBundle": true, 164 | "license": "Apache-2.0" 165 | }, 166 | "node_modules/aws-cdk-lib/node_modules/ajv": { 167 | "version": "8.17.1", 168 | "inBundle": true, 169 | "license": "MIT", 170 | "dependencies": { 171 | "fast-deep-equal": "^3.1.3", 172 | "fast-uri": "^3.0.1", 173 | "json-schema-traverse": "^1.0.0", 174 | "require-from-string": "^2.0.2" 175 | }, 176 | "funding": { 177 | "type": "github", 178 | "url": "https://github.com/sponsors/epoberezkin" 179 | } 180 | }, 181 | "node_modules/aws-cdk-lib/node_modules/ansi-regex": { 182 | "version": "5.0.1", 183 | "inBundle": true, 184 | "license": "MIT", 185 | "engines": { 186 | "node": ">=8" 187 | } 188 | }, 189 | "node_modules/aws-cdk-lib/node_modules/ansi-styles": { 190 | "version": "4.3.0", 191 | "inBundle": true, 192 | "license": "MIT", 193 | "dependencies": { 194 | "color-convert": "^2.0.1" 195 | }, 196 | "engines": { 197 | "node": ">=8" 198 | }, 199 | "funding": { 200 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 201 | } 202 | }, 203 | "node_modules/aws-cdk-lib/node_modules/astral-regex": { 204 | "version": "2.0.0", 205 | "inBundle": true, 206 | "license": "MIT", 207 | "engines": { 208 | "node": ">=8" 209 | } 210 | }, 211 | "node_modules/aws-cdk-lib/node_modules/balanced-match": { 212 | "version": "1.0.2", 213 | "inBundle": true, 214 | "license": "MIT" 215 | }, 216 | "node_modules/aws-cdk-lib/node_modules/brace-expansion": { 217 | "version": "1.1.11", 218 | "inBundle": true, 219 | "license": "MIT", 220 | "dependencies": { 221 | "balanced-match": "^1.0.0", 222 | "concat-map": "0.0.1" 223 | } 224 | }, 225 | "node_modules/aws-cdk-lib/node_modules/case": { 226 | "version": "1.6.3", 227 | "inBundle": true, 228 | "license": "(MIT OR GPL-3.0-or-later)", 229 | "engines": { 230 | "node": ">= 0.8.0" 231 | } 232 | }, 233 | "node_modules/aws-cdk-lib/node_modules/color-convert": { 234 | "version": "2.0.1", 235 | "inBundle": true, 236 | "license": "MIT", 237 | "dependencies": { 238 | "color-name": "~1.1.4" 239 | }, 240 | "engines": { 241 | "node": ">=7.0.0" 242 | } 243 | }, 244 | "node_modules/aws-cdk-lib/node_modules/color-name": { 245 | "version": "1.1.4", 246 | "inBundle": true, 247 | "license": "MIT" 248 | }, 249 | "node_modules/aws-cdk-lib/node_modules/concat-map": { 250 | "version": "0.0.1", 251 | "inBundle": true, 252 | "license": "MIT" 253 | }, 254 | "node_modules/aws-cdk-lib/node_modules/emoji-regex": { 255 | "version": "8.0.0", 256 | "inBundle": true, 257 | "license": "MIT" 258 | }, 259 | "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { 260 | "version": "3.1.3", 261 | "inBundle": true, 262 | "license": "MIT" 263 | }, 264 | "node_modules/aws-cdk-lib/node_modules/fast-uri": { 265 | "version": "3.0.1", 266 | "inBundle": true, 267 | "license": "MIT" 268 | }, 269 | "node_modules/aws-cdk-lib/node_modules/fs-extra": { 270 | "version": "11.2.0", 271 | "inBundle": true, 272 | "license": "MIT", 273 | "dependencies": { 274 | "graceful-fs": "^4.2.0", 275 | "jsonfile": "^6.0.1", 276 | "universalify": "^2.0.0" 277 | }, 278 | "engines": { 279 | "node": ">=14.14" 280 | } 281 | }, 282 | "node_modules/aws-cdk-lib/node_modules/graceful-fs": { 283 | "version": "4.2.11", 284 | "inBundle": true, 285 | "license": "ISC" 286 | }, 287 | "node_modules/aws-cdk-lib/node_modules/ignore": { 288 | "version": "5.3.2", 289 | "inBundle": true, 290 | "license": "MIT", 291 | "engines": { 292 | "node": ">= 4" 293 | } 294 | }, 295 | "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { 296 | "version": "3.0.0", 297 | "inBundle": true, 298 | "license": "MIT", 299 | "engines": { 300 | "node": ">=8" 301 | } 302 | }, 303 | "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { 304 | "version": "1.0.0", 305 | "inBundle": true, 306 | "license": "MIT" 307 | }, 308 | "node_modules/aws-cdk-lib/node_modules/jsonfile": { 309 | "version": "6.1.0", 310 | "inBundle": true, 311 | "license": "MIT", 312 | "dependencies": { 313 | "universalify": "^2.0.0" 314 | }, 315 | "optionalDependencies": { 316 | "graceful-fs": "^4.1.6" 317 | } 318 | }, 319 | "node_modules/aws-cdk-lib/node_modules/jsonschema": { 320 | "version": "1.4.1", 321 | "inBundle": true, 322 | "license": "MIT", 323 | "engines": { 324 | "node": "*" 325 | } 326 | }, 327 | "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { 328 | "version": "4.4.2", 329 | "inBundle": true, 330 | "license": "MIT" 331 | }, 332 | "node_modules/aws-cdk-lib/node_modules/mime-db": { 333 | "version": "1.52.0", 334 | "inBundle": true, 335 | "license": "MIT", 336 | "engines": { 337 | "node": ">= 0.6" 338 | } 339 | }, 340 | "node_modules/aws-cdk-lib/node_modules/mime-types": { 341 | "version": "2.1.35", 342 | "inBundle": true, 343 | "license": "MIT", 344 | "dependencies": { 345 | "mime-db": "1.52.0" 346 | }, 347 | "engines": { 348 | "node": ">= 0.6" 349 | } 350 | }, 351 | "node_modules/aws-cdk-lib/node_modules/minimatch": { 352 | "version": "3.1.2", 353 | "inBundle": true, 354 | "license": "ISC", 355 | "dependencies": { 356 | "brace-expansion": "^1.1.7" 357 | }, 358 | "engines": { 359 | "node": "*" 360 | } 361 | }, 362 | "node_modules/aws-cdk-lib/node_modules/punycode": { 363 | "version": "2.3.1", 364 | "inBundle": true, 365 | "license": "MIT", 366 | "engines": { 367 | "node": ">=6" 368 | } 369 | }, 370 | "node_modules/aws-cdk-lib/node_modules/require-from-string": { 371 | "version": "2.0.2", 372 | "inBundle": true, 373 | "license": "MIT", 374 | "engines": { 375 | "node": ">=0.10.0" 376 | } 377 | }, 378 | "node_modules/aws-cdk-lib/node_modules/semver": { 379 | "version": "7.6.3", 380 | "inBundle": true, 381 | "license": "ISC", 382 | "bin": { 383 | "semver": "bin/semver.js" 384 | }, 385 | "engines": { 386 | "node": ">=10" 387 | } 388 | }, 389 | "node_modules/aws-cdk-lib/node_modules/slice-ansi": { 390 | "version": "4.0.0", 391 | "inBundle": true, 392 | "license": "MIT", 393 | "dependencies": { 394 | "ansi-styles": "^4.0.0", 395 | "astral-regex": "^2.0.0", 396 | "is-fullwidth-code-point": "^3.0.0" 397 | }, 398 | "engines": { 399 | "node": ">=10" 400 | }, 401 | "funding": { 402 | "url": "https://github.com/chalk/slice-ansi?sponsor=1" 403 | } 404 | }, 405 | "node_modules/aws-cdk-lib/node_modules/string-width": { 406 | "version": "4.2.3", 407 | "inBundle": true, 408 | "license": "MIT", 409 | "dependencies": { 410 | "emoji-regex": "^8.0.0", 411 | "is-fullwidth-code-point": "^3.0.0", 412 | "strip-ansi": "^6.0.1" 413 | }, 414 | "engines": { 415 | "node": ">=8" 416 | } 417 | }, 418 | "node_modules/aws-cdk-lib/node_modules/strip-ansi": { 419 | "version": "6.0.1", 420 | "inBundle": true, 421 | "license": "MIT", 422 | "dependencies": { 423 | "ansi-regex": "^5.0.1" 424 | }, 425 | "engines": { 426 | "node": ">=8" 427 | } 428 | }, 429 | "node_modules/aws-cdk-lib/node_modules/table": { 430 | "version": "6.8.2", 431 | "inBundle": true, 432 | "license": "BSD-3-Clause", 433 | "dependencies": { 434 | "ajv": "^8.0.1", 435 | "lodash.truncate": "^4.4.2", 436 | "slice-ansi": "^4.0.0", 437 | "string-width": "^4.2.3", 438 | "strip-ansi": "^6.0.1" 439 | }, 440 | "engines": { 441 | "node": ">=10.0.0" 442 | } 443 | }, 444 | "node_modules/aws-cdk-lib/node_modules/universalify": { 445 | "version": "2.0.1", 446 | "inBundle": true, 447 | "license": "MIT", 448 | "engines": { 449 | "node": ">= 10.0.0" 450 | } 451 | }, 452 | "node_modules/aws-cdk-lib/node_modules/yaml": { 453 | "version": "1.10.2", 454 | "inBundle": true, 455 | "license": "ISC", 456 | "engines": { 457 | "node": ">= 6" 458 | } 459 | }, 460 | "node_modules/aws-sdk": { 461 | "version": "2.1354.0", 462 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1354.0.tgz", 463 | "integrity": "sha512-3aDxvyuOqMB9DqJguCq6p8momdsz0JR1axwkWOOCzHA7a35+Bw+WLmqt3pWwRjR1tGIwkkZ2CvGJObYHsOuw3w==", 464 | "dependencies": { 465 | "buffer": "4.9.2", 466 | "events": "1.1.1", 467 | "ieee754": "1.1.13", 468 | "jmespath": "0.16.0", 469 | "querystring": "0.2.0", 470 | "sax": "1.2.1", 471 | "url": "0.10.3", 472 | "util": "^0.12.4", 473 | "uuid": "8.0.0", 474 | "xml2js": "0.5.0" 475 | }, 476 | "engines": { 477 | "node": ">= 10.0.0" 478 | } 479 | }, 480 | "node_modules/base64-js": { 481 | "version": "1.5.1", 482 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 483 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 484 | "funding": [ 485 | { 486 | "type": "github", 487 | "url": "https://github.com/sponsors/feross" 488 | }, 489 | { 490 | "type": "patreon", 491 | "url": "https://www.patreon.com/feross" 492 | }, 493 | { 494 | "type": "consulting", 495 | "url": "https://feross.org/support" 496 | } 497 | ] 498 | }, 499 | "node_modules/buffer": { 500 | "version": "4.9.2", 501 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 502 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 503 | "dependencies": { 504 | "base64-js": "^1.0.2", 505 | "ieee754": "^1.1.4", 506 | "isarray": "^1.0.0" 507 | } 508 | }, 509 | "node_modules/buffer-from": { 510 | "version": "1.1.1", 511 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 512 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 513 | }, 514 | "node_modules/call-bind": { 515 | "version": "1.0.2", 516 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 517 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 518 | "dependencies": { 519 | "function-bind": "^1.1.1", 520 | "get-intrinsic": "^1.0.2" 521 | }, 522 | "funding": { 523 | "url": "https://github.com/sponsors/ljharb" 524 | } 525 | }, 526 | "node_modules/constructs": { 527 | "version": "10.4.2", 528 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", 529 | "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", 530 | "license": "Apache-2.0", 531 | "peer": true 532 | }, 533 | "node_modules/diff": { 534 | "version": "4.0.2", 535 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 536 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 537 | "dev": true, 538 | "engines": { 539 | "node": ">=0.3.1" 540 | } 541 | }, 542 | "node_modules/events": { 543 | "version": "1.1.1", 544 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 545 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", 546 | "engines": { 547 | "node": ">=0.4.x" 548 | } 549 | }, 550 | "node_modules/for-each": { 551 | "version": "0.3.3", 552 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", 553 | "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", 554 | "dependencies": { 555 | "is-callable": "^1.1.3" 556 | } 557 | }, 558 | "node_modules/fsevents": { 559 | "version": "2.3.2", 560 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 561 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 562 | "hasInstallScript": true, 563 | "optional": true, 564 | "os": [ 565 | "darwin" 566 | ], 567 | "engines": { 568 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 569 | } 570 | }, 571 | "node_modules/function-bind": { 572 | "version": "1.1.1", 573 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 574 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 575 | }, 576 | "node_modules/get-intrinsic": { 577 | "version": "1.2.0", 578 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", 579 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", 580 | "dependencies": { 581 | "function-bind": "^1.1.1", 582 | "has": "^1.0.3", 583 | "has-symbols": "^1.0.3" 584 | }, 585 | "funding": { 586 | "url": "https://github.com/sponsors/ljharb" 587 | } 588 | }, 589 | "node_modules/gopd": { 590 | "version": "1.0.1", 591 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 592 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 593 | "dependencies": { 594 | "get-intrinsic": "^1.1.3" 595 | }, 596 | "funding": { 597 | "url": "https://github.com/sponsors/ljharb" 598 | } 599 | }, 600 | "node_modules/has": { 601 | "version": "1.0.3", 602 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 603 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 604 | "dependencies": { 605 | "function-bind": "^1.1.1" 606 | }, 607 | "engines": { 608 | "node": ">= 0.4.0" 609 | } 610 | }, 611 | "node_modules/has-symbols": { 612 | "version": "1.0.3", 613 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 614 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 615 | "engines": { 616 | "node": ">= 0.4" 617 | }, 618 | "funding": { 619 | "url": "https://github.com/sponsors/ljharb" 620 | } 621 | }, 622 | "node_modules/has-tostringtag": { 623 | "version": "1.0.0", 624 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", 625 | "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", 626 | "dependencies": { 627 | "has-symbols": "^1.0.2" 628 | }, 629 | "engines": { 630 | "node": ">= 0.4" 631 | }, 632 | "funding": { 633 | "url": "https://github.com/sponsors/ljharb" 634 | } 635 | }, 636 | "node_modules/ieee754": { 637 | "version": "1.1.13", 638 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 639 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 640 | }, 641 | "node_modules/inherits": { 642 | "version": "2.0.4", 643 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 644 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 645 | }, 646 | "node_modules/is-arguments": { 647 | "version": "1.1.1", 648 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", 649 | "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", 650 | "dependencies": { 651 | "call-bind": "^1.0.2", 652 | "has-tostringtag": "^1.0.0" 653 | }, 654 | "engines": { 655 | "node": ">= 0.4" 656 | }, 657 | "funding": { 658 | "url": "https://github.com/sponsors/ljharb" 659 | } 660 | }, 661 | "node_modules/is-callable": { 662 | "version": "1.2.7", 663 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 664 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 665 | "engines": { 666 | "node": ">= 0.4" 667 | }, 668 | "funding": { 669 | "url": "https://github.com/sponsors/ljharb" 670 | } 671 | }, 672 | "node_modules/is-generator-function": { 673 | "version": "1.0.10", 674 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 675 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 676 | "dependencies": { 677 | "has-tostringtag": "^1.0.0" 678 | }, 679 | "engines": { 680 | "node": ">= 0.4" 681 | }, 682 | "funding": { 683 | "url": "https://github.com/sponsors/ljharb" 684 | } 685 | }, 686 | "node_modules/is-typed-array": { 687 | "version": "1.1.10", 688 | "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", 689 | "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", 690 | "dependencies": { 691 | "available-typed-arrays": "^1.0.5", 692 | "call-bind": "^1.0.2", 693 | "for-each": "^0.3.3", 694 | "gopd": "^1.0.1", 695 | "has-tostringtag": "^1.0.0" 696 | }, 697 | "engines": { 698 | "node": ">= 0.4" 699 | }, 700 | "funding": { 701 | "url": "https://github.com/sponsors/ljharb" 702 | } 703 | }, 704 | "node_modules/isarray": { 705 | "version": "1.0.0", 706 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 707 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 708 | }, 709 | "node_modules/jmespath": { 710 | "version": "0.16.0", 711 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", 712 | "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", 713 | "engines": { 714 | "node": ">= 0.6.0" 715 | } 716 | }, 717 | "node_modules/make-error": { 718 | "version": "1.3.6", 719 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 720 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 721 | "dev": true 722 | }, 723 | "node_modules/querystring": { 724 | "version": "0.2.0", 725 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 726 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", 727 | "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", 728 | "engines": { 729 | "node": ">=0.4.x" 730 | } 731 | }, 732 | "node_modules/sax": { 733 | "version": "1.2.1", 734 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 735 | "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" 736 | }, 737 | "node_modules/source-map": { 738 | "version": "0.6.1", 739 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 740 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 741 | "engines": { 742 | "node": ">=0.10.0" 743 | } 744 | }, 745 | "node_modules/source-map-support": { 746 | "version": "0.5.19", 747 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 748 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 749 | "dependencies": { 750 | "buffer-from": "^1.0.0", 751 | "source-map": "^0.6.0" 752 | } 753 | }, 754 | "node_modules/ts-node": { 755 | "version": "8.10.1", 756 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.1.tgz", 757 | "integrity": "sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw==", 758 | "dev": true, 759 | "dependencies": { 760 | "arg": "^4.1.0", 761 | "diff": "^4.0.1", 762 | "make-error": "^1.1.1", 763 | "source-map-support": "^0.5.17", 764 | "yn": "3.1.1" 765 | }, 766 | "bin": { 767 | "ts-node": "dist/bin.js", 768 | "ts-node-script": "dist/bin-script.js", 769 | "ts-node-transpile-only": "dist/bin-transpile.js", 770 | "ts-script": "dist/bin-script-deprecated.js" 771 | }, 772 | "engines": { 773 | "node": ">=6.0.0" 774 | }, 775 | "peerDependencies": { 776 | "typescript": ">=2.7" 777 | } 778 | }, 779 | "node_modules/typescript": { 780 | "version": "3.9.2", 781 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", 782 | "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", 783 | "dev": true, 784 | "bin": { 785 | "tsc": "bin/tsc", 786 | "tsserver": "bin/tsserver" 787 | }, 788 | "engines": { 789 | "node": ">=4.2.0" 790 | } 791 | }, 792 | "node_modules/url": { 793 | "version": "0.10.3", 794 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 795 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 796 | "dependencies": { 797 | "punycode": "1.3.2", 798 | "querystring": "0.2.0" 799 | } 800 | }, 801 | "node_modules/url/node_modules/punycode": { 802 | "version": "1.3.2", 803 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 804 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 805 | }, 806 | "node_modules/util": { 807 | "version": "0.12.5", 808 | "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", 809 | "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", 810 | "dependencies": { 811 | "inherits": "^2.0.3", 812 | "is-arguments": "^1.0.4", 813 | "is-generator-function": "^1.0.7", 814 | "is-typed-array": "^1.1.3", 815 | "which-typed-array": "^1.1.2" 816 | } 817 | }, 818 | "node_modules/uuid": { 819 | "version": "8.0.0", 820 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", 821 | "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", 822 | "bin": { 823 | "uuid": "dist/bin/uuid" 824 | } 825 | }, 826 | "node_modules/which-typed-array": { 827 | "version": "1.1.9", 828 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", 829 | "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", 830 | "dependencies": { 831 | "available-typed-arrays": "^1.0.5", 832 | "call-bind": "^1.0.2", 833 | "for-each": "^0.3.3", 834 | "gopd": "^1.0.1", 835 | "has-tostringtag": "^1.0.0", 836 | "is-typed-array": "^1.1.10" 837 | }, 838 | "engines": { 839 | "node": ">= 0.4" 840 | }, 841 | "funding": { 842 | "url": "https://github.com/sponsors/ljharb" 843 | } 844 | }, 845 | "node_modules/xml2js": { 846 | "version": "0.5.0", 847 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 848 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 849 | "dependencies": { 850 | "sax": ">=0.6.0", 851 | "xmlbuilder": "~11.0.0" 852 | }, 853 | "engines": { 854 | "node": ">=4.0.0" 855 | } 856 | }, 857 | "node_modules/xmlbuilder": { 858 | "version": "11.0.1", 859 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 860 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 861 | "engines": { 862 | "node": ">=4.0" 863 | } 864 | }, 865 | "node_modules/yn": { 866 | "version": "3.1.1", 867 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 868 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 869 | "dev": true, 870 | "engines": { 871 | "node": ">=6" 872 | } 873 | } 874 | }, 875 | "dependencies": { 876 | "@aws-cdk/asset-awscli-v1": { 877 | "version": "2.2.209", 878 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.209.tgz", 879 | "integrity": "sha512-tL7aBDzx/QBuZoQso9OST2BMCoev89v01iQZicOKlR0J6vWQLPiqZfn4vd9nissFbM4X+xIwi3UKasPBTQL0WQ==" 880 | }, 881 | "@aws-cdk/asset-kubectl-v20": { 882 | "version": "2.1.3", 883 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", 884 | "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==" 885 | }, 886 | "@aws-cdk/asset-node-proxy-agent-v6": { 887 | "version": "2.1.0", 888 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", 889 | "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" 890 | }, 891 | "@aws-cdk/cloud-assembly-schema": { 892 | "version": "38.0.1", 893 | "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", 894 | "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", 895 | "requires": { 896 | "jsonschema": "^1.4.1", 897 | "semver": "^7.6.3" 898 | }, 899 | "dependencies": { 900 | "jsonschema": { 901 | "version": "1.4.1", 902 | "bundled": true 903 | }, 904 | "semver": { 905 | "version": "7.6.3", 906 | "bundled": true 907 | } 908 | } 909 | }, 910 | "@types/aws-lambda": { 911 | "version": "8.10.51", 912 | "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.51.tgz", 913 | "integrity": "sha512-XK7RerpXj4r+IO0r7qIeNqUSU6L4qhPMwNhISxozJJiUX/jdXj9WYzTShRVisEcUQHXgJ4TTBqTArM8f9Mjb8g==", 914 | "dev": true 915 | }, 916 | "@types/node": { 917 | "version": "14.0.1", 918 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz", 919 | "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==", 920 | "dev": true 921 | }, 922 | "arg": { 923 | "version": "4.1.3", 924 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 925 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 926 | "dev": true 927 | }, 928 | "available-typed-arrays": { 929 | "version": "1.0.5", 930 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", 931 | "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" 932 | }, 933 | "aws-cdk": { 934 | "version": "2.164.1", 935 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.164.1.tgz", 936 | "integrity": "sha512-dWRViQgHLe7GHkPIQGA+8EQSm8TBcxemyCC3HHW3wbLMWUDbspio9Dktmw5EmWxlFjjWh86Dk1JWf1zKQo8C5g==", 937 | "requires": { 938 | "fsevents": "2.3.2" 939 | } 940 | }, 941 | "aws-cdk-lib": { 942 | "version": "2.164.1", 943 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.164.1.tgz", 944 | "integrity": "sha512-jNvVmfZJbZoAYU94b5dzTlF2z6JXJ204NgcYY5haOa6mq3m2bzdYPXnPtB5kpAX3oBi++yoRdmLhqgckdEhUZA==", 945 | "requires": { 946 | "@aws-cdk/asset-awscli-v1": "^2.2.202", 947 | "@aws-cdk/asset-kubectl-v20": "^2.1.2", 948 | "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", 949 | "@aws-cdk/cloud-assembly-schema": "^38.0.0", 950 | "@balena/dockerignore": "^1.0.2", 951 | "case": "1.6.3", 952 | "fs-extra": "^11.2.0", 953 | "ignore": "^5.3.2", 954 | "jsonschema": "^1.4.1", 955 | "mime-types": "^2.1.35", 956 | "minimatch": "^3.1.2", 957 | "punycode": "^2.3.1", 958 | "semver": "^7.6.3", 959 | "table": "^6.8.2", 960 | "yaml": "1.10.2" 961 | }, 962 | "dependencies": { 963 | "@balena/dockerignore": { 964 | "version": "1.0.2", 965 | "bundled": true 966 | }, 967 | "ajv": { 968 | "version": "8.17.1", 969 | "bundled": true, 970 | "requires": { 971 | "fast-deep-equal": "^3.1.3", 972 | "fast-uri": "^3.0.1", 973 | "json-schema-traverse": "^1.0.0", 974 | "require-from-string": "^2.0.2" 975 | } 976 | }, 977 | "ansi-regex": { 978 | "version": "5.0.1", 979 | "bundled": true 980 | }, 981 | "ansi-styles": { 982 | "version": "4.3.0", 983 | "bundled": true, 984 | "requires": { 985 | "color-convert": "^2.0.1" 986 | } 987 | }, 988 | "astral-regex": { 989 | "version": "2.0.0", 990 | "bundled": true 991 | }, 992 | "balanced-match": { 993 | "version": "1.0.2", 994 | "bundled": true 995 | }, 996 | "brace-expansion": { 997 | "version": "1.1.11", 998 | "bundled": true, 999 | "requires": { 1000 | "balanced-match": "^1.0.0", 1001 | "concat-map": "0.0.1" 1002 | } 1003 | }, 1004 | "case": { 1005 | "version": "1.6.3", 1006 | "bundled": true 1007 | }, 1008 | "color-convert": { 1009 | "version": "2.0.1", 1010 | "bundled": true, 1011 | "requires": { 1012 | "color-name": "~1.1.4" 1013 | } 1014 | }, 1015 | "color-name": { 1016 | "version": "1.1.4", 1017 | "bundled": true 1018 | }, 1019 | "concat-map": { 1020 | "version": "0.0.1", 1021 | "bundled": true 1022 | }, 1023 | "emoji-regex": { 1024 | "version": "8.0.0", 1025 | "bundled": true 1026 | }, 1027 | "fast-deep-equal": { 1028 | "version": "3.1.3", 1029 | "bundled": true 1030 | }, 1031 | "fast-uri": { 1032 | "version": "3.0.1", 1033 | "bundled": true 1034 | }, 1035 | "fs-extra": { 1036 | "version": "11.2.0", 1037 | "bundled": true, 1038 | "requires": { 1039 | "graceful-fs": "^4.2.0", 1040 | "jsonfile": "^6.0.1", 1041 | "universalify": "^2.0.0" 1042 | } 1043 | }, 1044 | "graceful-fs": { 1045 | "version": "4.2.11", 1046 | "bundled": true 1047 | }, 1048 | "ignore": { 1049 | "version": "5.3.2", 1050 | "bundled": true 1051 | }, 1052 | "is-fullwidth-code-point": { 1053 | "version": "3.0.0", 1054 | "bundled": true 1055 | }, 1056 | "json-schema-traverse": { 1057 | "version": "1.0.0", 1058 | "bundled": true 1059 | }, 1060 | "jsonfile": { 1061 | "version": "6.1.0", 1062 | "bundled": true, 1063 | "requires": { 1064 | "graceful-fs": "^4.1.6", 1065 | "universalify": "^2.0.0" 1066 | } 1067 | }, 1068 | "jsonschema": { 1069 | "version": "1.4.1", 1070 | "bundled": true 1071 | }, 1072 | "lodash.truncate": { 1073 | "version": "4.4.2", 1074 | "bundled": true 1075 | }, 1076 | "mime-db": { 1077 | "version": "1.52.0", 1078 | "bundled": true 1079 | }, 1080 | "mime-types": { 1081 | "version": "2.1.35", 1082 | "bundled": true, 1083 | "requires": { 1084 | "mime-db": "1.52.0" 1085 | } 1086 | }, 1087 | "minimatch": { 1088 | "version": "3.1.2", 1089 | "bundled": true, 1090 | "requires": { 1091 | "brace-expansion": "^1.1.7" 1092 | } 1093 | }, 1094 | "punycode": { 1095 | "version": "2.3.1", 1096 | "bundled": true 1097 | }, 1098 | "require-from-string": { 1099 | "version": "2.0.2", 1100 | "bundled": true 1101 | }, 1102 | "semver": { 1103 | "version": "7.6.3", 1104 | "bundled": true 1105 | }, 1106 | "slice-ansi": { 1107 | "version": "4.0.0", 1108 | "bundled": true, 1109 | "requires": { 1110 | "ansi-styles": "^4.0.0", 1111 | "astral-regex": "^2.0.0", 1112 | "is-fullwidth-code-point": "^3.0.0" 1113 | } 1114 | }, 1115 | "string-width": { 1116 | "version": "4.2.3", 1117 | "bundled": true, 1118 | "requires": { 1119 | "emoji-regex": "^8.0.0", 1120 | "is-fullwidth-code-point": "^3.0.0", 1121 | "strip-ansi": "^6.0.1" 1122 | } 1123 | }, 1124 | "strip-ansi": { 1125 | "version": "6.0.1", 1126 | "bundled": true, 1127 | "requires": { 1128 | "ansi-regex": "^5.0.1" 1129 | } 1130 | }, 1131 | "table": { 1132 | "version": "6.8.2", 1133 | "bundled": true, 1134 | "requires": { 1135 | "ajv": "^8.0.1", 1136 | "lodash.truncate": "^4.4.2", 1137 | "slice-ansi": "^4.0.0", 1138 | "string-width": "^4.2.3", 1139 | "strip-ansi": "^6.0.1" 1140 | } 1141 | }, 1142 | "universalify": { 1143 | "version": "2.0.1", 1144 | "bundled": true 1145 | }, 1146 | "yaml": { 1147 | "version": "1.10.2", 1148 | "bundled": true 1149 | } 1150 | } 1151 | }, 1152 | "aws-sdk": { 1153 | "version": "2.1354.0", 1154 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1354.0.tgz", 1155 | "integrity": "sha512-3aDxvyuOqMB9DqJguCq6p8momdsz0JR1axwkWOOCzHA7a35+Bw+WLmqt3pWwRjR1tGIwkkZ2CvGJObYHsOuw3w==", 1156 | "requires": { 1157 | "buffer": "4.9.2", 1158 | "events": "1.1.1", 1159 | "ieee754": "1.1.13", 1160 | "jmespath": "0.16.0", 1161 | "querystring": "0.2.0", 1162 | "sax": "1.2.1", 1163 | "url": "0.10.3", 1164 | "util": "^0.12.4", 1165 | "uuid": "8.0.0", 1166 | "xml2js": "0.5.0" 1167 | } 1168 | }, 1169 | "base64-js": { 1170 | "version": "1.5.1", 1171 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1172 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 1173 | }, 1174 | "buffer": { 1175 | "version": "4.9.2", 1176 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 1177 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 1178 | "requires": { 1179 | "base64-js": "^1.0.2", 1180 | "ieee754": "^1.1.4", 1181 | "isarray": "^1.0.0" 1182 | } 1183 | }, 1184 | "buffer-from": { 1185 | "version": "1.1.1", 1186 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 1187 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 1188 | }, 1189 | "call-bind": { 1190 | "version": "1.0.2", 1191 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 1192 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 1193 | "requires": { 1194 | "function-bind": "^1.1.1", 1195 | "get-intrinsic": "^1.0.2" 1196 | } 1197 | }, 1198 | "constructs": { 1199 | "version": "10.4.2", 1200 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", 1201 | "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", 1202 | "peer": true 1203 | }, 1204 | "diff": { 1205 | "version": "4.0.2", 1206 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 1207 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 1208 | "dev": true 1209 | }, 1210 | "events": { 1211 | "version": "1.1.1", 1212 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 1213 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 1214 | }, 1215 | "for-each": { 1216 | "version": "0.3.3", 1217 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", 1218 | "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", 1219 | "requires": { 1220 | "is-callable": "^1.1.3" 1221 | } 1222 | }, 1223 | "fsevents": { 1224 | "version": "2.3.2", 1225 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1226 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1227 | "optional": true 1228 | }, 1229 | "function-bind": { 1230 | "version": "1.1.1", 1231 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 1232 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 1233 | }, 1234 | "get-intrinsic": { 1235 | "version": "1.2.0", 1236 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", 1237 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", 1238 | "requires": { 1239 | "function-bind": "^1.1.1", 1240 | "has": "^1.0.3", 1241 | "has-symbols": "^1.0.3" 1242 | } 1243 | }, 1244 | "gopd": { 1245 | "version": "1.0.1", 1246 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 1247 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 1248 | "requires": { 1249 | "get-intrinsic": "^1.1.3" 1250 | } 1251 | }, 1252 | "has": { 1253 | "version": "1.0.3", 1254 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1255 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1256 | "requires": { 1257 | "function-bind": "^1.1.1" 1258 | } 1259 | }, 1260 | "has-symbols": { 1261 | "version": "1.0.3", 1262 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 1263 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 1264 | }, 1265 | "has-tostringtag": { 1266 | "version": "1.0.0", 1267 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", 1268 | "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", 1269 | "requires": { 1270 | "has-symbols": "^1.0.2" 1271 | } 1272 | }, 1273 | "ieee754": { 1274 | "version": "1.1.13", 1275 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 1276 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 1277 | }, 1278 | "inherits": { 1279 | "version": "2.0.4", 1280 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1281 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1282 | }, 1283 | "is-arguments": { 1284 | "version": "1.1.1", 1285 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", 1286 | "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", 1287 | "requires": { 1288 | "call-bind": "^1.0.2", 1289 | "has-tostringtag": "^1.0.0" 1290 | } 1291 | }, 1292 | "is-callable": { 1293 | "version": "1.2.7", 1294 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 1295 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" 1296 | }, 1297 | "is-generator-function": { 1298 | "version": "1.0.10", 1299 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 1300 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 1301 | "requires": { 1302 | "has-tostringtag": "^1.0.0" 1303 | } 1304 | }, 1305 | "is-typed-array": { 1306 | "version": "1.1.10", 1307 | "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", 1308 | "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", 1309 | "requires": { 1310 | "available-typed-arrays": "^1.0.5", 1311 | "call-bind": "^1.0.2", 1312 | "for-each": "^0.3.3", 1313 | "gopd": "^1.0.1", 1314 | "has-tostringtag": "^1.0.0" 1315 | } 1316 | }, 1317 | "isarray": { 1318 | "version": "1.0.0", 1319 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1320 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 1321 | }, 1322 | "jmespath": { 1323 | "version": "0.16.0", 1324 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", 1325 | "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" 1326 | }, 1327 | "make-error": { 1328 | "version": "1.3.6", 1329 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 1330 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 1331 | "dev": true 1332 | }, 1333 | "querystring": { 1334 | "version": "0.2.0", 1335 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1336 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 1337 | }, 1338 | "sax": { 1339 | "version": "1.2.1", 1340 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 1341 | "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" 1342 | }, 1343 | "source-map": { 1344 | "version": "0.6.1", 1345 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1346 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 1347 | }, 1348 | "source-map-support": { 1349 | "version": "0.5.19", 1350 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 1351 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 1352 | "requires": { 1353 | "buffer-from": "^1.0.0", 1354 | "source-map": "^0.6.0" 1355 | } 1356 | }, 1357 | "ts-node": { 1358 | "version": "8.10.1", 1359 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.1.tgz", 1360 | "integrity": "sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw==", 1361 | "dev": true, 1362 | "requires": { 1363 | "arg": "^4.1.0", 1364 | "diff": "^4.0.1", 1365 | "make-error": "^1.1.1", 1366 | "source-map-support": "^0.5.17", 1367 | "yn": "3.1.1" 1368 | } 1369 | }, 1370 | "typescript": { 1371 | "version": "3.9.2", 1372 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", 1373 | "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", 1374 | "dev": true 1375 | }, 1376 | "url": { 1377 | "version": "0.10.3", 1378 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 1379 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 1380 | "requires": { 1381 | "punycode": "1.3.2", 1382 | "querystring": "0.2.0" 1383 | }, 1384 | "dependencies": { 1385 | "punycode": { 1386 | "version": "1.3.2", 1387 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 1388 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 1389 | } 1390 | } 1391 | }, 1392 | "util": { 1393 | "version": "0.12.5", 1394 | "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", 1395 | "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", 1396 | "requires": { 1397 | "inherits": "^2.0.3", 1398 | "is-arguments": "^1.0.4", 1399 | "is-generator-function": "^1.0.7", 1400 | "is-typed-array": "^1.1.3", 1401 | "which-typed-array": "^1.1.2" 1402 | } 1403 | }, 1404 | "uuid": { 1405 | "version": "8.0.0", 1406 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", 1407 | "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" 1408 | }, 1409 | "which-typed-array": { 1410 | "version": "1.1.9", 1411 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", 1412 | "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", 1413 | "requires": { 1414 | "available-typed-arrays": "^1.0.5", 1415 | "call-bind": "^1.0.2", 1416 | "for-each": "^0.3.3", 1417 | "gopd": "^1.0.1", 1418 | "has-tostringtag": "^1.0.0", 1419 | "is-typed-array": "^1.1.10" 1420 | } 1421 | }, 1422 | "xml2js": { 1423 | "version": "0.5.0", 1424 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 1425 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 1426 | "requires": { 1427 | "sax": ">=0.6.0", 1428 | "xmlbuilder": "~11.0.0" 1429 | } 1430 | }, 1431 | "xmlbuilder": { 1432 | "version": "11.0.1", 1433 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 1434 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 1435 | }, 1436 | "yn": { 1437 | "version": "3.1.1", 1438 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1439 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1440 | "dev": true 1441 | } 1442 | } 1443 | } 1444 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cognito-example-for-external-idp-cdk", 3 | "version": "0.1.2", 4 | "author": "Eran Medan", 5 | "license": "MIT-0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/aws-samples/amazon-cognito-example-for-external-idp.git" 9 | }, 10 | "description": "Example for Identity and Access Control for Custom Enterprise Applications", 11 | "scripts": { 12 | "ts-node": "ts-node", 13 | "tsc": "tsc", 14 | "build": "tsc -b", 15 | "watch": "tsc -w", 16 | "clean": "tsc -b --clean && rm -rf cdk.out", 17 | "cdk": "cdk", 18 | "cdk-synth": "cdk synth", 19 | "cdk-diff": "cdk diff || true", 20 | "cdk-deploy": "cdk deploy", 21 | "cdk-destroy": "cdk destroy", 22 | "cdk-bootstrap": "cdk bootstrap", 23 | "generate-config": "ts-node ./src/generateConfig.ts" 24 | }, 25 | "devDependencies": { 26 | "@types/aws-lambda": "^8.10.51", 27 | "@types/node": "^14.0.1", 28 | "ts-node": "^8.10.1", 29 | "typescript": "^3.9.2" 30 | }, 31 | "dependencies": { 32 | "aws-cdk": "^2.164.1", 33 | "aws-cdk-lib": "^2.164.1", 34 | "aws-sdk": "^2.1354.0", 35 | "source-map-support": "^0.5.19" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cdk/src/cdk.ts: -------------------------------------------------------------------------------- 1 | import apigateway = require("aws-cdk-lib/aws-apigateway"); 2 | import cdk = require("aws-cdk-lib/core"); 3 | import dynamodb = require("aws-cdk-lib/aws-dynamodb"); 4 | import lambda = require("aws-cdk-lib/aws-lambda"); 5 | import cognito = require("aws-cdk-lib/aws-cognito"); 6 | import iam = require("aws-cdk-lib/aws-iam"); 7 | import s3 = require("aws-cdk-lib/aws-s3"); 8 | import cloudfront = require("aws-cdk-lib/aws-cloudfront"); 9 | import { BillingMode, StreamViewType } from "aws-cdk-lib/aws-dynamodb"; 10 | import "source-map-support/register"; 11 | import { AuthorizationType } from "aws-cdk-lib/aws-apigateway"; 12 | import { 13 | CfnUserPool, 14 | CfnUserPoolIdentityProvider, 15 | UserPool, 16 | } from "aws-cdk-lib/aws-cognito"; 17 | import { Utils } from "./utils"; 18 | import { Runtime } from "aws-cdk-lib/aws-lambda"; 19 | 20 | import { URL } from "url"; 21 | import { Duration } from "aws-cdk-lib/core"; 22 | import { Bucket } from "aws-cdk-lib/aws-s3"; 23 | import { Distribution } from "aws-cdk-lib/aws-cloudfront"; 24 | import origins = require("aws-cdk-lib/aws-cloudfront-origins") 25 | /** 26 | * Define a CloudFormation stack that creates a serverless application with 27 | * Amazon Cognito and an external SAML based IdP 28 | */ 29 | export class BackendStack extends cdk.Stack { 30 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 31 | super(scope, id, props); 32 | 33 | // ======================================================================== 34 | // Environment variables and constants 35 | // ======================================================================== 36 | 37 | const domain = Utils.getEnv("COGNITO_DOMAIN_NAME"); 38 | const identityProviderName = Utils.getEnv("IDENTITY_PROVIDER_NAME", ""); 39 | const OIDCProviderName = Utils.getEnv("OIDC_PROVIDER_NAME", ""); 40 | 41 | const identityProviderMetadataURLOrFile = Utils.getEnv( 42 | "IDENTITY_PROVIDER_METADATA", 43 | "" 44 | ); 45 | const OIDCClientId = Utils.getEnv('OIDC_CLIENT_ID', '') 46 | const OIDCClientSecret = Utils.getEnv('OIDC_CLIENT_SECRET', '') 47 | const OIDCIssuerUrl = Utils.getEnv('OIDC_ISSUER_URL', '') 48 | 49 | const appFrontendDeployMode = Utils.getEnv("APP_FRONTEND_DEPLOY_MODE", ""); 50 | 51 | const groupsAttributeName = Utils.getEnv("GROUPS_ATTRIBUTE_NAME", "groups"); 52 | const adminsGroupName = Utils.getEnv("ADMINS_GROUP_NAME", "pet-app-admins"); 53 | const usersGroupName = Utils.getEnv("USERS_GROUP_NAME", "pet-app-users"); 54 | const lambdaMemory = parseInt(Utils.getEnv("LAMBDA_MEMORY", "128")); 55 | const nodeRuntime: Runtime = lambda.Runtime.NODEJS_20_X; 56 | const authorizationHeaderName = "Authorization"; 57 | const groupsAttributeClaimName = "custom:" + groupsAttributeName; 58 | 59 | // ======================================================================== 60 | // Resource: (optional) S3 bucket / CloudFront distribution 61 | // ======================================================================== 62 | 63 | // Purpose: store the static frontend assets (the app's user interface) 64 | 65 | const isModeS3 = appFrontendDeployMode === "s3"; 66 | const isModeCloudfront = appFrontendDeployMode === "cloudfront"; 67 | let appUrl = Utils.getEnv("APP_URL", ""); 68 | let uiBucketName: string | undefined = undefined; 69 | let corsOrigin: string | undefined = undefined; 70 | if (isModeS3 || isModeCloudfront) { 71 | const uiBucket: Bucket = new s3.Bucket(this, "UIBucket"); 72 | uiBucketName = uiBucket.bucketName; 73 | 74 | if (isModeS3) { 75 | // s3 mode, for development / testing only 76 | appUrl = "https://" + uiBucket.bucketRegionalDomainName + "/index.html"; 77 | corsOrigin = "https://" + uiBucket.bucketRegionalDomainName; 78 | } else { 79 | // cloudfront mode 80 | const distribution = this.createCloudFrontDistribution(uiBucket); 81 | 82 | if (!appUrl) { 83 | // if appUrl ws not specified, use the distribution URL 84 | appUrl = "https://" + distribution.domainName; 85 | corsOrigin = "https://" + distribution.domainName; 86 | } 87 | } 88 | } 89 | 90 | if (!appUrl) { 91 | // if not s3 or cloudfront, APP_URL must be defined 92 | throw new Error(`APP_URL environment variable must be defined`); 93 | } 94 | 95 | if (!corsOrigin) { 96 | // if corsOrigin ws not set dynamically, get it from the appUrl 97 | corsOrigin = new URL(appUrl).origin; 98 | } 99 | 100 | // ======================================================================== 101 | // Resource: Pre Token Generation function 102 | // ======================================================================== 103 | 104 | // Purpose: map from a custom attribute mapped from SAML, e.g. {..., "custom:groups":"[a,b,c]", ...} 105 | // to cognito:groups claim, e.g. {..., "cognito:groups":["a","b","c"], ...} 106 | // it can also optionally add roles and preferred_role claims 107 | 108 | // See also: 109 | // - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html 110 | 111 | const preTokenGeneration = new lambda.Function(this, "PreTokenGeneration", { 112 | runtime: nodeRuntime, 113 | handler: "index.handler", 114 | code: lambda.Code.fromAsset("../lambda/pretokengeneration/dist/src"), 115 | environment: { 116 | GROUPS_ATTRIBUTE_CLAIM_NAME: groupsAttributeClaimName, 117 | }, 118 | }); 119 | 120 | // ======================================================================== 121 | // Resource: Amazon Cognito User Pool 122 | // ======================================================================== 123 | 124 | // Purpose: creates a user directory and allows federation from external IdPs 125 | 126 | // See also: 127 | // - https://aws.amazon.com/cognito/ 128 | // - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-cdk-lib_aws-cognito.CfnIdentityPool.html 129 | 130 | // high level construct 131 | const userPool: UserPool = new cognito.UserPool(this, id + "Pool", { 132 | signInAliases: { email: true }, 133 | autoVerify: { email: true }, 134 | lambdaTriggers: { preTokenGeneration: preTokenGeneration }, 135 | }); 136 | 137 | // any properties that are not part of the high level construct can be added using this method 138 | const userPoolCfn = userPool.node.defaultChild as CfnUserPool; 139 | userPoolCfn.userPoolAddOns = { advancedSecurityMode: "ENFORCED" }; 140 | userPoolCfn.schema = [ 141 | { 142 | name: groupsAttributeName, 143 | attributeDataType: "String", 144 | mutable: true, 145 | required: false, 146 | stringAttributeConstraints: { 147 | maxLength: "2000", 148 | }, 149 | }, 150 | ]; 151 | 152 | // create two groups, one for admins one for users 153 | // these groups can be used without configuring a 3rd party IdP 154 | 155 | new cognito.CfnUserPoolGroup(this, "AdminsGroup", { 156 | groupName: adminsGroupName, 157 | userPoolId: userPool.userPoolId, 158 | }); 159 | 160 | new cognito.CfnUserPoolGroup(this, "UsersGroup", { 161 | groupName: usersGroupName, 162 | userPoolId: userPool.userPoolId, 163 | }); 164 | 165 | // ======================================================================== 166 | // Resource: Amazon DynamoDB Table 167 | // ======================================================================== 168 | 169 | // Purpose: serverless, pay as you go, persistent storage for the demo app 170 | 171 | // See also: 172 | // - https://aws.amazon.com/dynamodb/ 173 | // - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-dynamodb-readme.html 174 | 175 | const itemsTable = new dynamodb.Table(this, "ItemsTable", { 176 | billingMode: BillingMode.PAY_PER_REQUEST, 177 | encryption: dynamodb.TableEncryption.AWS_MANAGED, 178 | stream: StreamViewType.NEW_AND_OLD_IMAGES, 179 | partitionKey: { name: "id", type: dynamodb.AttributeType.STRING }, 180 | }); 181 | 182 | const usersTable = new dynamodb.Table(this, "UsersTable", { 183 | billingMode: BillingMode.PAY_PER_REQUEST, 184 | encryption: dynamodb.TableEncryption.AWS_MANAGED, 185 | stream: StreamViewType.NEW_AND_OLD_IMAGES, 186 | partitionKey: { name: "username", type: dynamodb.AttributeType.STRING }, 187 | timeToLiveAttribute: "ttl", 188 | }); 189 | 190 | // ======================================================================== 191 | // Resource: AWS Lambda Function - CRUD API Backend 192 | // ======================================================================== 193 | 194 | // Purpose: serverless backend for the demo app, uses express.js 195 | 196 | // See also: 197 | // - https://aws.amazon.com/lambda/ 198 | // - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-readme.html 199 | 200 | const apiFunction = new lambda.Function(this, "APIFunction", { 201 | runtime: nodeRuntime, 202 | handler: "index.handler", 203 | code: lambda.Code.fromAsset("../lambda/api/dist/src"), 204 | timeout: Duration.seconds(30), 205 | memorySize: lambdaMemory, 206 | environment: { 207 | ITEMS_TABLE_NAME: itemsTable.tableName, 208 | USERS_TABLE_NAME: usersTable.tableName, 209 | ALLOWED_ORIGIN: corsOrigin, 210 | ADMINS_GROUP_NAME: adminsGroupName, 211 | USERS_GROUP_NAME: usersGroupName, 212 | USER_POOL_ID: userPool.userPoolId, 213 | AUTHORIZATION_HEADER_NAME: authorizationHeaderName, 214 | }, 215 | }); 216 | 217 | // grant the lambda full access to the tables (for a high level construct, we have a syntactic sugar way of doing it 218 | itemsTable.grantReadWriteData(apiFunction.role!); 219 | usersTable.grantReadWriteData(apiFunction.role!); 220 | 221 | // for Cfn building blocks, we need to create the policy 222 | // in here we allow us to do a global sign out from the backend, to avoid having to give users a stronger scope 223 | apiFunction.addToRolePolicy( 224 | new iam.PolicyStatement({ 225 | resources: [userPool.userPoolArn], 226 | actions: [ 227 | "cognito-idp:AdminUserGlobalSignOut", 228 | "cognito-idp:AdminGetUser", 229 | ], 230 | }) 231 | ); 232 | 233 | // ======================================================================== 234 | // Resource: Amazon API Gateway - API endpoints 235 | // ======================================================================== 236 | 237 | // Purpose: create API endpoints and integrate with Amazon Cognito for JWT validation 238 | 239 | // See also: 240 | // - https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html 241 | 242 | // ------------------------------------------------------------------------ 243 | // The API 244 | // ------------------------------------------------------------------------ 245 | 246 | const api = new apigateway.RestApi(this, id + "API"); 247 | const integration = new apigateway.LambdaIntegration(apiFunction, { 248 | // lambda proxy integration: 249 | // see https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-create-api-as-simple-proxy 250 | proxy: true, 251 | }); 252 | 253 | // ------------------------------------------------------------------------ 254 | // Cognito Authorizer 255 | // ------------------------------------------------------------------------ 256 | 257 | const cfnAuthorizer = new apigateway.CfnAuthorizer(this, id, { 258 | name: "CognitoAuthorizer", 259 | type: AuthorizationType.COGNITO, 260 | 261 | identitySource: "method.request.header." + authorizationHeaderName, 262 | restApiId: api.restApiId, 263 | providerArns: [userPool.userPoolArn], 264 | }); 265 | 266 | const rootResource = api.root; 267 | 268 | // all paths require the cognito authorizer (validates the JWT and passes it to the lambda) 269 | const proxyResource = rootResource.addResource("{proxy+}"); 270 | 271 | const method = proxyResource.addMethod("ANY", integration, { 272 | authorizer: { authorizerId: cfnAuthorizer.ref }, 273 | authorizationType: AuthorizationType.COGNITO, 274 | }); 275 | 276 | // uncomment to use an access token instead of an id token 277 | 278 | // const cfnMethod = method.node.defaultChild as apigateway.CfnMethod; 279 | // cfnMethod.authorizationScopes = ["openid"]; 280 | 281 | // ------------------------------------------------------------------------ 282 | // Add CORS support to all 283 | // ------------------------------------------------------------------------ 284 | 285 | Utils.addCorsOptions(proxyResource, corsOrigin); 286 | Utils.addCorsOptions(rootResource, corsOrigin); 287 | 288 | // ======================================================================== 289 | // Resource: Identity Provider Settings 290 | // ======================================================================== 291 | 292 | // Purpose: define the external Identity Provider details, field mappings etc. 293 | 294 | // See also: 295 | // - https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp.html 296 | 297 | // mapping from IdP fields to Cognito attributes 298 | const supportedIdentityProviders = ["COGNITO"]; 299 | let cognitoIdp: CfnUserPoolIdentityProvider | undefined = undefined; 300 | 301 | if (identityProviderMetadataURLOrFile && identityProviderName) { 302 | cognitoIdp = new cognito.CfnUserPoolIdentityProvider(this, "CognitoIdP", { 303 | providerName: identityProviderName, 304 | providerDetails: Utils.isURL(identityProviderMetadataURLOrFile) 305 | ? { 306 | MetadataURL: identityProviderMetadataURLOrFile, 307 | } 308 | : { 309 | MetadataFile: identityProviderMetadataURLOrFile, 310 | }, 311 | providerType: "SAML", 312 | // Structure: { "": "" } 313 | attributeMapping: { 314 | email: "email", 315 | family_name: "lastName", 316 | given_name: "firstName", 317 | name: "firstName", // alias to given_name 318 | [groupsAttributeClaimName]: "groups", //syntax for a dynamic key 319 | }, 320 | userPoolId: userPool.userPoolId, 321 | }); 322 | 323 | supportedIdentityProviders.push(identityProviderName); 324 | } 325 | 326 | if (OIDCProviderName && OIDCClientId && OIDCClientSecret && OIDCIssuerUrl) { 327 | const oidcProvider = new cognito.UserPoolIdentityProviderOidc(this, 'OidcProvider', { 328 | userPool, 329 | name: OIDCProviderName, 330 | clientId: OIDCClientId, 331 | clientSecret: OIDCClientSecret, 332 | issuerUrl: OIDCIssuerUrl, 333 | attributeRequestMethod: cognito.OidcAttributeRequestMethod.GET, 334 | scopes: ['openid', 'profile', 'email'], 335 | attributeMapping: { 336 | email: cognito.ProviderAttribute.other('email'), 337 | givenName: cognito.ProviderAttribute.other('given_name'), 338 | familyName: cognito.ProviderAttribute.other('family_name'), 339 | custom: { 340 | [groupsAttributeClaimName]: cognito.ProviderAttribute.other('groups'), 341 | } 342 | }, 343 | }); 344 | 345 | supportedIdentityProviders.push(OIDCProviderName); 346 | } 347 | 348 | // ======================================================================== 349 | // Resource: Cognito App Client 350 | // ======================================================================== 351 | 352 | // Purpose: each app needs an app client defined, where app specific details are set, such as redirect URIs 353 | 354 | // See also: 355 | // - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html 356 | 357 | const cfnUserPoolClient = new cognito.CfnUserPoolClient( 358 | this, 359 | "CognitoAppClient", 360 | { 361 | supportedIdentityProviders: supportedIdentityProviders, 362 | clientName: "Web", 363 | allowedOAuthFlowsUserPoolClient: true, 364 | allowedOAuthFlows: ["code"], 365 | allowedOAuthScopes: ["phone", "email", "openid", "profile"], 366 | explicitAuthFlows: ["ALLOW_REFRESH_TOKEN_AUTH"], 367 | preventUserExistenceErrors: "ENABLED", 368 | generateSecret: false, 369 | refreshTokenValidity: 1, 370 | callbackUrLs: [appUrl], 371 | logoutUrLs: [appUrl], 372 | userPoolId: userPool.userPoolId, 373 | } 374 | ); 375 | 376 | // we want to make sure we do things in the right order 377 | if (cognitoIdp) { 378 | cfnUserPoolClient.node.addDependency(cognitoIdp); 379 | } 380 | 381 | // ======================================================================== 382 | // Resource: Cognito Auth Domain 383 | // ======================================================================== 384 | 385 | // Purpose: creates / updates the custom subdomain for cognito's hosted UI 386 | 387 | // See also: 388 | // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html 389 | 390 | const cfnUserPoolDomain = new cognito.CfnUserPoolDomain( 391 | this, 392 | "CognitoDomain", 393 | { 394 | domain: domain, 395 | userPoolId: userPool.userPoolId, 396 | } 397 | ); 398 | 399 | // ======================================================================== 400 | // Stack Outputs 401 | // ======================================================================== 402 | 403 | // Publish the custom resource output 404 | new cdk.CfnOutput(this, "APIUrlOutput", { 405 | description: "API URL", 406 | value: api.url, 407 | }); 408 | 409 | new cdk.CfnOutput(this, "UserPoolIdOutput", { 410 | description: "UserPool ID", 411 | value: userPool.userPoolId, 412 | }); 413 | 414 | new cdk.CfnOutput(this, "AppClientIdOutput", { 415 | description: "App Client ID", 416 | value: cfnUserPoolClient.ref, 417 | }); 418 | 419 | new cdk.CfnOutput(this, "RegionOutput", { 420 | description: "Region", 421 | value: this.region, 422 | }); 423 | 424 | new cdk.CfnOutput(this, "CognitoDomainOutput", { 425 | description: "Cognito Domain", 426 | value: cfnUserPoolDomain.domain, 427 | }); 428 | 429 | new cdk.CfnOutput(this, "LambdaFunctionName", { 430 | description: "Lambda Function Name", 431 | value: apiFunction.functionName, 432 | }); 433 | 434 | new cdk.CfnOutput(this, "AppUrl", { 435 | description: "The frontend app's URL", 436 | value: appUrl, 437 | }); 438 | 439 | if (uiBucketName) { 440 | new cdk.CfnOutput(this, "UIBucketName", { 441 | description: "The frontend app's bucket name", 442 | value: uiBucketName, 443 | }); 444 | } 445 | } 446 | 447 | private createCloudFrontDistribution( 448 | uiBucket: Bucket 449 | ): Distribution { 450 | // create CloudFront distribution 451 | const distribution = new cloudfront.Distribution( 452 | this, 453 | "UIDistribution", { 454 | defaultBehavior: { 455 | origin: origins.S3BucketOrigin.withOriginAccessControl(uiBucket) 456 | }, 457 | defaultRootObject: "index.html" 458 | } 459 | ); 460 | 461 | return distribution; 462 | } 463 | } 464 | 465 | // generate the CDK app and stack 466 | 467 | const app = new cdk.App(); 468 | 469 | const stackName = Utils.getEnv("STACK_NAME"); 470 | const stackAccount = Utils.getEnv("STACK_ACCOUNT"); 471 | const stackRegion = Utils.getEnv("STACK_REGION"); 472 | 473 | // The AWS CDK team recommends that you explicitly set your account and region using the env property on a stack when 474 | // you deploy stacks to production. 475 | // see https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html 476 | 477 | const stackProps = { env: { region: stackRegion, account: stackAccount } }; 478 | const backendStack = new BackendStack(app, stackName, stackProps); 479 | 480 | backendStack.templateOptions.transforms = ["AWS::Serverless-2016-10-31"]; 481 | -------------------------------------------------------------------------------- /cdk/src/generateConfig.ts: -------------------------------------------------------------------------------- 1 | import {Utils} from "./utils"; 2 | import fs = require("fs"); 3 | 4 | 5 | class GenerateConfig { 6 | 7 | async generateConfig(stackName: string, stackRegion: string, filePath: string) { 8 | 9 | const outputs = await Utils.getStackOutputs(stackName, stackRegion); 10 | const outputsByName = new Map(); 11 | for (let output of outputs) { 12 | outputsByName.set(output.OutputKey!, output.OutputValue!); 13 | } 14 | 15 | const region = outputsByName.get("RegionOutput"); 16 | const cognitoDomainPrefix = outputsByName.get("CognitoDomainOutput"); 17 | const userPoolId = outputsByName.get("UserPoolIdOutput"); 18 | const appClientId = outputsByName.get("AppClientIdOutput"); 19 | const apiURL = outputsByName.get("APIUrlOutput"); 20 | const appURL = outputsByName.get("AppUrl"); 21 | const uiBucketName = outputsByName.get("UIBucketName") || ""; 22 | 23 | const cognitoDomain = `${cognitoDomainPrefix}.auth.${region}.amazoncognito.com`; 24 | const params = { 25 | cognitoDomain: cognitoDomain, 26 | region: region, 27 | cognitoUserPoolId: userPoolId, 28 | cognitoUserPoolAppClientId: appClientId, 29 | apiUrl: apiURL, 30 | appUrl: appURL, 31 | uiBucketName: uiBucketName 32 | }; 33 | 34 | const autoGenConfigFile = "// this file is auto generated, do not edit it directly\n" + 35 | "export default " + JSON.stringify(params, null, 2); 36 | 37 | console.log(autoGenConfigFile); 38 | 39 | fs.writeFileSync(filePath, autoGenConfigFile); 40 | 41 | console.log(` 42 | 43 | IdP Settings: 44 | 45 | - Single sign on URL / Assertion Consumer Service (ACS) URL: https://${cognitoDomain}/saml2/idpresponse 46 | - Audience URI (SP Entity ID): urn:amazon:cognito:sp:${userPoolId} 47 | - Group Attribute Statements (optional): Name=groups, Filter=Starts With (prefix) / Regex (.*) 48 | 49 | `) 50 | 51 | } 52 | } 53 | 54 | const stackName = process.argv[2]; 55 | if (!stackName) { 56 | throw new Error("stack name is required"); 57 | } 58 | const stackRegion = process.argv[3]; 59 | if (!stackName) { 60 | throw new Error("stack region is required"); 61 | } 62 | const filePath = process.argv[4]; 63 | if (!stackName) { 64 | throw new Error("file path is required"); 65 | } 66 | 67 | new GenerateConfig().generateConfig(stackName, stackRegion, filePath); 68 | -------------------------------------------------------------------------------- /cdk/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as aws from "aws-sdk"; 2 | import {CloudFormation} from "aws-sdk"; 3 | import apigateway = require("aws-cdk-lib/aws-apigateway"); 4 | import {URL} from "url"; 5 | 6 | export class Utils { 7 | static async getStackOutputs(stackName: string, stackRegion: string): Promise { 8 | aws.config.region = stackRegion; 9 | const cfn = new aws.CloudFormation(); 10 | const result = await cfn.describeStacks({StackName: stackName}).promise(); 11 | return result.Stacks![0].Outputs!; 12 | } 13 | 14 | static getEnv(variableName: string, defaultValue?: string) { 15 | const variable = process.env[variableName]; 16 | if (!variable) { 17 | if (defaultValue !== undefined) { 18 | return defaultValue; 19 | } 20 | throw new Error(`${variableName} environment variable must be defined`); 21 | } 22 | return variable 23 | } 24 | 25 | static addCorsOptions(apiResource: apigateway.IResource, 26 | origin: string, 27 | allowCredentials: boolean = false, 28 | allowMethods: string = "OPTIONS,GET,PUT,POST,DELETE" 29 | ) { 30 | 31 | apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({ 32 | integrationResponses: [{ 33 | statusCode: "200", 34 | responseParameters: { 35 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", 36 | "method.response.header.Access-Control-Allow-Origin": "'" + origin + "'", 37 | "method.response.header.Access-Control-Allow-Credentials": "'" + allowCredentials.toString() + "'", 38 | "method.response.header.Access-Control-Allow-Methods": "'" + allowMethods + "'", 39 | "method.response.header.Access-Control-Max-Age": "'7200'", 40 | }, 41 | }], 42 | passthroughBehavior: apigateway.PassthroughBehavior.NEVER, 43 | requestTemplates: { 44 | "application/json": "{\"statusCode\": 200}" 45 | }, 46 | }), { 47 | methodResponses: [{ 48 | statusCode: '200', 49 | responseParameters: { 50 | "method.response.header.Access-Control-Allow-Headers": true, 51 | "method.response.header.Access-Control-Allow-Methods": true, 52 | "method.response.header.Access-Control-Allow-Credentials": true, 53 | "method.response.header.Access-Control-Allow-Origin": true, 54 | "method.response.header.Access-Control-Max-Age": true, 55 | }, 56 | }] 57 | }) 58 | } 59 | 60 | static isURL(identityProviderMetadataURLOrFile: string) { 61 | try { 62 | new URL(identityProviderMetadataURLOrFile); 63 | return true; 64 | } catch { 65 | return false; 66 | } 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": false, 17 | "sourceMap": true, 18 | "inlineSources": false, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization": false 21 | }, 22 | "exclude": [ 23 | "cdk.out" 24 | ], 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /cdk/tslint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "tslint:recommended" 3 | rules: 4 | no-console: false 5 | object-literal-sort-keys: false 6 | no-unused-expression: false 7 | ordered-imports: false 8 | quotemark: 9 | options: double 10 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "folder: lambda/api" 4 | cd lambda 5 | cd api 6 | npm run clean && rm -rf node_modules 7 | cd .. 8 | cd pretokengeneration 9 | echo "folder: lambda/pretokengeneration" 10 | npm run clean && rm -rf node_modules 11 | echo "folder: cdk" 12 | cd ../.. 13 | cd cdk 14 | npm run clean && rm -rf node_modules 15 | cd .. 16 | echo "folder: ui-react" 17 | cd ui-react 18 | npm run clean && rm -rf node_modules 19 | cd .. -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | echo "Building backend " 7 | 8 | ./build.sh 9 | 10 | echo "Deploying backend stack..." 11 | 12 | # deploy the cdk stack (ignore the error in case it's due to 'No updates are to be performed') 13 | cd cdk 14 | npm run cdk-deploy --silent || true 15 | cd .. 16 | STACK_STATUS=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region "${STACK_REGION}" --query "Stacks[].StackStatus[]" --output text) 17 | 18 | if [[ "${STACK_STATUS}" != "CREATE_COMPLETE" && "${STACK_STATUS}" != "UPDATE_COMPLETE" ]]; then 19 | echo "Stack is in an unexpected status: ${STACK_STATUS}" 20 | exit 1 21 | fi 22 | 23 | echo "Generating UI configuration..." 24 | 25 | ./build-ui.sh 26 | 27 | BUCKET_NAME=$(node --print "require('./ui-react/src/config/autoGenConfig.js').default.uiBucketName") 28 | APP_URL=$(node --print "require('./ui-react/src/config/autoGenConfig.js').default.appUrl") 29 | COGNITO_INSTRUCTIONS="Create some users (in the pool or your IdP) and assign them the groups 'pet-app-admins' and/or 'pet-app-users'" 30 | 31 | if [[ "${BUCKET_NAME}" != "" ]]; then 32 | 33 | 34 | if [[ "${APP_FRONTEND_DEPLOY_MODE}" == "s3" ]]; then 35 | echo "Publishing frontend to ${BUCKET_NAME}" 36 | 37 | # NOTE: for development / demo purposes only, we use a public-read ACL on the frontend static files 38 | # in a production scenario use CloudFront and keep the s3 objects private 39 | aws s3 sync --delete --acl public-read ./ui-react/build/ "s3://${BUCKET_NAME}" &> /dev/null 40 | echo "${COGNITO_INSTRUCTIONS}" 41 | echo "Then visit the app at: ${APP_URL}" 42 | fi 43 | 44 | if [[ "${APP_FRONTEND_DEPLOY_MODE}" == "cloudfront" ]]; then 45 | echo "Publishing frontend to ${BUCKET_NAME}, will be availabing via CloudFront" 46 | 47 | # since we serve from CloudFront, we can keep the objects private, so we don't pass --acl public-read here 48 | aws s3 sync --delete ./ui-react/build/ "s3://${BUCKET_NAME}" &> /dev/null 49 | 50 | echo "${COGNITO_INSTRUCTIONS}" 51 | echo "Then visit the app at: ${APP_URL} (may take a few minutes for the distribution to finish deployment)" 52 | fi 53 | 54 | 55 | elif [[ "${APP_URL}" == "http://localhost"* ]]; then 56 | 57 | echo "${COGNITO_INSTRUCTIONS}" 58 | echo "Then run: cd ./ui-react && npm start # will launch the app in your browser at ${APP_URL}" 59 | 60 | fi 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | ./build.sh 7 | 8 | cd cdk 9 | echo "calculating diff" 10 | npm run cdk-diff 11 | cd - 12 | -------------------------------------------------------------------------------- /docs/OktaInstructions.md: -------------------------------------------------------------------------------- 1 | # Integrating IdP Sign In with Cognito 2 | 3 | ## Overview 4 | 5 | This walkthrough will help guide you through creating a working Okta Application in order to successfully run the demo. 6 | 7 | ## Okta Directory and Application Setup 8 | 9 | #### If you already have a developer account with Okta, please skip to Step 3 10 | 11 | 1. Sign up for a developer account on [Okta](https://developer.okta.com/) using your corporate credentials. 12 | 2. Activate your account and sign into your Okta domain *stated in the email*. 13 | 14 | **NOTE: THE Okta UI may be slightly different, but the steps involved remain the same** 15 | 16 | 3. Go to the Admin dashboard by clicking on the **Admin** button on the top-right corner of the page. 17 | 4. In the Admin dashboard, go to the top-left of the page where it says **Developer Console** and change it to **Classic UI**. 18 | 19 | ![Console Page](./images/dev-classicUI.png) 20 | 21 | 5. On the right-hand side of the page, under **Shortcuts**, click **Add Applications**. 22 | 23 | ![Add Applications](./images/add-applications.png) 24 | 25 | 6. Select **Create New App** from the left side of the page. 26 | 27 | ![Create New Application](images/add-app2.png) 28 | 29 | 7. For Platform, select **Web** and enable **SAML 2.0** for the Sign in method. Then, press **Create**. 30 | 31 | ![Set SAML 2.0](images/app-integration.png) 32 | 33 | 8. Give the app a name and a logo (*optional*), then select **Next**. 34 | 35 | ![Set App Name and Logo](images/gen-settings.png) 36 | 37 | 9. The next page describes the SAML settings for your app. 38 | 10. The **Single sign on URL** will be your Cognito user pool App Integration domain with */saml2/idpresponse* appended. 39 | * In Cognito, select **Manage User Pools** and then select the user pool for your application 40 | * On the left side, go to **App integration** and copy the domain 41 | * Example: *https://yourDomainPrefix.auth.yourRegion.amazoncognito.com/saml2/idpresponse* 42 | 11. Make sure the **Use this for Recipient URL and Destination URL** box is checked. 43 | 12. For the **Audience URI (SP Entity ID)**, enter the urn for your Cognito user pool, which is of the form *urn:amazon:cognito:sp:``*. 44 | * The user pool ID can be found at the top of the **General Settings** page in your Cognito user pool, and the full urn is printed by the `deploy.sh` script after `Audience URI (SP Entity ID):` 45 | 13. Leave the **Default RelayState** blank. 46 | 14. Select *unspecified* for **Name ID format**. 47 | 15. Select *Okta username* for **Application username**. 48 | 16. Under **Attribute Statements**, configure the following: 49 | 50 | Name | Name format | Value 51 | :---: | :---: | :---: 52 | email | Unspecified | user.email 53 | firstName | Unspecified | user.firstName 54 | lastName | Unspecified | user.lastName 55 | 56 | 17. Under Group Attribute Statements, add the following: 57 | 58 | Name | Name format | Filter | value 59 | :---: | :---: | :---: | :---: 60 | groups | Unspecified | Starts with | pet-app 61 | 62 | 63 | ![SAML Settings](images/saml-settings.png) 64 | 65 | 18. Click **Next**. 66 | 19. Select *I'm an Okta customer adding an internal app* for **Are you a customer or partner?**. 67 | 20. Select *This is an internal app that we have created* for **App Type**. 68 | 69 | ![App Purpose](images/app-config.png) 70 | 71 | 21. Click **Finish**. 72 | 73 | -------------------------------------------------------------------------------- /docs/images/add-app2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/add-app2.png -------------------------------------------------------------------------------- /docs/images/add-applications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/add-applications.png -------------------------------------------------------------------------------- /docs/images/app-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/app-config.png -------------------------------------------------------------------------------- /docs/images/app-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/app-integration.png -------------------------------------------------------------------------------- /docs/images/dev-classicUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/dev-classicUI.png -------------------------------------------------------------------------------- /docs/images/gen-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/gen-settings.png -------------------------------------------------------------------------------- /docs/images/saml-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/docs/images/saml-settings.png -------------------------------------------------------------------------------- /env.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## TODO: copy this file into env.sh with your settings 4 | 5 | export AWS_SDK_LOAD_CONFIG=1 # allows the SDK to load from config. see https://github.com/aws/aws-sdk-js/pull/1391 6 | 7 | ## ==================================================================================================================== 8 | ## 1. the CloudFormation stack name, e.g. "MyAppName" 9 | ## ==================================================================================================================== 10 | 11 | export STACK_NAME=ExternalIdPDemo 12 | 13 | ## ==================================================================================================================== 14 | ## 2. explicitly define the account you intend to deploy into 15 | ## for the simplicity of running the demo, it takes the current profile's account and region 16 | ## in production make sure you explicitly define these via the CI/CD environment variables as a safety mechanism 17 | ## ==================================================================================================================== 18 | 19 | export STACK_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text) 20 | export STACK_REGION=$(aws configure get region) 21 | 22 | ## ==================================================================================================================== 23 | ## 3. change to any unused domain name, default to a combination that is be unique per account+region 24 | ## ==================================================================================================================== 25 | 26 | export COGNITO_DOMAIN_NAME=auth-${STACK_ACCOUNT}-${STACK_REGION} 27 | 28 | ## ==================================================================================================================== 29 | ## 4. set the frontend app URL (used as the CORS origin and Cognito callback URLs) 30 | ## choose one of the following options and comment out the others: 31 | ## ==================================================================================================================== 32 | 33 | ## -------------------------------------------------------------------------------------------------------------------- 34 | ## a. local mode: use this for development (the UI is not deployed and served locally) 35 | ## -------------------------------------------------------------------------------------------------------------------- 36 | 37 | export APP_URL=http://localhost:3000 38 | 39 | ## -------------------------------------------------------------------------------------------------------------------- 40 | ## b. s3 mode: for development / demo purposes only. the UI will be deployed directly to s3 41 | ## the APP_URL will be setup automatically based on the created bucket (will use a random name based on the stack name) 42 | ## -------------------------------------------------------------------------------------------------------------------- 43 | 44 | # export APP_FRONTEND_DEPLOY_MODE=s3 45 | 46 | ## -------------------------------------------------------------------------------------------------------------------- 47 | ## c. cloudfront mode: the UI will be deployed to a private s3 bucket with a cloudfront distribution 48 | ## if not provided, the APP_URL will be determined automatically (the distribution URL) 49 | ## if you will be using a custom domain, explicitly set the APP_URL to the root of the app (e.g. https://example.com) 50 | ## -------------------------------------------------------------------------------------------------------------------- 51 | 52 | # export APP_FRONTEND_DEPLOY_MODE=cloudfront 53 | ## export APP_URL= 54 | 55 | ## ==================================================================================================================== 56 | ## 5. optional - for IdP integration 57 | ## ==================================================================================================================== 58 | 59 | ### SAML 60 | ## the name you want for the Identity Provider 61 | 62 | # export IDENTITY_PROVIDER_NAME=IdP 63 | 64 | ## the IdPs SAML metadata URL, or the actual XML string 65 | 66 | # export IDENTITY_PROVIDER_METADATA= 67 | 68 | ### OIDC 69 | ## the name you want for the Identity Provider 70 | 71 | # export OIDC_PROVIDER_NAME=OIDC 72 | 73 | ## the IdPs OIDC configuration settings 74 | 75 | # export OIDC_CLIENT_ID= 76 | # export OIDC_CLIENT_SECRET= 77 | # export OIDC_ISSUER_URL= 78 | 79 | ## ==================================================================================================================== 80 | ## 6. other optional configuration 81 | ## ==================================================================================================================== 82 | 83 | # export GROUPS_ATTRIBUTE_NAME= 84 | # export ADMINS_GROUP_NAME= 85 | # export USERS_GROUP_NAME= 86 | # export LAMBDA_MEMORY= 87 | 88 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | echo "this will run npm install in all relevant sub-folders, build the project, and install the CDK toolkit" 7 | 8 | cd lambda 9 | cd api 10 | npm install 11 | cd .. 12 | cd pretokengeneration 13 | npm install 14 | cd ../.. 15 | cd cdk 16 | npm install 17 | cd .. 18 | cd ui-react 19 | npm install 20 | cd .. 21 | 22 | ./build.sh 23 | 24 | cd cdk 25 | npm run cdk -- bootstrap 26 | cd .. 27 | -------------------------------------------------------------------------------- /lambda/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cognito-example-for-external-idp-lambda-api", 3 | "version": "0.1.2", 4 | "author": "Eran Medan", 5 | "license": "MIT-0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/aws-samples/amazon-cognito-example-for-external-idp.git" 9 | }, 10 | "description": "The backend API for the demo app", 11 | "main": "index.js", 12 | "scripts": { 13 | "tsc": "tsc", 14 | "start": "ts-node src/local.ts", 15 | "build": "tsc -b && copy-node-modules . dist/src", 16 | "clean": "tsc -b --clean && rm -rf dist", 17 | "package": "cd dist/src && zip -r ../function.zip .", 18 | "watch": "tsc -w", 19 | "test": "mocha -r ts-node/register tests/**/*.test.ts", 20 | "testWithCoverage": "nyc -r lcov -e .ts -x \"*.test.ts\" mocha -r ts-node/register tests/**/*.test.ts && nyc report" 21 | }, 22 | "keywords": [], 23 | "devDependencies": { 24 | "@aws-sdk/client-cognito-identity-provider": "^3.682.0", 25 | "@aws-sdk/client-dynamodb": "^3.682.0", 26 | "@types/aws-lambda": "^8.10.51", 27 | "@types/aws-serverless-express": "^3.3.3", 28 | "@types/chai": "^4.2.11", 29 | "@types/cors": "^2.8.6", 30 | "@types/express-serve-static-core": "^5.0.1", 31 | "@types/mocha": "^7.0.2", 32 | "@types/node": "^22.8.7", 33 | "@types/uuid": "^7.0.3", 34 | "chai": "^4.2.0", 35 | "copy-node-modules": "^1.1.1", 36 | "dynamodb-local": "^0.0.31", 37 | "mocha": "^10.8.2", 38 | "nyc": "^15.0.1", 39 | "ts-node": "^10.9.2", 40 | "tslint": "^6.1.2", 41 | "typescript": "^5.6.3" 42 | }, 43 | "dependencies": { 44 | "@aws-sdk/lib-dynamodb": "^3.685.0", 45 | "aws-serverless-express": "^3.3.8", 46 | "cors": "^2.8.5", 47 | "express": "^4.21.2", 48 | "uuid": "^8.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lambda/api/src/app.ts: -------------------------------------------------------------------------------- 1 | import express = require("express"); 2 | import { CognitoIdentityProviderClient, AdminUserGlobalSignOutCommand } from "@aws-sdk/client-cognito-identity-provider"; 3 | import {Express, json, Request, Response, urlencoded, RequestHandler} from "express"; 4 | import cors from "cors"; 5 | import {eventContext} from "aws-serverless-express/middleware"; 6 | 7 | import {v4 as uuid4} from 'uuid'; 8 | import {authorizationMiddleware, ForceSignOutHandler} from "./services/authorizationMiddleware"; 9 | import {StorageService} from "./services/storageService"; 10 | import {Pet} from "./models/pet"; 11 | 12 | export interface AppOptions { 13 | adminsGroupName: string; 14 | usersGroupName: string; 15 | authorizationHeaderName?: string; 16 | allowedOrigin: string; 17 | userPoolId: string; 18 | storageService: StorageService; 19 | cognito: CognitoIdentityProviderClient; 20 | expressApp?: Express; // intended for unit testing / mock purposes 21 | forceSignOutHandler?: ForceSignOutHandler; 22 | } 23 | 24 | /** 25 | * Using a separate class to allow easier unit testing 26 | * All dependencies are provided on the constructor to allow easier mocking 27 | * This is not intended to be an exemplary idiomatic express.js app 28 | * A lot of shortcuts have been made for brevity 29 | */ 30 | export class App { 31 | 32 | constructor(private opts: AppOptions, public expressApp: Express = express()) { 33 | 34 | const app = expressApp; 35 | 36 | app.use(cors({ 37 | credentials: false, 38 | origin: [(opts.allowedOrigin)], 39 | })); 40 | 41 | app.use(json() as RequestHandler); 42 | app.use(urlencoded({extended: true}) as RequestHandler); 43 | 44 | app.use(eventContext()); 45 | 46 | app.use(authorizationMiddleware({ 47 | authorizationHeaderName: opts.authorizationHeaderName, 48 | supportedGroups: [ 49 | opts.adminsGroupName, 50 | opts.usersGroupName, 51 | ], 52 | forceSignOutHandler: opts.forceSignOutHandler, 53 | allowedPaths: ["/"], 54 | })); 55 | 56 | /** 57 | * Ping 58 | */ 59 | app.get("/", async (req: Request, res: Response) => { 60 | res.json({status: "ok"}); 61 | }); 62 | 63 | /** 64 | * List all pets 65 | */ 66 | app.get("/pets", async (req: Request, res: Response) => { 67 | 68 | if (req.groups.has(opts.adminsGroupName)) { 69 | // if the user has the admin group, we return all pets 70 | res.json(await opts.storageService.getAllPets()); 71 | } else { 72 | // otherwise, just owned pets (middleware ensure that the user is in either of the 2 groups) 73 | res.json(await opts.storageService.getAllPetsByOwner(req.username)); 74 | } 75 | }); 76 | 77 | /** 78 | * Get a pet 79 | */ 80 | app.get("/pets/:petId", async (req: Request, res: Response) => { 81 | const petId = req.params.petId; 82 | 83 | const pet = await opts.storageService.getPet(petId); 84 | 85 | if (!pet) { 86 | res.status(404).json({error: `Pet with id ${petId} was not found`}); 87 | return; 88 | } 89 | 90 | if (req.groups.has(opts.adminsGroupName) || pet.owner === req.username) { 91 | // if the pet is owned by the user or they are an admin, return it. 92 | res.json(pet); 93 | } else { 94 | res.status(403).json({error: `Unauthorized`}); 95 | } 96 | }); 97 | 98 | /** 99 | * Create a pet 100 | */ 101 | app.post("/pets", async (req: Request, res: Response) => { 102 | 103 | const pet: Pet = req.body; 104 | 105 | // TODO: make sure body is parsed as JSON, post and put stopped working 106 | console.log("post /pets ", typeof pet, pet); 107 | 108 | if (pet.id) { 109 | res.status(400).json({error: "POST /pet auto assigns an id. In order to update use PUT /pet"}); 110 | return; 111 | } 112 | 113 | // auto generate an ID 114 | pet.id = uuid4(); 115 | // set the owner to the current user 116 | pet.owner = req.username; 117 | 118 | pet.ownerDisplayName = req.claims.email; 119 | 120 | await opts.storageService.savePet(pet); 121 | res.json(pet); 122 | }); 123 | 124 | /** 125 | * Update a pet 126 | */ 127 | app.put("/pets/:petId", async (req: Request, res: Response) => { 128 | 129 | const updatedPet: Pet = req.body; 130 | const petId = req.params.petId; 131 | 132 | if (!petId) { 133 | res.status(400).json({error: "Invalid request - missing Pet ID"}); 134 | return; 135 | } 136 | if (!updatedPet) { 137 | res.status(400).json({error: "Invalid request - missing Pet"}); 138 | return; 139 | } 140 | if (updatedPet.id !== petId) { 141 | res.status(400).json({error: "Invalid request - Pet.id doesn't match request param"}); 142 | return; 143 | } 144 | const existingPet = await opts.storageService.getPet(petId); 145 | 146 | if (!existingPet) { 147 | res.status(404).json({error: `Pet with id ${petId} was not found`}); 148 | return; 149 | } 150 | 151 | if (req.groups.has(opts.adminsGroupName) 152 | || updatedPet.owner === existingPet.owner && existingPet.owner === req.username) { 153 | // if the user is an admin, or the pet is owned by the owner and didn't change the owner, allow 154 | // only admin can change the owner 155 | await opts.storageService.savePet(updatedPet); 156 | res.json(updatedPet); 157 | 158 | } else { 159 | res.status(403).json({error: "Unauthorized"}); 160 | } 161 | }); 162 | 163 | /** 164 | * Delete a pet 165 | */ 166 | app.delete("/pets/:petId", async (req: Request, res: Response) => { 167 | 168 | const petId = req.params.petId; 169 | const pet = await opts.storageService.getPet(petId); 170 | 171 | if (!pet) { 172 | res.status(404).json({error: `Pet with id ${petId} was not found`}); 173 | return; 174 | } 175 | 176 | if (req.groups.has(opts.adminsGroupName) || pet.owner === req.username) { 177 | // if the pet is owned by the user or they are an admin, allow deleting it 178 | await opts.storageService.deletePet(petId); 179 | res.json(pet); 180 | } else { 181 | res.status(403).json({error: `Unauthorized`}); 182 | } 183 | }); 184 | 185 | app.post("/forceSignOut", async (req: Request, res: Response) => { 186 | // all tokens issued before this call will no longer be allowed to be used 187 | const params = { 188 | UserPoolId: opts.userPoolId, 189 | Username: req.username, 190 | } 191 | const command = new AdminUserGlobalSignOutCommand(params); 192 | await opts.cognito.send(command); 193 | if (opts.forceSignOutHandler) { 194 | await opts.forceSignOutHandler.forceSignOut(req); 195 | } 196 | res.status(200).send(); 197 | }); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lambda/api/src/expressApp.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./app"; 2 | import { DynamoDBStorageService } from "./services/dynamoDBStorageService"; 3 | import { DynamoDBForcedSignoutHandler } from "./services/dynamoDBForcedSignoutHandler"; 4 | import { CognitoIdentityProviderClient } from "@aws-sdk/client-cognito-identity-provider"; 5 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 6 | import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; 7 | 8 | if (!process.env.ITEMS_TABLE_NAME) { 9 | throw new Error("Required environment variable ITEMS_TABLE_NAME is missing"); 10 | } 11 | 12 | if (!process.env.USER_POOL_ID) { 13 | throw new Error("Required environment variable USER_POOL_ID is missing"); 14 | } 15 | 16 | if (!process.env.ALLOWED_ORIGIN) { 17 | throw new Error("Required environment variable ALLOWED_ORIGIN is missing"); 18 | } 19 | 20 | const ddbDocClient = DynamoDBDocumentClient.from(new DynamoDBClient()); 21 | 22 | export const expressApp = new App({ 23 | cognito: new CognitoIdentityProviderClient(), 24 | adminsGroupName: process.env.ADMINS_GROUP_NAME || "pet-app-admins", 25 | usersGroupName: process.env.USERS_GROUP_NAME || "pet-app-users", 26 | authorizationHeaderName: 27 | process.env.AUTHORIZATION_HEADER_NAME || "Authorization", 28 | userPoolId: process.env.USER_POOL_ID, 29 | forceSignOutHandler: process.env.USERS_TABLE_NAME 30 | ? new DynamoDBForcedSignoutHandler( 31 | process.env.USERS_TABLE_NAME, 32 | ddbDocClient 33 | ) 34 | : undefined, 35 | storageService: new DynamoDBStorageService(process.env.ITEMS_TABLE_NAME, ddbDocClient), 36 | allowedOrigin: process.env.ALLOWED_ORIGIN, 37 | }).expressApp; 38 | -------------------------------------------------------------------------------- /lambda/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, proxy } from "aws-serverless-express"; 2 | import { Context } from "aws-lambda"; 3 | import { expressApp } from "./expressApp"; 4 | 5 | const server = createServer(expressApp); 6 | 7 | // noinspection JSUnusedGlobalSymbols 8 | export const handler = (event: any, context: Context) => proxy(server, event, context); 9 | -------------------------------------------------------------------------------- /lambda/api/src/local.ts: -------------------------------------------------------------------------------- 1 | import {expressApp as app} from "./expressApp"; 2 | const port = 3001; 3 | app.listen(port); 4 | console.log(`Listening on port ${port}`); 5 | -------------------------------------------------------------------------------- /lambda/api/src/models/pet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Pet 3 | */ 4 | export class Pet { 5 | 6 | constructor( 7 | public id?: string | null, 8 | public type?: string | null, 9 | public price?: number | null, 10 | public owner?: string | null, 11 | public ownerDisplayName?: string | null) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lambda/api/src/services/authorizationMiddleware.ts: -------------------------------------------------------------------------------- 1 | import {NextFunction, Request, RequestHandler, Response} from "express"; 2 | 3 | /** 4 | * Common claims for both id and access tokens 5 | */ 6 | export interface ClaimsBase { 7 | [name: string]: any; 8 | 9 | aud: string; 10 | iss: string; 11 | "cognito:groups"?: string[]; 12 | exp: number; 13 | iat: number; 14 | sub: string; 15 | token_use: "id" | "access"; 16 | } 17 | 18 | /** 19 | * Some id token specific claims 20 | */ 21 | export interface IdTokenClaims extends ClaimsBase { 22 | 23 | "cognito:username": string; 24 | email?: string; 25 | email_verified?: string; 26 | auth_time: string; 27 | token_use: "id"; 28 | } 29 | 30 | /** 31 | * Some access token specific claims 32 | */ 33 | export interface AccessTokenClaims extends ClaimsBase { 34 | 35 | username?: string; 36 | token_use: "access"; 37 | } 38 | 39 | /** 40 | * combined type for Claims 41 | */ 42 | export type Claims = IdTokenClaims | AccessTokenClaims; 43 | 44 | // enrich the Express request type for type completion 45 | declare global { 46 | namespace Express { 47 | interface Request { 48 | claims: Claims; 49 | groups: Set; 50 | username: string; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Returns the groups claim from either the id or access tokens as a Set 57 | * @param claims 58 | */ 59 | const getGroups = (claims: ClaimsBase): Set => { 60 | const groups = claims["cognito:groups"]; 61 | if (groups) { 62 | return new Set(groups); 63 | } 64 | return new Set(); 65 | }; 66 | 67 | /** 68 | * Parses the token and returns the claims 69 | * @param token a base64 encoded JWT token (id or access) 70 | * @return parsed Claims or null if no token was provided 71 | */ 72 | const getClaimsFromToken = (token?: string): Claims | null => { 73 | if (!token) { 74 | return null; 75 | } 76 | try { 77 | const base64decoded = Buffer.from(token.split(".")[1], "base64").toString("ascii"); 78 | return JSON.parse(base64decoded); 79 | } catch (e) { 80 | console.error("Invalid JWT token", e); 81 | // in case a malformed token was provided, we treat it as if non was provided, users will get a 401 in our case 82 | return null; 83 | } 84 | }; 85 | 86 | /** 87 | * Handles force sign out event 88 | * e.g. a user who would like to force sign-out from all other devices 89 | * (not just invalidate the current user's tokens, but any token issued before this point in time 90 | * to that user in any device) 91 | */ 92 | export interface ForceSignOutHandler { 93 | 94 | isForcedSignOut(req: Request): Promise; 95 | 96 | forceSignOut(req: Request): Promise; 97 | } 98 | 99 | /** 100 | * Params for amazonCognitoAuthorizerMiddleware 101 | */ 102 | export interface Opts { 103 | /** 104 | * the header passing the token, e.g. Authorization 105 | */ 106 | authorizationHeaderName?: string; 107 | 108 | /** 109 | * optional, any group that allows the user to do something useful in the app 110 | * if the user has none of them, we just return 403 Forbidden as they can't do anything 111 | * if not provided, will not do this pre-check 112 | */ 113 | supportedGroups?: string[]; 114 | 115 | /** 116 | * optional, if provided, will be called to check if the current user has logged out globally 117 | */ 118 | forceSignOutHandler?: ForceSignOutHandler; 119 | 120 | /** 121 | * optional, if provided, will allow all requests to the provided paths 122 | */ 123 | allowedPaths?: string[]; 124 | } 125 | 126 | /** 127 | * Creates a middleware that enriches the request object with: 128 | * - claims: Claims (JWT ID token claims) 129 | * - groups: Set (Cognito User Pool Groups, from the cognito:groups claim) 130 | * 131 | * It will return a 403 if non of the supportedGroups exists in the claim 132 | * 133 | * @param opts 134 | * 135 | */ 136 | export const authorizationMiddleware = (opts?: Opts): RequestHandler => 137 | async (req: Request, res: Response, next: NextFunction): Promise => { 138 | 139 | if (opts && opts.allowedPaths && opts.allowedPaths.includes(req.path)) { 140 | next(); 141 | return; 142 | } 143 | 144 | const authorizationHeaderName = opts && opts.authorizationHeaderName || "Authorization"; 145 | const token = req.header(authorizationHeaderName); 146 | 147 | const claims = getClaimsFromToken(token); 148 | 149 | if (claims) { 150 | 151 | req.claims = claims; 152 | if (claims["cognito:username"]) { 153 | // username claim name in the id token 154 | req.username = claims["cognito:username"]; 155 | } else if (claims.username) { 156 | // username claim name in the access token 157 | req.username = claims.username; 158 | } else { 159 | console.warn(`No username claim found in token, using sub as username`); 160 | req.username = claims.sub; 161 | } 162 | 163 | // always returns a Set, if no groups, it will be empty. 164 | const groups = getGroups(claims); 165 | req.groups = groups; 166 | 167 | // check if the user has at least 1 required group 168 | // e.g. if the claim has [g1] 169 | // and basicGroups includes [g1, g2] 170 | // it means the user has at least one of the groups that allows them to do something 171 | 172 | if (opts && opts.supportedGroups) { 173 | const userHasAtLeastOneSupportedGroup = opts.supportedGroups.some((g) => groups.has(g)); 174 | if (!userHasAtLeastOneSupportedGroup) { 175 | 176 | res.status(403).json({error: "Unauthorized"}); 177 | return; 178 | } 179 | } 180 | 181 | // check if user did a global sign out (optional) 182 | if (opts && opts.forceSignOutHandler) { 183 | const isTokenRevoked = await opts.forceSignOutHandler.isForcedSignOut(req); 184 | if (isTokenRevoked) { 185 | res.status(401).json({error: "Your session has expired, please sign in again"}); 186 | return; 187 | } 188 | } 189 | 190 | // if we got here, continue with the request 191 | 192 | next(); 193 | 194 | } else { 195 | // the only way to get here (e.g. no claims on the request) is if it's on a path with no required auth 196 | // or if the token header name is incorrect 197 | // API Gateway would deny access otherwise 198 | // but for defense in depth, we return an explicit deny here (e.g. in case of running locally) 199 | 200 | res.status(401).json({error: "Please sign in"}); 201 | } 202 | 203 | }; 204 | -------------------------------------------------------------------------------- /lambda/api/src/services/dynamoDBForcedSignoutHandler.ts: -------------------------------------------------------------------------------- 1 | import {ForceSignOutHandler} from "./authorizationMiddleware"; 2 | import {Request} from "express"; 3 | import { DynamoDBDocumentClient, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb"; 4 | 5 | export class DynamoDBForcedSignoutHandler implements ForceSignOutHandler { 6 | 7 | constructor(private readonly tableName: string, 8 | private readonly docClient: DynamoDBDocumentClient, 9 | private readonly keyAttributeName: string = "username", 10 | private readonly lastForceSignOutTimeAttributeName: string = "lastForceSignOutTime", 11 | private readonly ttlAttributeName: string = "ttl" , 12 | private readonly ttlInSeconds: number = 2_592_000 /*30 days*/) { 13 | 14 | } 15 | 16 | public async isForcedSignOut(req: Request): Promise { 17 | 18 | const key = this.getKey(req.username); 19 | 20 | try { 21 | 22 | const params = { 23 | TableName: this.tableName, 24 | Key: key, 25 | }; 26 | 27 | const command = new GetCommand(params) 28 | const result = await this.docClient.send(command); 29 | 30 | if (result.Item && typeof result.Item[this.lastForceSignOutTimeAttributeName] === "number") { 31 | 32 | const issuedAtInMillis = req.claims.iat * 1000; // issued at is in seconds since epoch 33 | // if the token was issued before the last time this user issued a forced sign out, deny 34 | // (any newer sign-in will generate newer tokens hence will pass this check, but older ones will require re-auth 35 | if (issuedAtInMillis < result.Item[this.lastForceSignOutTimeAttributeName]) { 36 | // optionally log the event 37 | // console.warn("Login attempt with a token issued before a forced sign out:" + req.username, req.rawHeaders); 38 | return true; 39 | } 40 | } 41 | return false; 42 | } catch (ex) { 43 | console.error("Error checking forced sign out", ex); 44 | throw ex; 45 | } 46 | } 47 | 48 | public async forceSignOut(req: Request) { 49 | 50 | const nowInMillis = Date.now(); 51 | 52 | const item = this.getKey(req.username); 53 | 54 | item[this.ttlAttributeName] = Math.round(nowInMillis / 1000) + this.ttlInSeconds; 55 | item[this.lastForceSignOutTimeAttributeName] = nowInMillis; 56 | 57 | try { 58 | 59 | const command = new PutCommand({ 60 | TableName: this.tableName, 61 | Item: item, 62 | }); 63 | await this.docClient.send(command); 64 | 65 | } catch (ex) { 66 | console.error("Error revoking token: ", ex); 67 | } 68 | } 69 | 70 | private getKey(username: string) { 71 | const key = {} as any; 72 | key[this.keyAttributeName] = username; 73 | return key; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lambda/api/src/services/dynamoDBStorageService.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DynamoDBDocumentClient, PutCommand, GetCommand, ScanCommand, DeleteCommand, ScanCommandInput } from "@aws-sdk/lib-dynamodb"; 3 | import {StorageService} from "./storageService"; 4 | import {Pet} from "../models/pet"; 5 | 6 | export class DynamoDBStorageService implements StorageService { 7 | 8 | constructor(private readonly tableName: string, 9 | private readonly docClient: DynamoDBDocumentClient) { 10 | } 11 | 12 | public async getPet(id: string): Promise { 13 | 14 | try { 15 | const data = await this.docClient.send(new GetCommand({ 16 | TableName: this.tableName, 17 | Key: {id}, 18 | ConsistentRead: true, 19 | })); 20 | if (data && data.Item) { 21 | return data.Item as Pet; 22 | } 23 | return null; // return null vs undefined 24 | } catch (ex) { // AWSError 25 | console.warn("Error getting entry", ex); 26 | throw ex; 27 | } 28 | } 29 | 30 | public async savePet(pet: Pet): Promise { 31 | try { 32 | await this.docClient.send(new PutCommand({ 33 | TableName: this.tableName, 34 | Item: pet, 35 | })); 36 | } catch (ex) { 37 | console.warn("Error saving entry", ex); 38 | throw ex; 39 | } 40 | } 41 | 42 | public async getAllPets(): Promise { 43 | try { 44 | 45 | const result: Pet[] = []; 46 | 47 | const params: ScanCommandInput = {TableName: this.tableName}; 48 | 49 | while (true) { 50 | 51 | const data = await this.docClient.send(new ScanCommand(params)); 52 | result.push(...data.Items as Pet[]); 53 | 54 | if (!data.LastEvaluatedKey) { 55 | break; 56 | } 57 | 58 | params.ExclusiveStartKey = data.LastEvaluatedKey; 59 | 60 | } 61 | 62 | return result; 63 | } catch (ex) { // AWSError 64 | console.warn("Error getting all entries", ex); 65 | throw ex; 66 | } 67 | 68 | } 69 | 70 | public async deletePet(id: string): Promise { 71 | try { 72 | await this.docClient.send(new DeleteCommand({TableName: this.tableName, Key: {id}})); 73 | } catch (ex) { 74 | console.warn("Error deleting entry", ex); 75 | throw ex; 76 | } 77 | } 78 | 79 | public async getAllPetsByOwner(owner: string): Promise { 80 | // in a real world scenario this will be probably using a query on a global secondary index (owner) 81 | // for simplicity of the demo, this will just filter the scanned results 82 | 83 | return (await this.getAllPets()).filter((pet) => pet.owner === owner); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /lambda/api/src/services/storageService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Persistent Storage Service 3 | */ 4 | import {Pet} from "../models/pet"; 5 | 6 | export interface StorageService { 7 | 8 | /** 9 | * Returns a pet. Throws an error if not found. 10 | * @param key 11 | */ 12 | getPet(key: string): Promise; 13 | 14 | /** 15 | * Saves (creates or overwrites) a pet. id is required. 16 | * 17 | * @param pet 18 | */ 19 | savePet(pet: Pet): Promise; 20 | 21 | /** 22 | * Returns all pets in the database 23 | */ 24 | getAllPets(): Promise; 25 | 26 | /** 27 | * Returns all pets in the database 28 | */ 29 | getAllPetsByOwner(owner: string): Promise; 30 | 31 | /** 32 | * Deletes a pet if that pet exists 33 | * @param key 34 | */ 35 | deletePet(key: string): Promise; 36 | } 37 | -------------------------------------------------------------------------------- /lambda/api/tests/app.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:trailing-comma only-arrow-functions object-literal-shorthand */ 2 | 3 | import { Context } from "aws-lambda"; 4 | import { 5 | DynamoDBClient, 6 | GetItemCommand, 7 | PutItemCommand, 8 | ScanCommand, 9 | } from "@aws-sdk/client-dynamodb"; 10 | import { 11 | CognitoIdentityProviderClient, 12 | AdminUserGlobalSignOutCommand, 13 | } from "@aws-sdk/client-cognito-identity-provider"; 14 | import { 15 | createServer, 16 | proxy, 17 | ProxyResult, 18 | Response, 19 | } from "aws-serverless-express"; 20 | import { App } from "../src/app"; 21 | import { DynamoDBForcedSignoutHandler } from "../src/services/dynamoDBForcedSignoutHandler"; 22 | import { expect } from "chai"; 23 | import { Claims } from "../src/services/authorizationMiddleware"; 24 | import { Server } from "http"; 25 | import { Pet } from "../src/models/pet"; 26 | import { DynamoDBStorageService } from "../src/services/dynamoDBStorageService"; 27 | 28 | /** 29 | * An example integration test that can check authorization logic based on mock claims 30 | */ 31 | describe("integration test", async () => { 32 | const user1 = "user1"; 33 | const user2 = "user2"; 34 | const user1DisplayName = "User One"; 35 | const user2DisplayName = "User Two"; 36 | const itemsTableName = "Pets"; 37 | const itemsTable = new Map(); 38 | 39 | const usersTableName = "Users"; 40 | const usersTable = new Map< 41 | string, 42 | { username: string; ttl: number; lastForceSignOutTime: number } 43 | >(); 44 | const origin = "http://localhost:3000"; 45 | 46 | const adminsGroupName = "pet-app-admins"; 47 | const usersGroupName = "pet-app-users"; 48 | 49 | let server: Server; 50 | let handler: (event: any, context: Context) => ProxyResult; 51 | 52 | before(() => { 53 | server = createServer(mockApp.expressApp); 54 | handler = (event: any, context: Context) => 55 | proxy(server, event, context, "PROMISE"); 56 | itemsTable.set("p1", new Pet("p1", "cat", 10, user1, user1DisplayName)); 57 | itemsTable.set("p2", new Pet("p2", "dog", 10, user2, user2DisplayName)); 58 | }); 59 | 60 | after(() => { 61 | server.close(); 62 | }); 63 | 64 | it("test get allowed paths with no auth", async () => { 65 | const response: Response = await request("/", "GET"); 66 | 67 | expect(response.statusCode).to.equal(200); 68 | }); 69 | 70 | it("test get all pets as admin", async () => { 71 | const response: Response = await request("/pets", "GET", { 72 | username: user1, 73 | token_use: "access", 74 | "cognito:groups": [usersGroupName, adminsGroupName], 75 | }); 76 | 77 | const pets = JSON.parse(response.body) as any[]; 78 | 79 | expect(pets).to.have.lengthOf(itemsTable.size); 80 | }); 81 | 82 | it("test get only owned pets as user", async () => { 83 | const response: Response = await request("/pets", "GET", { 84 | username: user1, 85 | token_use: "access", 86 | name: "User", 87 | family_name: "One", 88 | "cognito:groups": [usersGroupName], 89 | }); 90 | 91 | const pets = JSON.parse(response.body) as any[]; 92 | 93 | expect(pets).to.have.lengthOf(1); 94 | expect(pets[0]).to.eql(itemsTable.get("p1")); 95 | }); 96 | 97 | it("test update other's pet not as owner - admin", async () => { 98 | const response: Response = await request( 99 | "/pets/p2", 100 | "PUT", 101 | { 102 | username: user1, 103 | token_use: "access", 104 | "cognito:groups": [usersGroupName, adminsGroupName], 105 | }, 106 | itemsTable.get("p2") 107 | ); 108 | 109 | expect(response.statusCode).to.equal(200); 110 | }); 111 | 112 | it("test update other's pet not as owner - regular user", async () => { 113 | const response: Response = await request( 114 | "/pets/p2", 115 | "PUT", 116 | { 117 | username: user1, 118 | token_use: "access", 119 | "cognito:groups": [usersGroupName], 120 | }, 121 | itemsTable.get("p2") 122 | ); 123 | 124 | expect(response.statusCode).to.equal(403); 125 | }); 126 | 127 | it("test no relevant groups", async () => { 128 | const response: Response = await request("/pets", "GET", { 129 | username: user1, 130 | token_use: "access", 131 | "cognito:groups": ["other"], 132 | }); 133 | expect(response.statusCode).to.equal(403); 134 | }); 135 | 136 | it("test no groups", async () => { 137 | const response: Response = await request("/pets", "GET", { 138 | username: user1, 139 | token_use: "access", 140 | "cognito:groups": [], 141 | }); 142 | expect(response.statusCode).to.equal(403); 143 | }); 144 | 145 | it("test null groups", async () => { 146 | const response: Response = await request("/pets", "GET", { 147 | username: user1, 148 | token_use: "access", 149 | }); 150 | expect(response.statusCode).to.equal(403); 151 | }); 152 | 153 | it("test force sign out", async () => { 154 | const iat = Date.now() / 1000 - 1; 155 | const claims: Partial = { 156 | username: user1, 157 | token_use: "access", 158 | "cognito:groups": [usersGroupName], 159 | iat: iat, // token was issued a minute ago 160 | }; 161 | 162 | // first request, should succeed 163 | const response0 = await request("/pets", "GET", claims); 164 | expect(response0.statusCode).to.equal(200); 165 | 166 | // second, forceSignOut, should succeed 167 | const response1 = await request("/forceSignOut", "POST", claims); 168 | expect(response1.statusCode).to.equal(200); 169 | 170 | // should fail because we are after forceSignOut and our token is "old" 171 | const response2 = await request("/pets", "GET", claims); 172 | expect(response2.statusCode).to.equal(401); 173 | 174 | // should succeed because this is a different user 175 | const response3 = await request("/pets", "GET", { 176 | ...claims, 177 | username: user2, 178 | }); 179 | expect(response3.statusCode).to.equal(200); 180 | 181 | // FF to the future, user logged in again, got a new token, should succeed 182 | const response4 = await request("/pets", "GET", { 183 | ...claims, 184 | iat: Date.now() / 1000 + 1, 185 | }); 186 | expect(response4.statusCode).to.equal(200); 187 | }); 188 | 189 | const request = async ( 190 | path: string, 191 | method: string, 192 | claims: Partial = {}, 193 | body?: object 194 | ) => { 195 | const tokenBase64 = Buffer.from(JSON.stringify(claims)).toString("base64"); 196 | 197 | const eventAndContext = { 198 | event: { 199 | resource: "/{proxy+}", 200 | path: path, 201 | httpMethod: method, 202 | headers: { 203 | Accept: "*/*", 204 | "Content-Type": "application/json", 205 | Authorization: `header.${tokenBase64}.signature`, 206 | Host: "xyz.execute-api.xx-xxxx-x.amazonaws.com", 207 | origin: origin, 208 | Referer: origin + "/", 209 | "User-Agent": "UserAgent", 210 | }, 211 | multiValueHeaders: { 212 | Accept: ["*/*"], 213 | "Content-Type": ["application/json"], 214 | Authorization: [`header.${tokenBase64}.signature`], 215 | Host: ["xyz.execute-api.xx-xxxx-x.amazonaws.com"], 216 | origin: [origin], 217 | Referer: [origin + "/"], 218 | "User-Agent": ["UserAgent"], 219 | }, 220 | queryStringParameters: null, 221 | multiValueQueryStringParameters: null, 222 | pathParameters: { proxy: "pets" }, 223 | stageVariables: null, 224 | requestContext: { 225 | resourcePath: "/{proxy+}", 226 | httpMethod: method, 227 | path: "/prod" + path, 228 | identity: {}, 229 | domainName: "xyz.execute-api.xx-xxxx-x.amazonaws.com", 230 | apiId: "xyz", 231 | }, 232 | isBase64Encoded: false, 233 | }, 234 | context: { 235 | callbackWaitsForEmptyEventLoop: true, 236 | getRemainingTimeInMillis(): number { 237 | return 1000; 238 | }, 239 | done(error?: Error, result?: any): void { 240 | console.log("done", error, result); 241 | }, 242 | fail(error: Error | string): void { 243 | console.log("fail", error); 244 | }, 245 | succeed(messageOrObject: any): void { 246 | console.log("succeed", messageOrObject); 247 | }, 248 | }, 249 | }; 250 | 251 | if (body) { 252 | (eventAndContext.event as any).body = JSON.stringify(body); 253 | } 254 | 255 | return handler(eventAndContext.event, eventAndContext.context as any) 256 | .promise; 257 | }; 258 | 259 | const mockApp = new App({ 260 | cognito: { 261 | send: async (command: AdminUserGlobalSignOutCommand) => { 262 | return Promise.resolve({}); 263 | }, 264 | } as unknown as CognitoIdentityProviderClient, 265 | adminsGroupName: adminsGroupName, 266 | usersGroupName: usersGroupName, 267 | authorizationHeaderName: "Authorization", 268 | userPoolId: "pool1", 269 | forceSignOutHandler: new DynamoDBForcedSignoutHandler(usersTableName, { 270 | send: async (command: GetItemCommand | PutItemCommand) => { 271 | if (command instanceof GetItemCommand) { 272 | const params = command.input; 273 | // Add null check for username 274 | const username = params.Key?.username?.S; 275 | if (!username) { 276 | throw new Error("Username is required"); 277 | } 278 | const item = usersTable.get(username); 279 | return Promise.resolve(item ? { Item: item } : {}); 280 | } else if (command instanceof PutItemCommand) { 281 | const params = command.input; 282 | const username = params.Item?.username?.S; 283 | if (!username) { 284 | throw new Error("Username is required"); 285 | } 286 | usersTable.set(username, params.Item as any); 287 | return Promise.resolve({}); 288 | } 289 | }, 290 | } as unknown as DynamoDBClient), 291 | storageService: new DynamoDBStorageService(itemsTableName, { 292 | send: async (command: GetItemCommand | PutItemCommand | ScanCommand) => { 293 | if (command instanceof GetItemCommand) { 294 | const params = command.input; 295 | const id = params.Key?.id?.S; 296 | if (!id) { 297 | throw new Error("Id is required"); 298 | } 299 | const item = itemsTable.get(id); 300 | return Promise.resolve(item ? { Item: item } : {}); 301 | } else if (command instanceof PutItemCommand) { 302 | const params = command.input; 303 | const id = params.Item?.id?.S; 304 | if (!id) { 305 | throw new Error("Id is required"); 306 | } 307 | itemsTable.set(id, params.Item as any); 308 | return Promise.resolve({}); 309 | } else if (command instanceof ScanCommand) { 310 | return Promise.resolve({ 311 | Items: [...itemsTable.values()], 312 | }); 313 | } 314 | }, 315 | } as unknown as DynamoDBClient), 316 | allowedOrigin: origin, 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /lambda/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["es2020"], 5 | "module": "commonjs", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "baseUrl": "./", 9 | "typeRoots": [ 10 | "./types","node_modules/@types" 11 | ], 12 | "types": [ 13 | "node", 14 | "mocha", 15 | "express" 16 | ], 17 | "esModuleInterop": true, 18 | "inlineSourceMap": false, 19 | "sourceMap": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lambda/api/tslint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "tslint:recommended" 3 | rules: 4 | no-console: false 5 | object-literal-sort-keys: false 6 | interface-name: 7 | options: never-prefix 8 | space-before-function-paren: false 9 | ordered-imports: false 10 | no-empty: false 11 | object-literal-key-quotes: false 12 | object-literal-shorthand: false 13 | # 14 | #rules: 15 | # max-line-length: 16 | # options: [120] 17 | # new-parens: true 18 | # no-arg: true 19 | # no-bitwise: true 20 | # no-conditional-assignment: true 21 | # no-consecutive-blank-lines: false 22 | # no-console: 23 | # severity: warning 24 | # options: 25 | # - debug 26 | # - info 27 | # - log 28 | # - time 29 | # - timeEnd 30 | # - trace 31 | #jsRules: 32 | # max-line-length: 33 | # options: [120] 34 | -------------------------------------------------------------------------------- /lambda/api/update-lambda-code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ../../env.sh 5 | 6 | FUNC_NAME=`aws cloudformation describe-stacks --stack-name ${STACK_NAME} | \ 7 | jq -r '.Stacks[].Outputs[] | select(.OutputKey == "LambdaFunctionName") | .OutputValue'` 8 | 9 | npm run build 10 | npm run package 11 | 12 | aws lambda update-function-code \ 13 | --region ${STACK_REGION} \ 14 | --function-name ${FUNC_NAME} \ 15 | --zip-file fileb://dist/function.zip 16 | 17 | rm ./dist/function.zip 18 | -------------------------------------------------------------------------------- /lambda/api/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019. Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0/ 9 | * 10 | * or in the "license" file accompanying this file. 11 | * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 12 | * OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions 14 | * and limitations under the License. 15 | * 16 | */ 17 | 18 | module.exports = { 19 | entry: ['./dist/src/index.js'], 20 | target: 'node', 21 | mode: 'production', 22 | output: { 23 | path: `${process.cwd()}/dist/packed`, 24 | filename: 'index.js', 25 | libraryTarget: 'umd' 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cognito-example-for-external-idp-lambda-pretokengeneration", 3 | "version": "0.1.2", 4 | "author": "Eran Medan", 5 | "license": "MIT-0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/aws-samples/amazon-cognito-example-for-external-idp.git" 9 | }, 10 | "description": "The trigger that maps groups from a single attribute (e.g. '[group1,group2]' to a cognito:groups array claim", 11 | "main": "index.js", 12 | "scripts": { 13 | "ts-node": "ts-node", 14 | "build": "tsc -b", 15 | "watch": "tsc -w", 16 | "clean": "tsc -b --clean && rm -rf dist", 17 | "test": "mocha -r ts-node/register tests/**/*.test.ts", 18 | "testWithCoverage": "nyc -r lcov -e .ts -x \"*.test.ts\" mocha -r ts-node/register tests/**/*.test.ts && nyc report" 19 | }, 20 | "keywords": [], 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.33", 23 | "@types/chai": "^4.2.3", 24 | "@types/mocha": "^5.2.7", 25 | "@types/node": "^12.11.2", 26 | "chai": "^4.2.0", 27 | "mocha": "^10.8.2", 28 | "nyc": "^14.1.1", 29 | "ts-node": "^8.4.1", 30 | "tslint": "^5.20.0", 31 | "typescript": "^3.6.4" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/src/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * get the name of the custom user pool attribute holding the mapped groups 3 | */ 4 | export const getGroupsCustomAttributeName = () => process.env.GROUPS_ATTRIBUTE_CLAIM_NAME || "custom:groups"; 5 | 6 | /** 7 | * converts a string in the form of "[group1,group2,group3]" 8 | * into an array ["group1","group2","group3"] 9 | * 10 | * or a single value "group1" to an array ["group1"] 11 | * 12 | * @param groupsFromIdP 13 | */ 14 | export const parseGroupsAttribute = (groupsFromIdP: string): string[] => { 15 | if (groupsFromIdP) { 16 | if (groupsFromIdP.startsWith("[") && groupsFromIdP.endsWith("]")) { 17 | // this is how it is received from SAML mapping if we have more than one group 18 | // remove [ and ] chars. (we would use JSON.parse but the items in the list are not quoted) 19 | return groupsFromIdP 20 | .substring(1, groupsFromIdP.length - 1) // unwrap the [ and ] 21 | .split(/\s*,\s*/) // split and handle whitespace 22 | .filter(group => group.length > 0); // handle the case of "[]" input 23 | 24 | } else { 25 | // this is just one group, no [ or ] added 26 | return [groupsFromIdP]; 27 | } 28 | } 29 | return []; 30 | }; 31 | 32 | /** 33 | * Type information for the lambda event for the PreTokenGeneration trigger 34 | */ 35 | export interface PreTokenGenerationEvent { 36 | triggerSource?: string; 37 | userPoolId?: string; 38 | request: { 39 | userAttributes: { [key: string]: string }, 40 | groupConfiguration: { 41 | groupsToOverride: string[], 42 | iamRolesToOverride: string[], 43 | preferredRole: string[] | null, 44 | }, 45 | }; 46 | 47 | response: { 48 | claimsOverrideDetails?: { 49 | claimsToAddOrOverride?: { [key: string]: string }, 50 | claimsToSuppress?: string[], 51 | 52 | groupOverrideDetails?: { 53 | groupsToOverride?: string[], 54 | iamRolesToOverride?: string[], 55 | preferredRole?: string, 56 | }, 57 | } | null, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/src/index.ts: -------------------------------------------------------------------------------- 1 | import {getGroupsCustomAttributeName, parseGroupsAttribute, PreTokenGenerationEvent} from "./helpers"; 2 | 3 | // noinspection JSUnusedGlobalSymbols 4 | /** 5 | * Converts a SAML mapped attribute, e.g. list of groups, to a cognito groups claim in the generated token 6 | * (groups claims are included in both id tokens and access tokens, where custom attributes only show in the id token) 7 | * 8 | * E.g. from a string attribute named "custom:groups" to an array attribute named "cognito:groups": 9 | *
10 |  * {
11 |  *  ...
12 |  *  "custom:groups": "[g1,g2]",
13 |  *  ...
14 |  * }
15 |  * 
16 | * to 17 | * 18 | *
19 |  * {
20 |  *  ...
21 |  *  "cognito:groups": ["g1","g2"],
22 |  *  ...
23 |  * }
24 |  * 
25 | * 26 | * To be used with the Pre Token Generation hook in Cognito. 27 | * 28 | * IMPORTANT: the scope `aws.cognito.signin.user.admin` should NOT be enabled for any app client that uses this 29 | * The reason is that with aws.cognito.signin.user.admin, users can modify their own attributes with their access token 30 | * 31 | * if you want to remove the temporary custom:groups attribute used as an intermediary from the token 32 | * 33 | * 34 | * event.response.claimsOverrideDetails.claimsToSuppress = [getGroupsCustomAttributeName()]; 35 | * 36 | * 37 | * @param event Lambda event as described above, 38 | * see here for details: 39 | * https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html 40 | * 41 | * @returns {Promise<*>} Lambda event as described above 42 | */ 43 | export const handler = async (event: PreTokenGenerationEvent): Promise => { 44 | 45 | event.response.claimsOverrideDetails = { 46 | groupOverrideDetails: { 47 | groupsToOverride: 48 | [ 49 | // any existing groups the user may belong to 50 | ...event.request.groupConfiguration.groupsToOverride, 51 | // groups from the IdP (parses a single value, e.g. "[g1,g2]" into a string array, e.g ["g1","g2"]) 52 | ...parseGroupsAttribute(event.request.userAttributes[getGroupsCustomAttributeName()]) 53 | ] 54 | } 55 | }; 56 | return event; 57 | }; 58 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression */ 2 | 3 | import {handler} from "../src"; 4 | import {expect} from "chai"; 5 | 6 | describe("lambda handler", () => { 7 | 8 | it("GET success - empty params", async () => { 9 | 10 | const result = await handler({ 11 | request: { 12 | groupConfiguration: 13 | { 14 | groupsToOverride: [], 15 | iamRolesToOverride: [], 16 | preferredRole: [], 17 | }, 18 | userAttributes: {}, 19 | }, 20 | response: {}, 21 | }); 22 | 23 | expect(result.response.claimsOverrideDetails!.claimsToSuppress).to.be.undefined; 24 | expect(result.response.claimsOverrideDetails!.groupOverrideDetails!.groupsToOverride).to.be.empty; 25 | expect(result.response.claimsOverrideDetails!.claimsToAddOrOverride).to.be.undefined; 26 | 27 | }); 28 | 29 | it("GET success - via attributes", async () => { 30 | 31 | const result = await handler({ 32 | request: { 33 | groupConfiguration: 34 | { 35 | groupsToOverride: [], 36 | iamRolesToOverride: [], 37 | preferredRole: [], 38 | }, 39 | userAttributes: { 40 | "custom:groups": "[test1, test2]", 41 | }, 42 | }, 43 | response: {}, 44 | }); 45 | 46 | expect(result.response.claimsOverrideDetails!.claimsToSuppress).to.be.undefined; 47 | // tslint:disable-next-line:max-line-length 48 | expect(result.response.claimsOverrideDetails!.groupOverrideDetails!.groupsToOverride).to.have.members(["test1", "test2"]); 49 | // expect(result.response.claimsOverrideDetails!.claimsToAddOrOverride).to.be.undefined; 50 | 51 | }); 52 | 53 | it("GET success - prior groups, empty attribute", async () => { 54 | 55 | const result = await handler({ 56 | request: { 57 | groupConfiguration: 58 | { 59 | groupsToOverride: ["test"], 60 | iamRolesToOverride: [], 61 | preferredRole: [], 62 | }, 63 | userAttributes: { 64 | "custom:groups": "[]", 65 | }, 66 | }, 67 | response: {}, 68 | }); 69 | 70 | expect(result.response.claimsOverrideDetails!.claimsToSuppress).to.be.undefined; 71 | // tslint:disable-next-line:max-line-length 72 | expect(result.response.claimsOverrideDetails!.groupOverrideDetails!.groupsToOverride).to.have.members(["test"]); 73 | 74 | }); 75 | 76 | it("GET success - mix", async () => { 77 | 78 | const result = await handler({ 79 | request: { 80 | groupConfiguration: 81 | { 82 | groupsToOverride: ["test"], 83 | iamRolesToOverride: [], 84 | preferredRole: [], 85 | }, 86 | userAttributes: { 87 | "custom:groups": "[DemoAppAdmins, DemoAppUsers]", 88 | }, 89 | }, 90 | response: {}, 91 | }); 92 | 93 | expect(result.response.claimsOverrideDetails!.claimsToSuppress).to.be.undefined; 94 | // tslint:disable-next-line:max-line-length 95 | expect(result.response.claimsOverrideDetails!.groupOverrideDetails!.groupsToOverride).to.have.members(["DemoAppAdmins", "DemoAppUsers", "test"]); 96 | // expect(result.response.claimsOverrideDetails!.claimsToAddOrOverride).to.be.undefined; 97 | 98 | }); 99 | 100 | it("GET success - prior groups, no attribute", async () => { 101 | 102 | const result = await handler({ 103 | request: { 104 | groupConfiguration: 105 | { 106 | groupsToOverride: ["test", "test2"], 107 | iamRolesToOverride: [], 108 | preferredRole: [], 109 | }, 110 | userAttributes: {}, 111 | }, 112 | response: {}, 113 | }); 114 | 115 | expect(result.response.claimsOverrideDetails!.claimsToSuppress).to.be.undefined; 116 | // tslint:disable-next-line:max-line-length 117 | expect(result.response.claimsOverrideDetails!.groupOverrideDetails!.groupsToOverride).to.have.members(["test", "test2"]); 118 | // expect(result.response.claimsOverrideDetails!.claimsToAddOrOverride).to.be.undefined; 119 | 120 | }); 121 | 122 | }); 123 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018"], 5 | "module": "commonjs", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "baseUrl": "./", 9 | "typeRoots": [ 10 | "node_modules/@types" 11 | ], 12 | "types": [ 13 | "node", 14 | "mocha" 15 | ], 16 | "esModuleInterop": true, 17 | "inlineSourceMap": false, 18 | "sourceMap": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lambda/pretokengeneration/tslint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "tslint:recommended" 3 | rules: 4 | no-console: false 5 | object-literal-sort-keys: false 6 | interface-name: 7 | options: never-prefix 8 | space-before-function-paren: false 9 | ordered-imports: false 10 | no-empty: false 11 | object-literal-key-quotes: false 12 | jsdoc-format: false 13 | trailing-comma: false 14 | arrow-parens: false 15 | object-literal-shorthand: false 16 | -------------------------------------------------------------------------------- /resize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Specify the desired volume size in GiB as a command line argument. If not specified, default to 20 GiB. 4 | SIZE=${1:-20} 5 | 6 | # Get the ID of the environment host Amazon EC2 instance. 7 | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id) 8 | REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/\(.*\)[a-z]/\1/') 9 | 10 | # Get the ID of the Amazon EBS volume associated with the instance. 11 | VOLUMEID=$(aws ec2 describe-instances \ 12 | --instance-id $INSTANCEID \ 13 | --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ 14 | --output text \ 15 | --region $REGION) 16 | 17 | # Resize the EBS volume. 18 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 19 | 20 | # Wait for the resize to finish. 21 | while [ \ 22 | "$(aws ec2 describe-volumes-modifications \ 23 | --volume-id $VOLUMEID \ 24 | --filters Name=modification-state,Values="optimizing","completed" \ 25 | --query "length(VolumesModifications)"\ 26 | --output text)" != "1" ]; do 27 | sleep 1 28 | done 29 | 30 | #Check if we're on an NVMe filesystem 31 | if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]] 32 | then 33 | # Rewrite the partition table so that the partition takes up all the space that it can. 34 | sudo growpart /dev/xvda 1 35 | 36 | # Expand the size of the file system. 37 | # Check if we're on AL2 38 | STR=$(cat /etc/os-release) 39 | SUB="VERSION_ID=\"2\"" 40 | if [[ "$STR" == *"$SUB"* ]] 41 | then 42 | sudo xfs_growfs -d / 43 | else 44 | sudo resize2fs /dev/xvda1 45 | fi 46 | 47 | else 48 | # Rewrite the partition table so that the partition takes up all the space that it can. 49 | sudo growpart /dev/nvme0n1 1 50 | 51 | # Expand the size of the file system. 52 | # Check if we're on AL2 53 | STR=$(cat /etc/os-release) 54 | SUB="VERSION_ID=\"2\"" 55 | if [[ "$STR" == *"$SUB"* ]] 56 | then 57 | sudo xfs_growfs -d / 58 | else 59 | sudo resize2fs /dev/nvme0n1p1 60 | fi 61 | fi -------------------------------------------------------------------------------- /synth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | ./build.sh 7 | 8 | cd ./cdk/ 9 | echo "generating CFN" 10 | npm run cdk-synth 11 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | source ./env.sh 5 | 6 | cd lambda/api && npm run test && cd - 7 | cd lambda/pretokengeneration && npm run test && cd - 8 | -------------------------------------------------------------------------------- /ui-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea/ 25 | react-amplify-demo.iml 26 | 27 | #amplify 28 | amplify/\#current-cloud-backend 29 | amplify/.config/local-* 30 | amplify/backend/amplify-meta.json 31 | amplify/backend/awscloudformation 32 | build/ 33 | dist/ 34 | node_modules/ 35 | aws-exports.js 36 | awsconfiguration.json 37 | src/config/autoGenConfig.* 38 | -------------------------------------------------------------------------------- /ui-react/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /ui-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cognito-example-for-external-idp-ui-react", 3 | "version": "0.2.0", 4 | "author": "Eran Medan", 5 | "license": "MIT-0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/aws-samples/amazon-cognito-example-for-external-idp.git" 9 | }, 10 | "description": "The frontend of the demo", 11 | "dependencies": { 12 | "@aws-amplify/ui-react": "6.5.5", 13 | "aws-amplify": "6.7.0", 14 | "react": "^16.14.0", 15 | "react-dom": "^16.14.0", 16 | "react-scripts": "^5.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "clean": "tsc -b --clean && rm -rf build", 24 | "compile-config": "tsc src/config/autoGenConfig.ts" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ], 35 | "devDependencies": { 36 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 37 | "@types/chai": "^4.2.3", 38 | "@types/jest": "^24.0.19", 39 | "@types/node": "^20.4.5", 40 | "@types/react": "^16.14.6", 41 | "@types/react-dom": "^16.9.12", 42 | "@types/sinon": "^17.0.3", 43 | "chai": "^4.2.0", 44 | "fork-ts-checker-webpack-plugin": "^6.5.3", 45 | "sinon": "^19.0.2", 46 | "typescript": "^5.1.6" 47 | }, 48 | "overrides": { 49 | "typescript": "^5.1.6" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/ui-react/public/favicon.ico -------------------------------------------------------------------------------- /ui-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | 26 | 27 | 28 | 29 | 30 | React App 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /ui-react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui-react/src/components/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cognito-example-for-external-idp/1cdb2a76a938da4382bd229930469d884ccc9151/ui-react/src/components/App.css -------------------------------------------------------------------------------- /ui-react/src/components/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import {APIService} from "../service/APIService"; 5 | import {Pet} from "../model/pet"; 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div'); 9 | 10 | const abstractPetService = (new class implements APIService{ 11 | forceSignOut(): Promise { 12 | return undefined; 13 | } 14 | deletePet(pet: Pet): Promise { 15 | return undefined; 16 | } 17 | getAllPets(): Promise { 18 | //TODO: implement 19 | return undefined; 20 | } 21 | 22 | savePet(pet: Pet): Promise { 23 | //TODO: implement 24 | return undefined; 25 | } 26 | }); 27 | 28 | ReactDOM.render(, div); 29 | ReactDOM.unmountComponentAtNode(div); 30 | }); 31 | -------------------------------------------------------------------------------- /ui-react/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import {ChangeEvent, Component, FormEvent, Fragment} from 'react'; 2 | import './App.css'; 3 | import { signOut, signInWithRedirect, fetchAuthSession } from 'aws-amplify/auth'; 4 | import { Hub } from 'aws-amplify/utils'; 5 | import {Pet} from "../model/pet"; 6 | import {User} from "../model/user"; 7 | import {APIService} from "../service/APIService"; 8 | 9 | const numberFormat = new Intl.NumberFormat(undefined, { 10 | style: 'currency', 11 | currency: 'USD', 12 | minimumFractionDigits: 0, 13 | maximumFractionDigits: 0 14 | }); 15 | 16 | interface AppProps { 17 | apiService: APIService 18 | } 19 | 20 | export interface State { 21 | authState?: 'signedIn' | 'signedOut' | 'loading'; 22 | user?: User; 23 | pets?: Pet[]; 24 | error?: any; 25 | message?: string; 26 | selectedPet?: Pet; 27 | loading?: boolean; 28 | } 29 | 30 | class App extends Component { 31 | 32 | private apiService: APIService; 33 | private user: User | undefined; 34 | 35 | constructor(props: AppProps) { 36 | super(props); 37 | this.apiService = props.apiService; 38 | this.state = { 39 | authState: 'loading', 40 | } 41 | } 42 | 43 | async componentDidMount() { 44 | console.log("componentDidMount"); 45 | Hub.listen('auth', async ({payload}) => { 46 | switch (payload.event) { 47 | case 'signInWithRedirect': 48 | this.user = await this.getUser(); 49 | // workaround for FF bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1422334 50 | window.location.hash = window.location.hash; 51 | this.setState({authState: 'signedIn', user: this.user}); 52 | break; 53 | case 'signInWithRedirect_failure': 54 | this.setState({ 55 | authState: 'signedOut', 56 | user: null, 57 | error: payload.message || 'Sign in failed' 58 | }); 59 | break; 60 | case 'signedIn': 61 | this.user = await this.getUser(); 62 | this.setState({authState: 'signedIn', user: this.user}); 63 | break; 64 | case 'signedOut': 65 | this.setState({authState: 'signedOut', user: null}); 66 | break; 67 | default: 68 | break; 69 | } 70 | }); 71 | 72 | const urlParams = new URLSearchParams(window.location.search); 73 | const idpParamName = 'identity_provider'; 74 | const idp = urlParams.get(idpParamName); 75 | 76 | try { 77 | let user = await this.getUser(); 78 | 79 | if (idp) { 80 | urlParams.delete(idpParamName); 81 | const params = urlParams.toString(); 82 | window.history.replaceState(null, null, window.location.pathname + (params ? '?' + params : '')); 83 | } 84 | 85 | this.setState({authState: 'signedIn', user: user}); 86 | } catch (e) { 87 | if (e === 'not authenticated' && idp) { 88 | await signInWithRedirect({ 89 | provider: { 90 | custom: 'Idp' 91 | } 92 | }); 93 | } else { 94 | console.warn(e); 95 | this.setState({authState: 'signedOut', user: null}); 96 | } 97 | } 98 | } 99 | 100 | private async getUser() { 101 | try { 102 | const { 103 | tokens: session 104 | } = await fetchAuthSession(); 105 | const user = new User(session); 106 | return user; 107 | } catch (error) { 108 | console.error('Authentication error:', error); 109 | throw new Error('not authenticated'); 110 | } 111 | } 112 | 113 | async componentDidUpdate(prevProps: Readonly, prevState: Readonly) { 114 | if (prevState.authState !== this.state.authState && this.state.authState === "signedIn") { 115 | await this.getAllPets(); 116 | } 117 | } 118 | 119 | async signOut() { 120 | try { 121 | this.setState({authState: 'signedOut', pets: null, user: null}); 122 | await signOut(); 123 | await this.apiService.forceSignOut(); 124 | } catch (e) { 125 | console.log(e); 126 | } 127 | } 128 | 129 | render() { 130 | 131 | const {authState, pets, user, error, selectedPet, message, loading}: Readonly = this.state; 132 | 133 | let username: string; 134 | let groups: string[] = []; 135 | if(user) { 136 | // using first name for display 137 | username = user.name || user.email; 138 | groups = user.groups; 139 | } 140 | return ( 141 | 142 | 195 | 196 |
197 | 198 | {error && 199 |
this.setState({error: null})}>{error.toString()}
} 200 | 201 | {message && 202 |
this.setState({message: null})}>{message.toString()}
} 203 | 204 | {authState === 'signedOut' &&
Please sign in
} 205 | 206 | {authState === 'signedIn' &&
207 | {pets && 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | {pets.map(pet => 219 | this.setState({selectedPet: pet})} 221 | className={selectedPet && pet.id === selectedPet.id ? "table-active" : ""} 222 | > 223 | 224 | 225 | 226 | ) 227 | } 228 | 229 |
ownertypeprice
{pet.ownerDisplayName}{pet.type}{numberFormat.format(pet.price || 0)}
} 230 | 231 | 232 | {selectedPet && selectedPet.id && 233 | } 234 | 235 | {} 236 | 237 | {} 238 | 239 | 240 | {selectedPet && 241 |
242 |
243 |
this.savePet(e)}> 244 | this.handleChange(e, (state, value) => state.selectedPet.id = value)}/> 246 | this.handleChange(e, (state, value) => state.selectedPet.type = value)}/> 248 | this.handleChange(e, (state, value) => state.selectedPet.price = this.getAsNumber(value))}/> 250 | 251 | 252 |
253 | 254 |
255 |
} 256 | 257 | 258 | {loading &&
259 |
260 | Loading... 261 |
262 |
} 263 | 264 | 265 |
} 266 | 267 |
268 | 269 | 270 |
271 | ); 272 | } 273 | 274 | handleChange(event: ChangeEvent, mapper: (state: State, value: any) => void) { 275 | const value = event.target.value; 276 | this.setState(state => { 277 | mapper(state, value); 278 | return state; 279 | }); 280 | } 281 | 282 | newOnClick() { 283 | // we explicitly set to null, undefined causes react to assume there was no change 284 | this.setState({selectedPet: new Pet()}); 285 | 286 | } 287 | 288 | async getAllPets() { 289 | try { 290 | this.setState({loading: true, selectedPet: undefined}); 291 | let pets: Pet[] = await this.apiService.getAllPets(); 292 | this.setState({pets, loading: false}); 293 | } catch (e) { 294 | console.log(e); 295 | this.setState({error: `Failed to load pets: ${e}`, pets: [], loading: false}); 296 | } 297 | } 298 | 299 | async savePet(event: FormEvent) { 300 | 301 | event.preventDefault(); 302 | 303 | 304 | const pet = this.state.selectedPet; 305 | 306 | if (!pet) { 307 | this.setState({error: "Pet is needed"}); 308 | return; 309 | } 310 | try { 311 | this.setState({loading: true}); 312 | await this.apiService.savePet(pet); 313 | 314 | await this.getAllPets(); 315 | } catch (e) { 316 | this.setState({error: "Failed to save pet. " + e, loading: false}); 317 | } 318 | } 319 | 320 | async deletePet() { 321 | 322 | if (!window.confirm("Are you sure?")) { 323 | return; 324 | } 325 | const pet = this.state.selectedPet; 326 | 327 | if (!pet) { 328 | this.setState({error: "Pet is needed"}); 329 | return; 330 | } 331 | try { 332 | this.setState({loading: true}); 333 | await this.apiService.deletePet(pet); 334 | return this.getAllPets(); 335 | } catch (e) { 336 | this.setState({error: "Failed to save pet. " + e, loading: false}); 337 | } 338 | } 339 | 340 | private getAsNumber(value: any): number | undefined { 341 | if (value) { 342 | try { 343 | return parseInt(value) 344 | } catch (ignored) { 345 | } 346 | } 347 | return undefined; 348 | } 349 | } 350 | 351 | export default App; 352 | -------------------------------------------------------------------------------- /ui-react/src/config/amplifyConfig.ts: -------------------------------------------------------------------------------- 1 | // auto generated based on CloudFormation stack output values 2 | import autoGenConfig from './autoGenConfig'; 3 | // for demonstration purposes, replace with actual URL 4 | 5 | export const REST_API_NAME = "main"; 6 | 7 | export default { 8 | Auth: { 9 | Cognito: { 10 | // REQUIRED - Amazon Cognito Region 11 | region: autoGenConfig.region, 12 | // OPTIONAL - Amazon Cognito User Pool ID 13 | userPoolId: autoGenConfig.cognitoUserPoolId, 14 | 15 | // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string) 16 | userPoolClientId: autoGenConfig.cognitoUserPoolAppClientId, 17 | 18 | // OPTIONAL - Enforce user authentication prior to accessing AWS resources or not 19 | // mandatorySignIn: false, 20 | loginWith: { 21 | oauth: { 22 | 23 | domain: autoGenConfig.cognitoDomain, 24 | 25 | scopes: ['phone', 'email', 'openid', 'profile'], 26 | 27 | redirectSignIn: [autoGenConfig.appUrl], 28 | 29 | redirectSignOut: [autoGenConfig.appUrl], 30 | 31 | responseType: 'code' as const, // or token 32 | 33 | // optional, for Cognito hosted ui specified options 34 | options: { 35 | // Indicates if the data collection is enabled to support Cognito advanced security features. By default, this flag is set to true. 36 | AdvancedSecurityDataCollectionFlag: true 37 | } 38 | } 39 | } 40 | } 41 | 42 | /*// OPTIONAL - Configuration for cookie storage 43 | // Note: if the secure flag is set to true, then the cookie transmission requires a secure protocol 44 | cookieStorage: { 45 | // REQUIRED - Cookie domain (only required if cookieStorage is provided) 46 | domain: '.yourdomain.com', 47 | // OPTIONAL - Cookie path 48 | path: '/', 49 | // OPTIONAL - Cookie expiration in days 50 | expires: 365, 51 | // OPTIONAL - Cookie secure flag 52 | // Either true or false, indicating if the cookie transmission requires a secure protocol (https). 53 | secure: true 54 | }, 55 | 56 | // OPTIONAL - customized storage object 57 | storage: new MyStorage(), 58 | 59 | // OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH' 60 | authenticationFlowType: 'USER_PASSWORD_AUTH'*/ 61 | 62 | }, 63 | 64 | API: { 65 | REST: { 66 | [REST_API_NAME]: { 67 | name: REST_API_NAME, 68 | endpoint: autoGenConfig.apiUrl // for local test change to something such as 'http://localhost:3001' 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ui-react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /ui-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import {HttpAPIService} from "./service/APIService"; 7 | import {Amplify} from 'aws-amplify'; 8 | import amplifyConfig from './config/amplifyConfig'; 9 | 10 | Amplify.configure({ 11 | Auth: amplifyConfig.Auth, 12 | API: amplifyConfig.API 13 | }); 14 | 15 | const apiService = new HttpAPIService(); 16 | 17 | 18 | ReactDOM.render(, document.getElementById('root')); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: https://bit.ly/CRA-PWA 23 | serviceWorker.unregister(); 24 | -------------------------------------------------------------------------------- /ui-react/src/model/pet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Pet 3 | */ 4 | export class Pet { 5 | 6 | constructor( 7 | public id?: string | null, 8 | public type?: string | null, 9 | public price?: number | null, 10 | public owner?: string | null, 11 | public ownerDisplayName?: string | null) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui-react/src/model/user.test.ts: -------------------------------------------------------------------------------- 1 | import {User} from "./user"; 2 | import {expect} from "chai"; 3 | 4 | describe("User class should", ()=>{ 5 | 6 | it("return the correct groups based on the claim", () => { 7 | const groups = ["a","b","c"]; 8 | const user = new User({ 9 | getSignInUserSession() { 10 | return { 11 | isValid() { 12 | return true; 13 | }, 14 | getIdToken() { 15 | return { 16 | decodePayload() { 17 | 18 | return { 19 | "cognito:groups": groups 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } as any); 27 | 28 | console.log(user.groups); 29 | expect(user.groups).to.have.members(groups); 30 | 31 | }); 32 | 33 | it("return an empty array if no groups", () => { 34 | const user = new User({ 35 | getSignInUserSession() { 36 | return { 37 | isValid() { 38 | return true; 39 | }, 40 | getIdToken() { 41 | return { 42 | decodePayload() { 43 | return {} 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } as any); 50 | 51 | console.log(user.groups); 52 | expect(user.groups).to.be.empty; 53 | 54 | }); 55 | 56 | it("return an empty object if invalid ", () => { 57 | const user = new User({ 58 | getSignInUserSession() { 59 | return { 60 | isValid() { 61 | return false; 62 | } 63 | } 64 | } 65 | } as any); 66 | 67 | console.log(user.attributes); 68 | expect(user.attributes).eql({}); 69 | 70 | }) 71 | it("return an empty object if session is null ", () => { 72 | const user = new User({ 73 | getSignInUserSession(): any { 74 | return null; 75 | } 76 | } as any); 77 | 78 | console.log(user.attributes); 79 | expect(user.attributes).eql({}); 80 | 81 | }) 82 | 83 | 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /ui-react/src/model/user.ts: -------------------------------------------------------------------------------- 1 | import { AuthTokens } from 'aws-amplify/auth'; 2 | 3 | export class User { 4 | private readonly _attributes?: { [id: string]: any }; 5 | 6 | constructor(session: AuthTokens) { 7 | this._attributes = session.idToken.payload; 8 | console.log(this._attributes) 9 | } 10 | 11 | get groups(): string[] { 12 | return this.attributes['cognito:groups'] || []; 13 | } 14 | 15 | get attributes(): { [id: string]: any } { 16 | return this._attributes || {}; 17 | } 18 | 19 | get name(): string { 20 | return this.attributes['name']; 21 | } 22 | 23 | get email(): string { 24 | return this.attributes['email']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui-react/src/service/APIService.ts: -------------------------------------------------------------------------------- 1 | import { Pet } from "../model/pet"; 2 | import { REST_API_NAME } from "../config/amplifyConfig"; 3 | import { post, get, put, del } from "aws-amplify/api"; 4 | import { signOut, fetchAuthSession } from "aws-amplify/auth"; 5 | 6 | export interface APIService { 7 | /** * returns all pets that the user is permitted to see */ 8 | getAllPets(): Promise< 9 | Pet[] 10 | >; 11 | /** * saves or updates a pet (if it has an id, it's an update, if not, it's a create) * @param pet the pet to save */ 12 | savePet( 13 | pet: Pet 14 | ): Promise; 15 | /** * deletes a pet, if the user is permitted * @param pet */ 16 | deletePet( 17 | pet: Pet 18 | ): Promise; 19 | /** * forces sign out globally */ 20 | forceSignOut(): Promise; 21 | } 22 | /** * As the name suggests, handles API calls to the Pet service. */ 23 | export class HttpAPIService 24 | implements APIService 25 | { 26 | /** * returns all pets that the user is permitted to see */ 27 | public async getAllPets(): Promise< 28 | Pet[] 29 | > { 30 | const authorizationHeader = await this.getAuthorizationHeader(); 31 | const response = await get({ 32 | apiName: REST_API_NAME, 33 | path: "pets", 34 | options: { headers: authorizationHeader }, 35 | }); 36 | 37 | const data = await (await response.response).body.json(); 38 | 39 | if (!Array.isArray(data)) { 40 | throw new Error('API response is not an array'); 41 | } 42 | 43 | return data as Pet[]; 44 | } 45 | /** * saves or updates a pet (if it has an id, it's an update, if not, it's a create) * 46 | * @param pet the pet to save */ 47 | public async savePet( 48 | pet: Pet 49 | ) { 50 | const authorizationHeader = await this.getAuthorizationHeader(); 51 | if (pet.id) { 52 | await put({ 53 | apiName: REST_API_NAME, 54 | path: `pets/${pet.id}`, 55 | options: { body: pet as Record, headers: authorizationHeader }, 56 | }); 57 | } else { 58 | await post({ 59 | apiName: REST_API_NAME, 60 | path: "pets", 61 | options: { body: pet as Record, headers: authorizationHeader }, 62 | }); 63 | } 64 | } 65 | /** * deletes a pet * @param pet */ 66 | public async deletePet( 67 | pet: Pet 68 | ): Promise { 69 | const authorizationHeader = await this.getAuthorizationHeader(); 70 | await del({ 71 | apiName: REST_API_NAME, 72 | path: `pets/${pet.id}`, 73 | options: { headers: authorizationHeader }, 74 | }); 75 | } 76 | /** * forces sign out globally */ 77 | public async forceSignOut() { 78 | const authorizationHeader = await this.getAuthorizationHeader(); 79 | try { 80 | await post({ 81 | apiName: "main", 82 | path: "forceSignOut", 83 | options: { headers: authorizationHeader }, 84 | }); 85 | } finally { 86 | await signOut(); 87 | } 88 | } 89 | private async getAuthorizationHeader() { 90 | const { tokens } = await fetchAuthSession(); 91 | return { Authorization: tokens?.idToken?.toString() }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ui-react/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /ui-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "strictNullChecks": false, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /workshop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export AWS_SDK_LOAD_CONFIG=1 # allows the SDK to load from config. see https://github.com/aws/aws-sdk-js/pull/1391 4 | export STACK_NAME=ExternalIdPDemo 5 | export STACK_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text) 6 | EC2_AVAIL_ZONE=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone` 7 | export EC2_REGION="`echo \"$EC2_AVAIL_ZONE\" | sed 's/[a-z]$//'`" 8 | aws configure set default.region ${EC2_REGION} 9 | export STACK_REGION=$(aws configure get region) 10 | export COGNITO_DOMAIN_NAME=auth-${STACK_ACCOUNT}-${STACK_REGION} 11 | export APP_FRONTEND_DEPLOY_MODE=cloudfront 12 | 13 | echo "this will run npm install in all relevant sub-folders, build the project, and install the CDK toolkit" 14 | 15 | cd lambda 16 | cd api 17 | npm install 18 | cd .. 19 | cd pretokengeneration 20 | npm install 21 | cd ../.. 22 | cd cdk 23 | npm install 24 | cd .. 25 | cd ui-react 26 | npm install 27 | cd .. 28 | 29 | cd lambda 30 | cd api 31 | npm run build 32 | cd .. 33 | cd pretokengeneration 34 | npm run build 35 | cd ../.. 36 | cd cdk 37 | npm run build 38 | cd .. 39 | echo "Build successful" 40 | 41 | touch ~/.aws/config 42 | cd cdk 43 | npm run cdk -- bootstrap 44 | cd .. 45 | 46 | echo "Building backend " 47 | 48 | cd lambda 49 | cd api 50 | npm run build 51 | cd .. 52 | cd pretokengeneration 53 | npm run build 54 | cd ../.. 55 | cd cdk 56 | npm run build 57 | cd .. 58 | echo "Build successful" 59 | 60 | echo "Deploying backend stack..." 61 | 62 | # deploy the cdk stack (ignore the error in case it's due to 'No updates are to be performed') 63 | cd cdk 64 | npm run cdk-deploy --silent || true 65 | cd .. 66 | STACK_STATUS=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region "${STACK_REGION}" --query "Stacks[].StackStatus[]" --output text) 67 | 68 | if [[ "${STACK_STATUS}" != "CREATE_COMPLETE" && "${STACK_STATUS}" != "UPDATE_COMPLETE" ]]; then 69 | echo "Stack is in an unexpected status: ${STACK_STATUS}" 70 | exit 1 71 | fi 72 | 73 | echo "Generating UI configuration..." 74 | 75 | echo "Generating config for UI based on stack outputs" 76 | cd cdk 77 | npm run generate-config -- "${STACK_NAME}" "${STACK_REGION}" ../ui-react/src/config/autoGenConfig.ts 78 | cd .. 79 | echo "Building UIs" 80 | 81 | cd ui-react 82 | npm run compile-config 83 | npm run build 84 | cd .. 85 | 86 | BUCKET_NAME=$(node --print "require('./ui-react/src/config/autoGenConfig.js').default.uiBucketName") 87 | APP_URL=$(node --print "require('./ui-react/src/config/autoGenConfig.js').default.appUrl") 88 | COGNITO_INSTRUCTIONS="Create some users (in the pool or your IdP) and assign them the groups 'pet-app-admins' and/or 'pet-app-users'" 89 | 90 | if [[ "${BUCKET_NAME}" != "" ]]; then 91 | 92 | 93 | if [[ "${APP_FRONTEND_DEPLOY_MODE}" == "s3" ]]; then 94 | echo "Publishing frontend to ${BUCKET_NAME}" 95 | 96 | # NOTE: for development / demo purposes only, we use a public-read ACL on the frontend static files 97 | # in a production scenario use CloudFront and keep the s3 objects private 98 | aws s3 sync --delete --acl public-read ./ui-react/build/ "s3://${BUCKET_NAME}" &> /dev/null 99 | echo "${COGNITO_INSTRUCTIONS}" 100 | echo "Then visit the app at: ${APP_URL}" 101 | fi 102 | 103 | if [[ "${APP_FRONTEND_DEPLOY_MODE}" == "cloudfront" ]]; then 104 | echo "Publishing frontend to ${BUCKET_NAME}, will be availabing via CloudFront" 105 | 106 | # since we serve from CloudFront, we can keep the objects private, so we don't pass --acl public-read here 107 | aws s3 sync --delete ./ui-react/build/ "s3://${BUCKET_NAME}" &> /dev/null 108 | 109 | echo "${COGNITO_INSTRUCTIONS}" 110 | echo "Then visit the app at: ${APP_URL} (may take a few minutes for the distribution to finish deployment)" 111 | fi 112 | 113 | 114 | elif [[ "${APP_URL}" == "http://localhost"* ]]; then 115 | 116 | echo "${COGNITO_INSTRUCTIONS}" 117 | echo "Then run: cd ./ui-react && npm start # will launch the app in your browser at ${APP_URL}" 118 | 119 | fi 120 | 121 | 122 | 123 | 124 | --------------------------------------------------------------------------------