├── .github └── workflows │ ├── build.yml │ └── update_snapshot.yml ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_en.md ├── backend ├── .gitignore ├── authorizer │ └── index.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── websocket │ └── index.ts ├── cdk ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── cdk.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── construct │ │ ├── auth.ts │ │ ├── events.ts │ │ ├── handler.ts │ │ ├── storage.ts │ │ └── websocket.ts │ └── stack │ │ └── backend.ts ├── package-lock.json ├── package.json ├── test │ ├── __snapshots__ │ │ └── cdk.test.ts.snap │ └── cdk.test.ts └── tsconfig.json ├── doc └── img │ ├── architecture.png │ └── ui.png └── frontend ├── .env.sample ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.tsx ├── components │ ├── echo-events.tsx │ └── echo.tsx ├── config.ts ├── favicon.svg ├── index.css └── main.tsx ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | pull_request: 8 | jobs: 9 | Build-and-Test-CDK: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: "22.x" 17 | - run: | 18 | npm ci 19 | npm run build 20 | npm run test 21 | npx cdk synth 22 | working-directory: ./cdk 23 | Build-and-Test-Backend: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: "22.x" 31 | - run: | 32 | npm ci 33 | npm run build 34 | working-directory: ./backend 35 | Build-and-Test-Frontend: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Use Node.js 40 | uses: actions/setup-node@v1 41 | with: 42 | node-version: "22.x" 43 | - run: | 44 | npm ci 45 | npm run build 46 | working-directory: ./frontend 47 | -------------------------------------------------------------------------------- /.github/workflows/update_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Update snapshot 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'dependabot/**' 8 | 9 | jobs: 10 | update: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "22.x" 18 | - run: | 19 | npm ci 20 | working-directory: ./backend 21 | - run: | 22 | npm ci 23 | npm run test -- -u 24 | working-directory: ./cdk 25 | - name: Add & Commit 26 | uses: EndBug/add-and-commit@v7.2.0 27 | with: 28 | add: "cdk/test/__snapshots__/." 29 | message: "update snapshot" 30 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 150 7 | } 8 | -------------------------------------------------------------------------------- /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, or recently closed, 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 *main* 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' 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](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 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 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket API Cognito Auth Sample 2 | [![Build](https://github.com/aws-samples/websocket-api-cognito-auth-sample/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/websocket-api-cognito-auth-sample/actions/workflows/build.yml) 3 | 4 | **NOTE:** [English version is here](README_en.md). 5 | 6 | ## 概要 7 | Amazon API Gateway WebSocket APIにCognito認証を組み込むサンプルです。 8 | 9 | Lambda AuthorizerとAPI GatewayのためのLambda関数と、バックエンドデプロイのためのCDKコード、動作確認のためのフロントエンドの実装が含まれます。 10 | 11 | > [!NOTE] 12 | > [AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html)での実装例も追加しました。こちらはCognito UserPoolを利用した認証の仕組みが隠蔽されるため、ユーザー側の実装はよりシンプルになります。詳細は[`echo-events.tsx` (React)](./frontend/src/components/echo-events.tsx)と [`events.ts` (CDK)](./cdk/lib//construct/events.ts)もご覧ください。 13 | 14 | ## アーキテクチャ 15 | 本サンプルは、WebSocket APIでのCognito JWT認証を実現するための最小限のアーキテクチャを実装しています。 16 | 17 | 実装の詳細は、[実装の説明](#実装の説明)の節を参照してください。 18 | 19 | 本アーキテクチャを他のシステムと連携する際は、DynamoDBのテーブルに保存されたCognitoユーザーIDとWebSocket Connection IDのペアを利用することも可能です。 20 | 21 | ![architecture](doc/img/architecture.png) 22 | 23 | ## デプロイ方法 24 | **前提条件**: 下記が準備・インストールされていることを前提とします: 25 | * IAM権限の設定 26 | * `npm` のインストール 27 | 28 | 29 | バックエンドは下記のコマンドによりデプロイしてください。 30 | CDKについての詳細は、[Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html)をご覧ください。 31 | 32 | ```sh 33 | cd cdk 34 | npm ci 35 | npx cdk deploy --require-approval never 36 | ``` 37 | 38 | デプロイが完了すると、CLIに下記のメッセージが表示されます。 39 | 40 | ```sh 41 | Outputs: 42 | BackendWebSocketStack.AppSyncEventsEndpoint = https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/event 43 | BackendWebSocketStack.Region = ap-northeast-1 44 | BackendWebSocketStack.UserPoolId = ap-northeast-1_xxxxxxx 45 | BackendWebSocketStack.UserPoolWebClientId = xxxxxxxxxxxxxxxxxxxxxxxxxx 46 | BackendWebSocketStack.WebSocketEndpoint = wss://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod 47 | ``` 48 | 49 | この情報は次のフロントエンドのセットアップに利用することができます。 50 | 51 | フロントエンドは`localhost:3000`で動作確認します。 52 | 下記のように `.env.sample` をリネームして**必要な項目を埋めてください**。 53 | ```sh 54 | cd frontend 55 | cp .env.sample .env.local 56 | ``` 57 | 58 | あとは必要なパッケージをインストールして、localhostでlistenさせます。 59 | ```sh 60 | # frontend ディレクトリ内で実行 61 | npm ci 62 | npm run dev 63 | ``` 64 | 65 | ブラウザを開き、http://localhost:3000 にアクセスしてください。 66 | 67 | ## 実装の説明 68 | ### IaC 69 | 下記のAWSリソースを作成するCDKプロジェクトです: 70 | * API Gateway WebSocket API 71 | * Lambda関数 2つ 72 | * WebSocket APIのインテグレーション 73 | * Lambda Authorizer 74 | * Cognito ユーザープール・クライアント 75 | * DynamoDB テーブル 76 | * Cognito userIdとWebSocket接続IDの対応を保存するテーブル 77 | 78 | コードは `cdk` ディレクトリ以下に存在します。 79 | 80 | ### バックエンド 81 | バックエンドは2つのLambda関数で構成されます。 82 | 1. `authorizer` 83 | 2. `websocket` 84 | 85 | `authorizer` はTypeScriptで実装された、[WebSocket API用のLambda authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html)です。 86 | この関数は次の処理を実行します: 87 | 1. `idToken` クエリストリングからJWTを取得し、デコード 88 | 2. Cognitoサーバーから、JWT署名の公開鍵を取得 89 | 3. JWTの`kid`からJWT署名を検証 90 | 4. その他のClaimを、Lambdaの環境変数で注入された値と比較して検証 91 | 92 | 実装の詳細は、[Verifying a JSON Web Token 93 | ](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html)も参照してください。 94 | 95 | `websocket` はTypeScriptで実装された、WebSocket APIのインテグレーションです。 96 | この関数はWebSocketのルートに応じて、次の処理を実行します: 97 | 1. `$connect` ルート 98 | * CognitoのUserIdとWebSocketの接続IDのペアをDynamoDBに保存します 99 | 2. `$disconnect` ルート 100 | * 接続IDをDynamoDBから削除します 101 | 3. `$default` ルート 102 | * 送られた内容をそのままエコーバックします 103 | 104 | DynamoDBに保存された情報を利用して、他のサーバーから特定のCognitoユーザーにWebSocketメッセージを送信する、といった用途に応用可能です。 105 | [Use @connections commands in your backend service 106 | ](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html)もご参照ください。 107 | 108 | ### フロントエンド 109 | フロントエンドは、ReactのSPAで構成されています。 110 | 主要なコンポーネントは [`src/components/echo.tsx`](frontend/src/components/echo.tsx) に実装されています。 111 | 112 | 認証のために、Cognito UserPoolのIDトークンをクエリストリングに付与しています。WebSocket APIの認証方法にはいくつか考えられますが、それぞれトレードオフがあります。詳細は[こちらのIssue](https://github.com/aws-samples/websocket-api-cognito-auth-sample/issues/15#issuecomment-1173401338)もご覧ください。 113 | 114 | ## Clean up 115 | 検証が完了した後は、下記のコマンドで作成されたAWSリソースを削除することができます。 116 | 117 | ```sh 118 | cd cdk 119 | npx cdk destroy --force 120 | ``` 121 | 122 | ## Security 123 | 124 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 125 | 126 | ## License 127 | 128 | This library is licensed under the MIT-0 License. See the LICENSE file. 129 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # WebSocket API Cognito Auth Sample 2 | [![Build](https://github.com/aws-samples/websocket-api-cognito-auth-sample/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/websocket-api-cognito-auth-sample/actions/workflows/build.yml) 3 | 4 | This sample demonstrates how to integrate Cognito authentication with Amazon API Gateway WebSocket API. 5 | 6 | It includes the Lambda implementations for Lambda Authorizer and API Gateway Lambda proxy, AWS Cloud Development Kit (CDK) code to deploy backend infrastructure, and React frontend implementation for demonstration purposes. 7 | 8 | > [!NOTE] 9 | > We added an example implementation for [AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html) as well. In this case, because most of the authentication mechanism using Cognito user pool is abstracted away, users will not need much code to implement auth. Please refer to [`echo-events.tsx` (React)](./frontend/src/components/echo-events.tsx) and [`events.ts` (CDK)](./cdk/lib//construct/events.ts) for more details. 10 | 11 | ## Architecture 12 | This sample contains the least and simplest set of implementations to achieve Cognito JWT authentication and authorization for WebSocket API. Please refer to [implementation](#implementation) section for the details. 13 | 14 | ![architecture](doc/img/architecture.png) 15 | 16 | When you integrate this architecture with other system, you can use the pairs of Cognito user ID and WebSocket Connection ID which is stored on the DynamoDB table. 17 | 18 | ## Deploy 19 | **Prerequisites**: You need the following tools to deploy this sample: 20 | 21 | * [Node.js](https://nodejs.org/en/download/) (>= v16) 22 | * [Docker](https://docs.docker.com/get-docker/) 23 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and a configured IAM profile 24 | 25 | Then run the following commands: 26 | 27 | ```sh 28 | cd backend 29 | npm ci 30 | cd ../cdk 31 | npm ci 32 | npx cdk bootstrap 33 | npx cdk deploy 34 | ``` 35 | 36 | Initial deployment usually takes about 10 minutes. After a successful deployment, you will get a CLI output like the below: 37 | 38 | ```sh 39 | Outputs: 40 | BackendWebSocketStack.AppSyncEventsEndpoint = https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/event 41 | BackendWebSocketStack.Region = ap-northeast-1 42 | BackendWebSocketStack.UserPoolId = ap-northeast-1_xxxxxxx 43 | BackendWebSocketStack.UserPoolWebClientId = xxxxxxxxxxxxxxxxxxxxxxxxxx 44 | BackendWebSocketStack.WebSocketEndpoint = wss://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod 45 | ``` 46 | 47 | You can use those values to configure the frontend app next. Please create `.env.local` file by running the following command: 48 | 49 | ```sh 50 | cd frontend 51 | cp .env.sample .env.local 52 | ``` 53 | 54 | and then fill all the required values in the file by referring to the stack outputs. 55 | 56 | Finally, run the below commands to start the frontend server locally: 57 | 58 | ```sh 59 | cd frontend 60 | npm ci 61 | npm run dev 62 | ``` 63 | 64 | You can now open the browser and go to `http://localhost:3000` to try the working demo. 65 | 66 | ## Implementation 67 | ### Infrastructure as Code 68 | Our CDK code deploy the following resources: 69 | 70 | * API Gateway WebSocket API 71 | * Two Lambda functions 72 | * WebSocket API integration 73 | * Lambda Authorizer 74 | * Cognito UserPool and UserPoolClient 75 | * DynamoDB Table 76 | * A table to store the association between Cognito User ID and WebSocket connection ID 77 | 78 | All the code is located at `cdk` directory. 79 | 80 | ### Backend 81 | Our backend consists of two Lambda functions (at `backend` directory): 82 | 83 | 1. `authorizer` 84 | 2. `websocket` 85 | 86 | `Authorizer` is [a Lambda authorizer for WebSocket API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html) written in TypeScript. This handler runs in the following steps: 87 | 88 | 1. Get a JWT from the request query string 89 | 2. Verify the token by [aws-jwt-verify library](https://github.com/awslabs/aws-jwt-verify) 90 | 91 | You can also refer to [Verifying a JSON Web Token](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html) for more details. 92 | 93 | `Websocket` is a WebSocket API Lambda proxy integration written in TypeScript. This handler runs differently according to the request route: 94 | 95 | 1. `$connect` route 96 | * Save the pair of Cognito User ID and WebSocket connection ID to the DynamoDB table 97 | 2. `$disconnect` route 98 | * Delete the WebSocket connection ID from the Dynamo table 99 | 3. `$default` route 100 | * Receive the message and just send it back as-is to the same WebSocket connection 101 | 102 | You can utilize the information stored on the DynamoDB table. For example you can send notification to a particular Cognito user from other services if they have established WebSocket connection. For more details, you can refer to [Use @connections commands in your backend service 103 | ](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html). 104 | 105 | ### Frontend 106 | Our frontend is a single page application using React. 107 | The main component is located at[`src/components/echo.tsx`](frontend/src/components/echo.tsx). 108 | 109 | For authentication, we add an ID token of Cognito UserPool to request query string. There are several ways to implement WebSocket API authentication and trade-offs among them. You can refer to [this issue comment](https://github.com/aws-samples/websocket-api-cognito-auth-sample/issues/15#issuecomment-1173401338) for more details. 110 | 111 | ## Clean up 112 | To avoid incurring future charges, clean up the resources you created. 113 | 114 | You can remove all the AWS resources deployed by this sample running the following command: 115 | 116 | ```sh 117 | npx cdk destroy --force 118 | ``` 119 | 120 | ## Security 121 | 122 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 123 | 124 | ## License 125 | 126 | This library is licensed under the MIT-0 License. See the LICENSE file. 127 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /backend/authorizer/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { APIGatewayRequestAuthorizerHandler } from "aws-lambda"; 5 | import { CognitoJwtVerifier } from "aws-jwt-verify"; 6 | 7 | const UserPoolId = process.env.USER_POOL_ID!; 8 | const AppClientId = process.env.APP_CLIENT_ID!; 9 | 10 | export const handler: APIGatewayRequestAuthorizerHandler = async (event, context) => { 11 | try { 12 | const verifier = CognitoJwtVerifier.create({ 13 | userPoolId: UserPoolId, 14 | tokenUse: "id", 15 | clientId: AppClientId, 16 | }); 17 | 18 | const encodedToken = event.queryStringParameters!.idToken!; 19 | const payload = await verifier.verify(encodedToken); 20 | console.log("Token is valid. Payload:", payload); 21 | 22 | return allowPolicy(event.methodArn, payload); 23 | } catch (error: any) { 24 | console.log(error.message); 25 | return denyAllPolicy(); 26 | } 27 | }; 28 | 29 | const denyAllPolicy = () => { 30 | return { 31 | principalId: "*", 32 | policyDocument: { 33 | Version: "2012-10-17", 34 | Statement: [ 35 | { 36 | Action: "*", 37 | Effect: "Deny", 38 | Resource: "*", 39 | } as const, 40 | ], 41 | }, 42 | }; 43 | }; 44 | 45 | const allowPolicy = (methodArn: string, idToken: any) => { 46 | return { 47 | principalId: idToken.sub, 48 | policyDocument: { 49 | Version: "2012-10-17", 50 | Statement: [ 51 | { 52 | Action: "execute-api:Invoke", 53 | Effect: "Allow", 54 | Resource: methodArn, 55 | } as const, 56 | ], 57 | }, 58 | context: { 59 | // set userId in the context 60 | userId: idToken.sub, 61 | }, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsc" 4 | }, 5 | "dependencies": { 6 | "@aws-sdk/client-apigatewaymanagementapi": "^3.350.0", 7 | "@aws-sdk/client-dynamodb": "^3.351.0", 8 | "@aws-sdk/lib-dynamodb": "^3.254.0", 9 | "aws-jwt-verify": "^4.0.0" 10 | }, 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.72", 13 | "@types/jest": "^26.0.10", 14 | "@types/node": "^14.14.37", 15 | "esbuild": "^0.25.0", 16 | "jest": "^29.7.0", 17 | "ts-jest": "^29.1.4", 18 | "ts-node": "^9.0.0", 19 | "typescript": "^4.9.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018", "dom"], 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": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "noEmit": true, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /backend/websocket/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | import { APIGatewayProxyHandler } from "aws-lambda"; 4 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 5 | import { DeleteCommand, DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; 6 | import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; 7 | 8 | const client = DynamoDBDocumentClient.from(new DynamoDBClient({})); 9 | const ConnectionTableName = process.env.CONNECTION_TABLE_NAME!; 10 | 11 | export const handler: APIGatewayProxyHandler = async (event, context) => { 12 | console.log(event); 13 | const routeKey = event.requestContext.routeKey!; 14 | const connectionId = event.requestContext.connectionId!; 15 | 16 | if (routeKey == "$connect") { 17 | const userId = event.requestContext.authorizer!.userId; 18 | 19 | try { 20 | await client.send( 21 | new PutCommand({ 22 | TableName: ConnectionTableName, 23 | Item: { 24 | userId: userId, 25 | connectionId: connectionId, 26 | removedAt: Math.ceil(Date.now() / 1000) + 3600 * 3, 27 | }, 28 | }), 29 | ); 30 | return { statusCode: 200, body: "Connected." }; 31 | } catch (err) { 32 | console.error(err); 33 | return { statusCode: 500, body: "Connection failed." }; 34 | } 35 | } 36 | if (routeKey == "$disconnect") { 37 | try { 38 | await removeConnectionId(connectionId); 39 | return { statusCode: 200, body: "Disconnected." }; 40 | } catch (err) { 41 | console.error(err); 42 | return { statusCode: 500, body: "Disconnection failed." }; 43 | } 44 | } 45 | 46 | // Just echo back messages in other route than connect, disconnect (for testing purpose) 47 | const domainName = event.requestContext.domainName!; 48 | // When we use a custom domain, we don't need to append a stage name 49 | const endpoint = domainName.endsWith("amazonaws.com") 50 | ? `https://${event.requestContext.domainName}/${event.requestContext.stage}` 51 | : `https://${event.requestContext.domainName}`; 52 | const managementApi = new ApiGatewayManagementApiClient({ 53 | endpoint, 54 | }); 55 | 56 | try { 57 | await managementApi.send( 58 | new PostToConnectionCommand({ 59 | ConnectionId: connectionId, 60 | Data: Buffer.from(JSON.stringify({ message: event.body }), "utf-8"), 61 | }), 62 | ); 63 | } catch (e: any) { 64 | if (e.statusCode == 410) { 65 | await removeConnectionId(connectionId); 66 | } else { 67 | console.log(e); 68 | throw e; 69 | } 70 | } 71 | 72 | return { statusCode: 200, body: "Received." }; 73 | }; 74 | 75 | const removeConnectionId = async (connectionId: string) => { 76 | return await client.send( 77 | new DeleteCommand({ 78 | TableName: ConnectionTableName, 79 | Key: { 80 | connectionId, 81 | }, 82 | }), 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | .tmp 11 | -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // SPDX-License-Identifier: MIT-0 4 | 5 | import { App } from "aws-cdk-lib"; 6 | import "source-map-support/register"; 7 | import { BackendStack } from "../lib/stack/backend"; 8 | 9 | const app = new App(); 10 | new BackendStack(app, `BackendWebSocketStack`); 11 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | 4 | "context": { 5 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 6 | "@aws-cdk/core:checkSecretUsage": true, 7 | "@aws-cdk/core:target-partitions": [ 8 | "aws", 9 | "aws-cn" 10 | ], 11 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 12 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 13 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 14 | "@aws-cdk/aws-iam:minimizePolicies": true, 15 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 16 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 17 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 18 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 19 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 20 | "@aws-cdk/core:enablePartitionLiterals": true, 21 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 22 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 23 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 24 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 25 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 26 | "@aws-cdk/aws-route53-patters:useCertificate": true, 27 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 28 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 29 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 30 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 31 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 32 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 33 | "@aws-cdk/aws-redshift:columnId": true, 34 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 35 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 36 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 37 | "@aws-cdk/aws-kms:aliasNameRef": true, 38 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 39 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 40 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 41 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 42 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 43 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 44 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 45 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 46 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 47 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 48 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 49 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 50 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 51 | "@aws-cdk/cognito:logUserPoolClientSecretValue": false 52 | }, 53 | "watch": { 54 | "include": [ 55 | "**", 56 | "../backend/**/*.ts" 57 | ], 58 | "exclude": [ 59 | "README.md", 60 | "cdk*.json", 61 | "**/*.d.ts", 62 | "**/*.js", 63 | "tsconfig.json", 64 | "package*.json", 65 | "yarn.lock", 66 | "node_modules", 67 | "../backend/node_modules", 68 | "test" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | testMatch: ["**/*.test.ts"], 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cdk/lib/construct/auth.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Construct } from "constructs"; 5 | import { aws_cognito as cognito, RemovalPolicy } from "aws-cdk-lib"; 6 | 7 | export class Auth extends Construct { 8 | readonly userPool: cognito.IUserPool; 9 | readonly userPoolClient: cognito.IUserPoolClient; 10 | 11 | constructor(scope: Construct, id: string) { 12 | super(scope, id); 13 | 14 | const userPool = new cognito.UserPool(this, "UserPool", { 15 | selfSignUpEnabled: true, 16 | autoVerify: { 17 | email: true, 18 | }, 19 | removalPolicy: RemovalPolicy.DESTROY, 20 | }); 21 | 22 | const client = userPool.addClient("Client", { 23 | authFlows: { 24 | userPassword: true, 25 | userSrp: true, 26 | }, 27 | }); 28 | 29 | this.userPool = userPool; 30 | this.userPoolClient = client; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cdk/lib/construct/events.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { CfnApi, CfnApiKey, CfnChannelNamespace } from "aws-cdk-lib/aws-appsync"; 3 | import { CfnOutput, Names, Stack } from "aws-cdk-lib"; 4 | import { IUserPool } from "aws-cdk-lib/aws-cognito"; 5 | 6 | export interface EventsProps { 7 | userPool: IUserPool; 8 | } 9 | 10 | export class Events extends Construct { 11 | readonly endpoint: string; 12 | 13 | constructor(scope: Construct, id: string, props: EventsProps) { 14 | super(scope, id); 15 | 16 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-api.html 17 | const api = new CfnApi(this, "Resource", { 18 | eventConfig: { 19 | authProviders: [ 20 | { authType: "API_KEY" }, 21 | { 22 | authType: "AMAZON_COGNITO_USER_POOLS", 23 | cognitoConfig: { 24 | awsRegion: Stack.of(props.userPool).region, 25 | userPoolId: props.userPool.userPoolId, 26 | }, 27 | }, 28 | ], 29 | connectionAuthModes: [{ authType: "API_KEY" }, { authType: "AMAZON_COGNITO_USER_POOLS" }], 30 | defaultPublishAuthModes: [{ authType: "API_KEY" }, { authType: "AMAZON_COGNITO_USER_POOLS" }], 31 | defaultSubscribeAuthModes: [{ authType: "API_KEY" }, { authType: "AMAZON_COGNITO_USER_POOLS" }], 32 | }, 33 | name: Names.uniqueResourceName(this, { maxLength: 50, separator: "-" }), 34 | }); 35 | const apiId = api.getAtt("ApiId").toString(); 36 | 37 | const namespace = new CfnChannelNamespace(this, "Namespace", { 38 | apiId, 39 | name: "default", 40 | }); 41 | 42 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-apikey.html 43 | // const apiKey = new CfnApiKey(this, "ApiKey", { 44 | // apiId, 45 | // }); 46 | // new CfnOutput(this, "ApiKeyOutput", { value: apiKey.getAtt("ApiKey").toString() }); 47 | 48 | this.endpoint = `https://${api.getAtt("Dns.Http").toString()}/event`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cdk/lib/construct/handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Construct } from "constructs"; 5 | import { aws_dynamodb as dynamo, aws_lambda as lambda, aws_lambda_nodejs as lambdanode, aws_cognito as cognito, RemovalPolicy } from "aws-cdk-lib"; 6 | import { Runtime } from "aws-cdk-lib/aws-lambda"; 7 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 8 | 9 | interface HandlerProps { 10 | connectionIdTable: dynamo.ITable; 11 | userPool: cognito.IUserPool; 12 | userPoolClient: cognito.IUserPoolClient; 13 | } 14 | 15 | export class Handler extends Construct { 16 | readonly authHandler: lambda.IFunction; 17 | readonly websocketHandler: lambda.IFunction; 18 | 19 | constructor(scope: Construct, id: string, props: HandlerProps) { 20 | super(scope, id); 21 | 22 | const authHandler = new NodejsFunction(this, "AuthHandler", { 23 | runtime: Runtime.NODEJS_22_X, 24 | entry: "../backend/authorizer/index.ts", 25 | environment: { 26 | USER_POOL_ID: props.userPool.userPoolId, 27 | APP_CLIENT_ID: props.userPoolClient.userPoolClientId, 28 | }, 29 | bundling: { 30 | commandHooks: { 31 | beforeBundling: (i, o) => [`cd ${i} && npm ci`], 32 | afterBundling: (i, o) => [], 33 | beforeInstall: (i, o) => [], 34 | }, 35 | }, 36 | depsLockFilePath: "../backend/package-lock.json", 37 | }); 38 | 39 | const websocketHandler = new lambdanode.NodejsFunction(this, "WebSocketHandler", { 40 | runtime: Runtime.NODEJS_22_X, 41 | entry: "../backend/websocket/index.ts", 42 | environment: { 43 | CONNECTION_TABLE_NAME: props.connectionIdTable.tableName, 44 | }, 45 | bundling: { 46 | commandHooks: { 47 | beforeBundling: (i, o) => [`cd ${i} && npm ci`], 48 | afterBundling: (i, o) => [], 49 | beforeInstall: (i, o) => [], 50 | }, 51 | }, 52 | depsLockFilePath: "../backend/package-lock.json", 53 | }); 54 | 55 | props.connectionIdTable.grantReadWriteData(websocketHandler); 56 | 57 | this.authHandler = authHandler; 58 | this.websocketHandler = websocketHandler; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cdk/lib/construct/storage.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Construct } from "constructs"; 5 | import { aws_dynamodb as dynamo, RemovalPolicy } from "aws-cdk-lib"; 6 | 7 | export class Storage extends Construct { 8 | readonly connectionIdTable: dynamo.ITable; 9 | 10 | constructor(scope: Construct, id: string) { 11 | super(scope, id); 12 | 13 | const connectionIdTable = new dynamo.Table(this, "ConnectionIdTable", { 14 | partitionKey: { name: "connectionId", type: dynamo.AttributeType.STRING }, 15 | timeToLiveAttribute: "removedAt", 16 | billingMode: dynamo.BillingMode.PAY_PER_REQUEST, 17 | removalPolicy: RemovalPolicy.DESTROY, 18 | }); 19 | 20 | connectionIdTable.addGlobalSecondaryIndex({ 21 | partitionKey: { name: "userId", type: dynamo.AttributeType.STRING }, 22 | indexName: "userIdIndex", 23 | }); 24 | 25 | this.connectionIdTable = connectionIdTable; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cdk/lib/construct/websocket.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Construct } from "constructs"; 5 | import { aws_lambda as lambda } from "aws-cdk-lib"; 6 | import * as agw from "aws-cdk-lib/aws-apigatewayv2"; 7 | import * as agwi from "aws-cdk-lib/aws-apigatewayv2-integrations"; 8 | import * as agwa from "aws-cdk-lib/aws-apigatewayv2-authorizers"; 9 | 10 | interface WebSocketProps { 11 | websocketHandler: lambda.IFunction; 12 | authHandler: lambda.IFunction; 13 | /** 14 | * The querystring key for setting Cognito idToken. 15 | */ 16 | querystringKeyForIdToken?: string; 17 | } 18 | 19 | export class WebSocket extends Construct { 20 | readonly api: agw.WebSocketApi; 21 | private readonly defaultStageName = "prod"; 22 | 23 | constructor(scope: Construct, id: string, props: WebSocketProps) { 24 | super(scope, id); 25 | 26 | const authorizer = new agwa.WebSocketLambdaAuthorizer("Authorizer", props.authHandler, { 27 | identitySource: [`route.request.querystring.${props.querystringKeyForIdToken ?? "idToken"}`], 28 | }); 29 | 30 | this.api = new agw.WebSocketApi(this, "Api", { 31 | connectRouteOptions: { 32 | authorizer, 33 | integration: new agwi.WebSocketLambdaIntegration("ConnectIntegration", props.websocketHandler), 34 | }, 35 | disconnectRouteOptions: { 36 | integration: new agwi.WebSocketLambdaIntegration("DisconnectIntegration", props.websocketHandler), 37 | }, 38 | defaultRouteOptions: { 39 | integration: new agwi.WebSocketLambdaIntegration("DefaultIntegration", props.websocketHandler), 40 | }, 41 | }); 42 | 43 | new agw.WebSocketStage(this, `Stage`, { 44 | webSocketApi: this.api, 45 | stageName: this.defaultStageName, 46 | autoDeploy: true, 47 | }); 48 | } 49 | 50 | get apiEndpoint() { 51 | return `${this.api.apiEndpoint}/${this.defaultStageName}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cdk/lib/stack/backend.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { Construct } from "constructs"; 6 | import { Auth } from "../construct/auth"; 7 | import { Storage } from "../construct/storage"; 8 | import { Handler } from "../construct/handler"; 9 | import { WebSocket } from "../construct/websocket"; 10 | import { Events } from "../construct/events"; 11 | 12 | export class BackendStack extends cdk.Stack { 13 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 14 | super(scope, id, props); 15 | 16 | const auth = new Auth(this, `Auth`); 17 | const storage = new Storage(this, `Storage`); 18 | const handler = new Handler(this, `Handler`, { 19 | userPool: auth.userPool, 20 | userPoolClient: auth.userPoolClient, 21 | connectionIdTable: storage.connectionIdTable, 22 | }); 23 | 24 | const websocket = new WebSocket(this, `Websocket`, { 25 | authHandler: handler.authHandler, 26 | websocketHandler: handler.websocketHandler, 27 | }); 28 | 29 | websocket.api.grantManageConnections(handler.websocketHandler); 30 | 31 | const events = new Events(this, "Events", { userPool: auth.userPool }); 32 | 33 | { 34 | new cdk.CfnOutput(this, `Region`, { 35 | value: cdk.Stack.of(this).region, 36 | }); 37 | 38 | new cdk.CfnOutput(this, `UserPoolId`, { 39 | value: auth.userPool.userPoolId, 40 | }); 41 | 42 | new cdk.CfnOutput(this, `UserPoolWebClientId`, { 43 | value: auth.userPoolClient.userPoolClientId, 44 | }); 45 | 46 | new cdk.CfnOutput(this, `WebSocketEndpoint`, { 47 | value: websocket.apiEndpoint, 48 | }); 49 | 50 | new cdk.CfnOutput(this, `AppSyncEventsEndpoint`, { 51 | value: events.endpoint, 52 | }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^26.0.10", 15 | "@types/node": "10.17.27", 16 | "esbuild": "^0.25.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.4", 19 | "ts-node": "^10.0.0", 20 | "typescript": "~4.8.0" 21 | }, 22 | "dependencies": { 23 | "aws-cdk": "^2.1007.0", 24 | "aws-cdk-lib": "^2.189.1", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.16" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cdk/test/__snapshots__/cdk.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot test 1`] = ` 4 | { 5 | "Outputs": { 6 | "AppSyncEventsEndpoint": { 7 | "Value": { 8 | "Fn::Join": [ 9 | "", 10 | [ 11 | "https://", 12 | { 13 | "Fn::GetAtt": [ 14 | "EventsD32975C2", 15 | "Dns.Http", 16 | ], 17 | }, 18 | "/event", 19 | ], 20 | ], 21 | }, 22 | }, 23 | "Region": { 24 | "Value": { 25 | "Ref": "AWS::Region", 26 | }, 27 | }, 28 | "UserPoolId": { 29 | "Value": { 30 | "Ref": "AuthUserPool8115E87F", 31 | }, 32 | }, 33 | "UserPoolWebClientId": { 34 | "Value": { 35 | "Ref": "AuthUserPoolClientC635291F", 36 | }, 37 | }, 38 | "WebSocketEndpoint": { 39 | "Value": { 40 | "Fn::Join": [ 41 | "", 42 | [ 43 | { 44 | "Fn::GetAtt": [ 45 | "WebsocketApiD2C932E4", 46 | "ApiEndpoint", 47 | ], 48 | }, 49 | "/prod", 50 | ], 51 | ], 52 | }, 53 | }, 54 | }, 55 | "Parameters": { 56 | "BootstrapVersion": { 57 | "Default": "/cdk-bootstrap/hnb659fds/version", 58 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 59 | "Type": "AWS::SSM::Parameter::Value", 60 | }, 61 | }, 62 | "Resources": { 63 | "AuthUserPool8115E87F": { 64 | "DeletionPolicy": "Delete", 65 | "Properties": { 66 | "AccountRecoverySetting": { 67 | "RecoveryMechanisms": [ 68 | { 69 | "Name": "verified_phone_number", 70 | "Priority": 1, 71 | }, 72 | { 73 | "Name": "verified_email", 74 | "Priority": 2, 75 | }, 76 | ], 77 | }, 78 | "AdminCreateUserConfig": { 79 | "AllowAdminCreateUserOnly": false, 80 | }, 81 | "AutoVerifiedAttributes": [ 82 | "email", 83 | ], 84 | "EmailVerificationMessage": "The verification code to your new account is {####}", 85 | "EmailVerificationSubject": "Verify your new account", 86 | "SmsVerificationMessage": "The verification code to your new account is {####}", 87 | "VerificationMessageTemplate": { 88 | "DefaultEmailOption": "CONFIRM_WITH_CODE", 89 | "EmailMessage": "The verification code to your new account is {####}", 90 | "EmailSubject": "Verify your new account", 91 | "SmsMessage": "The verification code to your new account is {####}", 92 | }, 93 | }, 94 | "Type": "AWS::Cognito::UserPool", 95 | "UpdateReplacePolicy": "Delete", 96 | }, 97 | "AuthUserPoolClientC635291F": { 98 | "Properties": { 99 | "AllowedOAuthFlows": [ 100 | "implicit", 101 | "code", 102 | ], 103 | "AllowedOAuthFlowsUserPoolClient": true, 104 | "AllowedOAuthScopes": [ 105 | "profile", 106 | "phone", 107 | "email", 108 | "openid", 109 | "aws.cognito.signin.user.admin", 110 | ], 111 | "CallbackURLs": [ 112 | "https://example.com", 113 | ], 114 | "ExplicitAuthFlows": [ 115 | "ALLOW_USER_PASSWORD_AUTH", 116 | "ALLOW_USER_SRP_AUTH", 117 | "ALLOW_REFRESH_TOKEN_AUTH", 118 | ], 119 | "SupportedIdentityProviders": [ 120 | "COGNITO", 121 | ], 122 | "UserPoolId": { 123 | "Ref": "AuthUserPool8115E87F", 124 | }, 125 | }, 126 | "Type": "AWS::Cognito::UserPoolClient", 127 | }, 128 | "EventsD32975C2": { 129 | "Properties": { 130 | "EventConfig": { 131 | "AuthProviders": [ 132 | { 133 | "AuthType": "API_KEY", 134 | }, 135 | { 136 | "AuthType": "AMAZON_COGNITO_USER_POOLS", 137 | "CognitoConfig": { 138 | "AwsRegion": { 139 | "Ref": "AWS::Region", 140 | }, 141 | "UserPoolId": { 142 | "Ref": "AuthUserPool8115E87F", 143 | }, 144 | }, 145 | }, 146 | ], 147 | "ConnectionAuthModes": [ 148 | { 149 | "AuthType": "API_KEY", 150 | }, 151 | { 152 | "AuthType": "AMAZON_COGNITO_USER_POOLS", 153 | }, 154 | ], 155 | "DefaultPublishAuthModes": [ 156 | { 157 | "AuthType": "API_KEY", 158 | }, 159 | { 160 | "AuthType": "AMAZON_COGNITO_USER_POOLS", 161 | }, 162 | ], 163 | "DefaultSubscribeAuthModes": [ 164 | { 165 | "AuthType": "API_KEY", 166 | }, 167 | { 168 | "AuthType": "AMAZON_COGNITO_USER_POOLS", 169 | }, 170 | ], 171 | }, 172 | "Name": "BackendStack-Events-C56593BE", 173 | }, 174 | "Type": "AWS::AppSync::Api", 175 | }, 176 | "EventsNamespaceB28AAACA": { 177 | "Properties": { 178 | "ApiId": { 179 | "Fn::GetAtt": [ 180 | "EventsD32975C2", 181 | "ApiId", 182 | ], 183 | }, 184 | "Name": "default", 185 | }, 186 | "Type": "AWS::AppSync::ChannelNamespace", 187 | }, 188 | "HandlerAuthHandlerEB1BC6C8": { 189 | "DependsOn": [ 190 | "HandlerAuthHandlerServiceRoleC7D9E369", 191 | ], 192 | "Properties": { 193 | "Code": { 194 | "S3Bucket": { 195 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 196 | }, 197 | "S3Key": "49724f6f63ef051e4ffe30f9d395f77724d30d70203aa8936c4d8a237e3e0d53.zip", 198 | }, 199 | "Environment": { 200 | "Variables": { 201 | "APP_CLIENT_ID": { 202 | "Ref": "AuthUserPoolClientC635291F", 203 | }, 204 | "USER_POOL_ID": { 205 | "Ref": "AuthUserPool8115E87F", 206 | }, 207 | }, 208 | }, 209 | "Handler": "index.handler", 210 | "Role": { 211 | "Fn::GetAtt": [ 212 | "HandlerAuthHandlerServiceRoleC7D9E369", 213 | "Arn", 214 | ], 215 | }, 216 | "Runtime": "nodejs22.x", 217 | }, 218 | "Type": "AWS::Lambda::Function", 219 | }, 220 | "HandlerAuthHandlerServiceRoleC7D9E369": { 221 | "Properties": { 222 | "AssumeRolePolicyDocument": { 223 | "Statement": [ 224 | { 225 | "Action": "sts:AssumeRole", 226 | "Effect": "Allow", 227 | "Principal": { 228 | "Service": "lambda.amazonaws.com", 229 | }, 230 | }, 231 | ], 232 | "Version": "2012-10-17", 233 | }, 234 | "ManagedPolicyArns": [ 235 | { 236 | "Fn::Join": [ 237 | "", 238 | [ 239 | "arn:", 240 | { 241 | "Ref": "AWS::Partition", 242 | }, 243 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 244 | ], 245 | ], 246 | }, 247 | ], 248 | }, 249 | "Type": "AWS::IAM::Role", 250 | }, 251 | "HandlerWebSocketHandlerAD178334": { 252 | "DependsOn": [ 253 | "HandlerWebSocketHandlerServiceRoleDefaultPolicy29CB2487", 254 | "HandlerWebSocketHandlerServiceRole834C0C01", 255 | ], 256 | "Properties": { 257 | "Code": { 258 | "S3Bucket": { 259 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 260 | }, 261 | "S3Key": "1420582258203038c25a74ce463fd06027cf773e9fd7165eb8ed20a360b78bda.zip", 262 | }, 263 | "Environment": { 264 | "Variables": { 265 | "CONNECTION_TABLE_NAME": { 266 | "Ref": "StorageConnectionIdTable8B3A349D", 267 | }, 268 | }, 269 | }, 270 | "Handler": "index.handler", 271 | "Role": { 272 | "Fn::GetAtt": [ 273 | "HandlerWebSocketHandlerServiceRole834C0C01", 274 | "Arn", 275 | ], 276 | }, 277 | "Runtime": "nodejs22.x", 278 | }, 279 | "Type": "AWS::Lambda::Function", 280 | }, 281 | "HandlerWebSocketHandlerServiceRole834C0C01": { 282 | "Properties": { 283 | "AssumeRolePolicyDocument": { 284 | "Statement": [ 285 | { 286 | "Action": "sts:AssumeRole", 287 | "Effect": "Allow", 288 | "Principal": { 289 | "Service": "lambda.amazonaws.com", 290 | }, 291 | }, 292 | ], 293 | "Version": "2012-10-17", 294 | }, 295 | "ManagedPolicyArns": [ 296 | { 297 | "Fn::Join": [ 298 | "", 299 | [ 300 | "arn:", 301 | { 302 | "Ref": "AWS::Partition", 303 | }, 304 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 305 | ], 306 | ], 307 | }, 308 | ], 309 | }, 310 | "Type": "AWS::IAM::Role", 311 | }, 312 | "HandlerWebSocketHandlerServiceRoleDefaultPolicy29CB2487": { 313 | "Properties": { 314 | "PolicyDocument": { 315 | "Statement": [ 316 | { 317 | "Action": [ 318 | "dynamodb:BatchGetItem", 319 | "dynamodb:GetRecords", 320 | "dynamodb:GetShardIterator", 321 | "dynamodb:Query", 322 | "dynamodb:GetItem", 323 | "dynamodb:Scan", 324 | "dynamodb:ConditionCheckItem", 325 | "dynamodb:BatchWriteItem", 326 | "dynamodb:PutItem", 327 | "dynamodb:UpdateItem", 328 | "dynamodb:DeleteItem", 329 | "dynamodb:DescribeTable", 330 | ], 331 | "Effect": "Allow", 332 | "Resource": [ 333 | { 334 | "Fn::GetAtt": [ 335 | "StorageConnectionIdTable8B3A349D", 336 | "Arn", 337 | ], 338 | }, 339 | { 340 | "Fn::Join": [ 341 | "", 342 | [ 343 | { 344 | "Fn::GetAtt": [ 345 | "StorageConnectionIdTable8B3A349D", 346 | "Arn", 347 | ], 348 | }, 349 | "/index/*", 350 | ], 351 | ], 352 | }, 353 | ], 354 | }, 355 | { 356 | "Action": "execute-api:ManageConnections", 357 | "Effect": "Allow", 358 | "Resource": { 359 | "Fn::Join": [ 360 | "", 361 | [ 362 | "arn:", 363 | { 364 | "Ref": "AWS::Partition", 365 | }, 366 | ":execute-api:", 367 | { 368 | "Ref": "AWS::Region", 369 | }, 370 | ":", 371 | { 372 | "Ref": "AWS::AccountId", 373 | }, 374 | ":", 375 | { 376 | "Ref": "WebsocketApiD2C932E4", 377 | }, 378 | "/*/*/@connections/*", 379 | ], 380 | ], 381 | }, 382 | }, 383 | ], 384 | "Version": "2012-10-17", 385 | }, 386 | "PolicyName": "HandlerWebSocketHandlerServiceRoleDefaultPolicy29CB2487", 387 | "Roles": [ 388 | { 389 | "Ref": "HandlerWebSocketHandlerServiceRole834C0C01", 390 | }, 391 | ], 392 | }, 393 | "Type": "AWS::IAM::Policy", 394 | }, 395 | "StorageConnectionIdTable8B3A349D": { 396 | "DeletionPolicy": "Delete", 397 | "Properties": { 398 | "AttributeDefinitions": [ 399 | { 400 | "AttributeName": "connectionId", 401 | "AttributeType": "S", 402 | }, 403 | { 404 | "AttributeName": "userId", 405 | "AttributeType": "S", 406 | }, 407 | ], 408 | "BillingMode": "PAY_PER_REQUEST", 409 | "GlobalSecondaryIndexes": [ 410 | { 411 | "IndexName": "userIdIndex", 412 | "KeySchema": [ 413 | { 414 | "AttributeName": "userId", 415 | "KeyType": "HASH", 416 | }, 417 | ], 418 | "Projection": { 419 | "ProjectionType": "ALL", 420 | }, 421 | }, 422 | ], 423 | "KeySchema": [ 424 | { 425 | "AttributeName": "connectionId", 426 | "KeyType": "HASH", 427 | }, 428 | ], 429 | "TimeToLiveSpecification": { 430 | "AttributeName": "removedAt", 431 | "Enabled": true, 432 | }, 433 | }, 434 | "Type": "AWS::DynamoDB::Table", 435 | "UpdateReplacePolicy": "Delete", 436 | }, 437 | "WebsocketApiAuthorizer8462BD7C": { 438 | "Properties": { 439 | "ApiId": { 440 | "Ref": "WebsocketApiD2C932E4", 441 | }, 442 | "AuthorizerType": "REQUEST", 443 | "AuthorizerUri": { 444 | "Fn::Join": [ 445 | "", 446 | [ 447 | "arn:", 448 | { 449 | "Ref": "AWS::Partition", 450 | }, 451 | ":apigateway:", 452 | { 453 | "Ref": "AWS::Region", 454 | }, 455 | ":lambda:path/2015-03-31/functions/", 456 | { 457 | "Fn::GetAtt": [ 458 | "HandlerAuthHandlerEB1BC6C8", 459 | "Arn", 460 | ], 461 | }, 462 | "/invocations", 463 | ], 464 | ], 465 | }, 466 | "IdentitySource": [ 467 | "route.request.querystring.idToken", 468 | ], 469 | "Name": "Authorizer", 470 | }, 471 | "Type": "AWS::ApiGatewayV2::Authorizer", 472 | }, 473 | "WebsocketApiBackendStackWebsocketApiAuthorizerB3A276A3PermissionB6709820": { 474 | "Properties": { 475 | "Action": "lambda:InvokeFunction", 476 | "FunctionName": { 477 | "Fn::GetAtt": [ 478 | "HandlerAuthHandlerEB1BC6C8", 479 | "Arn", 480 | ], 481 | }, 482 | "Principal": "apigateway.amazonaws.com", 483 | "SourceArn": { 484 | "Fn::Join": [ 485 | "", 486 | [ 487 | "arn:", 488 | { 489 | "Ref": "AWS::Partition", 490 | }, 491 | ":execute-api:", 492 | { 493 | "Ref": "AWS::Region", 494 | }, 495 | ":", 496 | { 497 | "Ref": "AWS::AccountId", 498 | }, 499 | ":", 500 | { 501 | "Ref": "WebsocketApiD2C932E4", 502 | }, 503 | "/authorizers/", 504 | { 505 | "Ref": "WebsocketApiAuthorizer8462BD7C", 506 | }, 507 | ], 508 | ], 509 | }, 510 | }, 511 | "Type": "AWS::Lambda::Permission", 512 | }, 513 | "WebsocketApiD2C932E4": { 514 | "Properties": { 515 | "Name": "Api", 516 | "ProtocolType": "WEBSOCKET", 517 | "RouteSelectionExpression": "$request.body.action", 518 | }, 519 | "Type": "AWS::ApiGatewayV2::Api", 520 | }, 521 | "WebsocketApiconnectRouteConnectIntegration5ACB3823": { 522 | "Properties": { 523 | "ApiId": { 524 | "Ref": "WebsocketApiD2C932E4", 525 | }, 526 | "IntegrationType": "AWS_PROXY", 527 | "IntegrationUri": { 528 | "Fn::Join": [ 529 | "", 530 | [ 531 | "arn:", 532 | { 533 | "Ref": "AWS::Partition", 534 | }, 535 | ":apigateway:", 536 | { 537 | "Ref": "AWS::Region", 538 | }, 539 | ":lambda:path/2015-03-31/functions/", 540 | { 541 | "Fn::GetAtt": [ 542 | "HandlerWebSocketHandlerAD178334", 543 | "Arn", 544 | ], 545 | }, 546 | "/invocations", 547 | ], 548 | ], 549 | }, 550 | }, 551 | "Type": "AWS::ApiGatewayV2::Integration", 552 | }, 553 | "WebsocketApiconnectRouteConnectIntegrationPermissionC03255D5": { 554 | "Properties": { 555 | "Action": "lambda:InvokeFunction", 556 | "FunctionName": { 557 | "Fn::GetAtt": [ 558 | "HandlerWebSocketHandlerAD178334", 559 | "Arn", 560 | ], 561 | }, 562 | "Principal": "apigateway.amazonaws.com", 563 | "SourceArn": { 564 | "Fn::Join": [ 565 | "", 566 | [ 567 | "arn:", 568 | { 569 | "Ref": "AWS::Partition", 570 | }, 571 | ":execute-api:", 572 | { 573 | "Ref": "AWS::Region", 574 | }, 575 | ":", 576 | { 577 | "Ref": "AWS::AccountId", 578 | }, 579 | ":", 580 | { 581 | "Ref": "WebsocketApiD2C932E4", 582 | }, 583 | "/*$connect", 584 | ], 585 | ], 586 | }, 587 | }, 588 | "Type": "AWS::Lambda::Permission", 589 | }, 590 | "WebsocketApiconnectRouteFCBDA9B8": { 591 | "Properties": { 592 | "ApiId": { 593 | "Ref": "WebsocketApiD2C932E4", 594 | }, 595 | "AuthorizationType": "CUSTOM", 596 | "AuthorizerId": { 597 | "Ref": "WebsocketApiAuthorizer8462BD7C", 598 | }, 599 | "RouteKey": "$connect", 600 | "Target": { 601 | "Fn::Join": [ 602 | "", 603 | [ 604 | "integrations/", 605 | { 606 | "Ref": "WebsocketApiconnectRouteConnectIntegration5ACB3823", 607 | }, 608 | ], 609 | ], 610 | }, 611 | }, 612 | "Type": "AWS::ApiGatewayV2::Route", 613 | }, 614 | "WebsocketApidefaultRoute88AB6B56": { 615 | "Properties": { 616 | "ApiId": { 617 | "Ref": "WebsocketApiD2C932E4", 618 | }, 619 | "AuthorizationType": "NONE", 620 | "RouteKey": "$default", 621 | "Target": { 622 | "Fn::Join": [ 623 | "", 624 | [ 625 | "integrations/", 626 | { 627 | "Ref": "WebsocketApidefaultRouteDefaultIntegration4E7A24BE", 628 | }, 629 | ], 630 | ], 631 | }, 632 | }, 633 | "Type": "AWS::ApiGatewayV2::Route", 634 | }, 635 | "WebsocketApidefaultRouteDefaultIntegration4E7A24BE": { 636 | "Properties": { 637 | "ApiId": { 638 | "Ref": "WebsocketApiD2C932E4", 639 | }, 640 | "IntegrationType": "AWS_PROXY", 641 | "IntegrationUri": { 642 | "Fn::Join": [ 643 | "", 644 | [ 645 | "arn:", 646 | { 647 | "Ref": "AWS::Partition", 648 | }, 649 | ":apigateway:", 650 | { 651 | "Ref": "AWS::Region", 652 | }, 653 | ":lambda:path/2015-03-31/functions/", 654 | { 655 | "Fn::GetAtt": [ 656 | "HandlerWebSocketHandlerAD178334", 657 | "Arn", 658 | ], 659 | }, 660 | "/invocations", 661 | ], 662 | ], 663 | }, 664 | }, 665 | "Type": "AWS::ApiGatewayV2::Integration", 666 | }, 667 | "WebsocketApidefaultRouteDefaultIntegrationPermission32373F5B": { 668 | "Properties": { 669 | "Action": "lambda:InvokeFunction", 670 | "FunctionName": { 671 | "Fn::GetAtt": [ 672 | "HandlerWebSocketHandlerAD178334", 673 | "Arn", 674 | ], 675 | }, 676 | "Principal": "apigateway.amazonaws.com", 677 | "SourceArn": { 678 | "Fn::Join": [ 679 | "", 680 | [ 681 | "arn:", 682 | { 683 | "Ref": "AWS::Partition", 684 | }, 685 | ":execute-api:", 686 | { 687 | "Ref": "AWS::Region", 688 | }, 689 | ":", 690 | { 691 | "Ref": "AWS::AccountId", 692 | }, 693 | ":", 694 | { 695 | "Ref": "WebsocketApiD2C932E4", 696 | }, 697 | "/*$default", 698 | ], 699 | ], 700 | }, 701 | }, 702 | "Type": "AWS::Lambda::Permission", 703 | }, 704 | "WebsocketApidisconnectRoute04ED391F": { 705 | "Properties": { 706 | "ApiId": { 707 | "Ref": "WebsocketApiD2C932E4", 708 | }, 709 | "AuthorizationType": "NONE", 710 | "RouteKey": "$disconnect", 711 | "Target": { 712 | "Fn::Join": [ 713 | "", 714 | [ 715 | "integrations/", 716 | { 717 | "Ref": "WebsocketApidisconnectRouteDisconnectIntegrationB768D80E", 718 | }, 719 | ], 720 | ], 721 | }, 722 | }, 723 | "Type": "AWS::ApiGatewayV2::Route", 724 | }, 725 | "WebsocketApidisconnectRouteDisconnectIntegrationB768D80E": { 726 | "Properties": { 727 | "ApiId": { 728 | "Ref": "WebsocketApiD2C932E4", 729 | }, 730 | "IntegrationType": "AWS_PROXY", 731 | "IntegrationUri": { 732 | "Fn::Join": [ 733 | "", 734 | [ 735 | "arn:", 736 | { 737 | "Ref": "AWS::Partition", 738 | }, 739 | ":apigateway:", 740 | { 741 | "Ref": "AWS::Region", 742 | }, 743 | ":lambda:path/2015-03-31/functions/", 744 | { 745 | "Fn::GetAtt": [ 746 | "HandlerWebSocketHandlerAD178334", 747 | "Arn", 748 | ], 749 | }, 750 | "/invocations", 751 | ], 752 | ], 753 | }, 754 | }, 755 | "Type": "AWS::ApiGatewayV2::Integration", 756 | }, 757 | "WebsocketApidisconnectRouteDisconnectIntegrationPermission54ECF5B3": { 758 | "Properties": { 759 | "Action": "lambda:InvokeFunction", 760 | "FunctionName": { 761 | "Fn::GetAtt": [ 762 | "HandlerWebSocketHandlerAD178334", 763 | "Arn", 764 | ], 765 | }, 766 | "Principal": "apigateway.amazonaws.com", 767 | "SourceArn": { 768 | "Fn::Join": [ 769 | "", 770 | [ 771 | "arn:", 772 | { 773 | "Ref": "AWS::Partition", 774 | }, 775 | ":execute-api:", 776 | { 777 | "Ref": "AWS::Region", 778 | }, 779 | ":", 780 | { 781 | "Ref": "AWS::AccountId", 782 | }, 783 | ":", 784 | { 785 | "Ref": "WebsocketApiD2C932E4", 786 | }, 787 | "/*$disconnect", 788 | ], 789 | ], 790 | }, 791 | }, 792 | "Type": "AWS::Lambda::Permission", 793 | }, 794 | "WebsocketStage50369996": { 795 | "Properties": { 796 | "ApiId": { 797 | "Ref": "WebsocketApiD2C932E4", 798 | }, 799 | "AutoDeploy": true, 800 | "StageName": "prod", 801 | }, 802 | "Type": "AWS::ApiGatewayV2::Stage", 803 | }, 804 | }, 805 | "Rules": { 806 | "CheckBootstrapVersion": { 807 | "Assertions": [ 808 | { 809 | "Assert": { 810 | "Fn::Not": [ 811 | { 812 | "Fn::Contains": [ 813 | [ 814 | "1", 815 | "2", 816 | "3", 817 | "4", 818 | "5", 819 | ], 820 | { 821 | "Ref": "BootstrapVersion", 822 | }, 823 | ], 824 | }, 825 | ], 826 | }, 827 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 828 | }, 829 | ], 830 | }, 831 | }, 832 | } 833 | `; 834 | -------------------------------------------------------------------------------- /cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { App } from "aws-cdk-lib"; 5 | import { Template } from "aws-cdk-lib/assertions"; 6 | import { BackendStack } from "../lib/stack/backend"; 7 | 8 | test("Snapshot test", () => { 9 | const app = new App(); 10 | const stack = new BackendStack(app, "BackendStack"); 11 | expect(Template.fromStack(stack)).toMatchSnapshot(); 12 | }); 13 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 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": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "noEmit": true, 20 | "strictPropertyInitialization": false, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["cdk.out", "node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /doc/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/websocket-api-cognito-auth-sample/c143dedc4fc1c65279a02a6a519f956dd3241826/doc/img/architecture.png -------------------------------------------------------------------------------- /doc/img/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/websocket-api-cognito-auth-sample/c143dedc4fc1c65279a02a6a519f956dd3241826/doc/img/ui.png -------------------------------------------------------------------------------- /frontend/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_WEBSOCKET_API_URL=wss://xxxxx 2 | VITE_APPSYNC_EVENTS_URL=https://xxxxx/event 3 | VITE_USER_POOL_ID=xxxxx 4 | VITE_USER_POOL_CLIENT_ID=xxxxx 5 | VITE_AWS_REGION=ap-northeast-1 6 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env* 27 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend for demo 2 | A frontend app only for demo purpose, with React, Amplify libraries, MUI, and Vite. 3 | 4 | ![ui](../doc/img/ui.png) 5 | 6 | ## Run locally 7 | Create `.env.local` first. 8 | 9 | ```sh 10 | cp .env.sample .env.local 11 | # fill values according to the outputs of the CDK stack you deployed to AWS. 12 | ``` 13 | 14 | Then run Vite. 15 | 16 | ```sh 17 | npm run dev 18 | ``` 19 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Amazon API Gateway WebSocketAPI demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "bundle": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@aws-amplify/ui-react": "^6.0.0", 13 | "@emotion/react": "^11.9.0", 14 | "@emotion/styled": "^11.8.1", 15 | "@mui/material": "^6.0.0", 16 | "@rollup/plugin-node-resolve": "^15.3.0", 17 | "aws-amplify": "^6.0.0", 18 | "react": "^18.0.0", 19 | "react-dom": "^18.0.0", 20 | "react-hook-form": "^7.31.3" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.0", 24 | "@types/react-dom": "^18.0.0", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "typescript": "^4.6.3", 27 | "vite": "^6.3.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Amplify, ResourcesConfig } from "aws-amplify"; 2 | import config from "./config"; 3 | import "@aws-amplify/ui-react/styles.css"; 4 | import { Authenticator } from "@aws-amplify/ui-react"; 5 | import Echo from "./components/echo"; 6 | import { AppBar, Avatar, Button, Container, Select, SelectChangeEvent, Toolbar, Typography, MenuItem } from "@mui/material"; 7 | import { useState } from "react"; 8 | import EchoEvents from "./components/echo-events"; 9 | 10 | function App() { 11 | const [mode, setMode] = useState<"events" | "apigw">("apigw"); 12 | const handleModeChange = (e: SelectChangeEvent<"events" | "apigw">) => { 13 | setMode(e.target.value as any); 14 | }; 15 | 16 | const amplifyConfig: ResourcesConfig = { 17 | Auth: { 18 | Cognito: { 19 | userPoolId: config.userPoolId, 20 | userPoolClientId: config.userPoolClientId, 21 | }, 22 | }, 23 | API: { 24 | Events: { 25 | endpoint: config.eventsEndpoint, 26 | defaultAuthMode: "userPool", 27 | }, 28 | }, 29 | }; 30 | Amplify.configure(amplifyConfig); 31 | 32 | return ( 33 | <> 34 | 35 | {({ signOut, user }) => { 36 | return ( 37 | <> 38 | 39 | 40 | 41 | Real-time events with Cognito authentication 42 | 43 | 44 | {user == null ? "" : user.username} 45 | 48 | 49 | 50 |
51 | 52 | 56 | {mode === "events" ? : } 57 | 58 |
59 | 60 | ); 61 | }} 62 |
63 | 64 | ); 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /frontend/src/components/echo-events.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import { Typography, Button, TextField, Stack } from "@mui/material"; 3 | import { SubmitHandler, useForm } from "react-hook-form"; 4 | import { events } from "aws-amplify/data"; 5 | 6 | type EchoInput = { 7 | message: string; 8 | }; 9 | 10 | const EchoEvents: FC = () => { 11 | const { register, handleSubmit, reset } = useForm(); 12 | const [status, setStatus] = useState("initializing"); 13 | const [messages, setMessages] = useState([]); 14 | 15 | const initializeClient = async () => { 16 | console.log(`initializing connection...`); 17 | const channel = await events.connect("/default/test"); 18 | channel.subscribe({ 19 | next: (data) => { 20 | console.log(data); 21 | setMessages((prev) => [...prev, data.message.message]); 22 | }, 23 | error: (err) => { 24 | console.error(`error on subscription ${err}`); 25 | setStatus("error"); 26 | }, 27 | }); 28 | setStatus("connected"); 29 | console.log("successfully initialized connection."); 30 | return channel; 31 | }; 32 | 33 | const sendMessage: SubmitHandler = async (input) => { 34 | await events.post(`/default/test`, { message: input }); 35 | }; 36 | 37 | const handleUserKeyDown = (e: any) => { 38 | if (e.key === "Enter" && !e.shiftKey) { 39 | handleSubmit(sendMessage)(); // this won't be triggered 40 | } 41 | }; 42 | 43 | useEffect(() => { 44 | const pr = initializeClient(); 45 | return () => { 46 | pr.then((channel) => { 47 | channel.close(); 48 | // events.closeAll(); 49 | console.log(`successfully closed connection.`); 50 | }); 51 | setStatus("closed"); 52 | }; 53 | }, []); 54 | 55 | return ( 56 | 57 | 58 | AppSync Events Pub/Sub demo 59 | 60 | 61 | 62 | status: {status} 63 | 64 | 65 | 66 | 76 | 79 | 80 | 81 | 82 | Messages returned from AppSync Events 83 | 84 | 85 | {messages.map((msg, i) => ( 86 | 87 | {msg} 88 | 89 | ))} 90 | 91 | ); 92 | }; 93 | 94 | export default EchoEvents; 95 | -------------------------------------------------------------------------------- /frontend/src/components/echo.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useReducer, useState } from "react"; 2 | import { fetchAuthSession } from "aws-amplify/auth"; 3 | import { Typography, Button, TextField, Stack } from "@mui/material"; 4 | import { SubmitHandler, useForm } from "react-hook-form"; 5 | import config from "../config"; 6 | 7 | type EchoInput = { 8 | message: string; 9 | }; 10 | 11 | const Echo: FC = () => { 12 | const { register, handleSubmit, reset } = useForm(); 13 | const [status, setStatus] = useState("initializing"); 14 | const [messages, setMessages] = useState([]); 15 | const [client, setClient] = useState(); 16 | const [closed, forceClose] = useReducer(() => true, false); 17 | 18 | const initializeClient = async () => { 19 | const currentSession = await fetchAuthSession(); 20 | const idToken = currentSession.tokens?.idToken; 21 | 22 | const client = new WebSocket(`${config.apiEndpoint}?idToken=${idToken}`); 23 | 24 | client.onopen = () => { 25 | setStatus("connected"); 26 | }; 27 | 28 | client.onerror = (e: any) => { 29 | setStatus("error (reconnecting...)"); 30 | console.error(e); 31 | 32 | setTimeout(async () => { 33 | await initializeClient(); 34 | }); 35 | }; 36 | 37 | client.onclose = () => { 38 | if (!closed) { 39 | setStatus("closed (reconnecting...)"); 40 | 41 | setTimeout(async () => { 42 | await initializeClient(); 43 | }); 44 | } else { 45 | setStatus("closed"); 46 | } 47 | }; 48 | 49 | client.onmessage = async (message: any) => { 50 | const messageStr = JSON.parse(message.data); 51 | console.log(messages); 52 | setMessages((prev) => [...prev, messageStr.message]); 53 | }; 54 | 55 | setClient(client); 56 | }; 57 | 58 | const sendMessage: SubmitHandler = async (input) => { 59 | if (client != null) { 60 | client.send(input.message); 61 | reset({ message: "" }); 62 | } 63 | }; 64 | 65 | const handleUserKeyDown = (e: any) => { 66 | if (e.key === "Enter" && !e.shiftKey) { 67 | handleSubmit(sendMessage)(); // this won't be triggered 68 | } 69 | }; 70 | 71 | useEffect(() => { 72 | initializeClient(); 73 | return () => { 74 | if (client != null) { 75 | forceClose(); 76 | client.close(); 77 | } 78 | }; 79 | }, []); 80 | 81 | return ( 82 | 83 | 84 | WebSocket echo demo 85 | 86 | 87 | 88 | status: {status} 89 | 90 | 91 | 92 | 102 | 105 | 106 | 107 | 108 | Messages returned from WebSocket server 109 | 110 | 111 | {messages.map((msg) => ( 112 | {msg} 113 | ))} 114 | 115 | ); 116 | }; 117 | 118 | export default Echo; 119 | -------------------------------------------------------------------------------- /frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | apiEndpoint: import.meta.env.VITE_WEBSOCKET_API_URL!, 3 | eventsEndpoint: import.meta.env.VITE_APPSYNC_EVENTS_URL!, 4 | userPoolId: import.meta.env.VITE_USER_POOL_ID!, 5 | awsRegion: import.meta.env.VITE_AWS_REGION!, 6 | userPoolClientId: import.meta.env.VITE_USER_POOL_CLIENT_ID!, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import { Authenticator } from '@aws-amplify/ui-react'; 5 | import './index.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "types": ["vite/client"], 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | build: { 8 | rollupOptions: { 9 | // without this vite build fails 10 | // related to: https://github.com/aws-amplify/amplify-js/issues/9866 11 | external: ["mapbox-gl"], 12 | }, 13 | }, 14 | define: { 15 | // https://stackoverflow.com/a/73541205/18550269 16 | global: "window", 17 | }, 18 | plugins: [ 19 | react(), 20 | // https://github.com/aws/aws-sdk-js/issues/3673#issuecomment-1130779518 21 | { 22 | ...resolve({ 23 | preferBuiltins: false, 24 | browser: true, 25 | }), 26 | enforce: "pre", 27 | apply: "build", 28 | }, 29 | ], 30 | }); 31 | --------------------------------------------------------------------------------