From 5572fd18734531650e5cedb2bf032424b343507e Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Mon, 27 Jan 2025 13:00:55 +0000 Subject: [PATCH 01/15] adding Lambda function code and template --- .../README.md | 60 +++++ .../example-pattern.json | 59 +++++ .../src/authorizer.mjs | 32 +++ .../src/onconnect.mjs | 22 ++ .../src/ondisconnect.mjs | 20 ++ .../src/sendmessage.mjs | 46 ++++ .../template.yaml | 233 ++++++++++++++++++ 7 files changed, 472 insertions(+) create mode 100644 apigw-websocket-mapping-template-authorizer/README.md create mode 100644 apigw-websocket-mapping-template-authorizer/example-pattern.json create mode 100644 apigw-websocket-mapping-template-authorizer/src/authorizer.mjs create mode 100644 apigw-websocket-mapping-template-authorizer/src/onconnect.mjs create mode 100644 apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs create mode 100644 apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs create mode 100644 apigw-websocket-mapping-template-authorizer/template.yaml diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md new file mode 100644 index 000000000..3ca759d23 --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -0,0 +1,60 @@ +# AWS Service 1 to AWS Service 2 + +This pattern << explain usage >> + +Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd _patterns-model + ``` +1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam deploy --guided + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region + * Allow SAM CLI to create IAM roles with the required permissions. + + Once you have run `sam deploy --guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +1. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +Explain how the service interaction works. + +## Testing + +Provide steps to trigger the integration and show what should be observed if successful. + +## Cleanup + +1. Delete the stack + ```bash + aws cloudformation delete-stack --stack-name STACK_NAME + ``` +1. Confirm the stack has been deleted + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" + ``` +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-websocket-mapping-template-authorizer/example-pattern.json b/apigw-websocket-mapping-template-authorizer/example-pattern.json new file mode 100644 index 000000000..b7012bdfa --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/example-pattern.json @@ -0,0 +1,59 @@ +{ + "title": "Step Functions to Athena", + "description": "Create a Step Functions workflow to query Amazon Athena.", + "language": "Python", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to use an AWS Step Functions state machine to query Athena and get the results. This pattern is leveraging the native integration between these 2 services which means only JSON-based, structured language is used to define the implementation.", + "With Amazon Athena you can get up to 1000 results per invocation of the GetQueryResults method and this is the reason why the Step Function has a loop to get more results. The results are sent to a Map which can be configured to handle (the DoSomething state) the items in parallel or one by one by modifying the max_concurrency parameter.", + "This pattern deploys one Step Functions, two S3 Buckets, one Glue table and one Glue database." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/sfn-athena-cdk-python", + "templateURL": "serverless-patterns/sfn-athena-cdk-python", + "projectFolder": "sfn-athena-cdk-python", + "templateFile": "sfn_athena_cdk_python_stack.py" + } + }, + "resources": { + "bullets": [ + { + "text": "Call Athena with Step Functions", + "link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html" + }, + { + "text": "Amazon Athena - Serverless Interactive Query Service", + "link": "https://aws.amazon.com/athena/" + } + ] + }, + "deploy": { + "text": [ + "sam deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk delete." + ] + }, + "authors": [ + { + "name": "Your name", + "image": "link-to-your-photo.jpg", + "bio": "Your bio.", + "linkedin": "linked-in-ID", + "twitter": "twitter-handle" + } + ] +} diff --git a/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs b/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs new file mode 100644 index 000000000..651a6e626 --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs @@ -0,0 +1,32 @@ + export const handler = function(event, context, callback) { + console.log('Received event:', JSON.stringify(event, null, 2)); + // Retrieve request parameters from the Lambda function input: + var headers = event.headers; + + if (headers.token === "hello"){ + callback(null, generateAllow('me', event.methodArn)); + } else { + callback("Unauthorized"); + } + } + // Help function to generate an IAM policy + var generatePolicy = function(principalId, effect, resource) { + // Required output: + var authResponse = {}; + authResponse.principalId = principalId; + if (effect && resource) { + var policyDocument = {}; + policyDocument.Version = '2012-10-17'; // default version + policyDocument.Statement = []; + var statementOne = {}; + statementOne.Action = 'execute-api:Invoke'; // default action + statementOne.Effect = effect; + statementOne.Resource = resource; + policyDocument.Statement[0] = statementOne; + authResponse.policyDocument = policyDocument; + } + return authResponse; + } + var generateAllow = function(principalId, resource) { + return generatePolicy(principalId, 'Allow', resource); + } \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs b/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs new file mode 100644 index 000000000..834ee6a00 --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs @@ -0,0 +1,22 @@ + import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; + const client = new DynamoDBClient({ region: process.env.AWS_REGION }); + export const handler = async (event) => { + const connectionId = event.requestContext.connectionId; + console.log("connection ID:", connectionId ); // Print the connection ID + const putParams = { + "Item": { + "connectionId": { + "S": connectionId + }, + }, + "TableName": process.env.TABLE_NAME + }; + try { + const command = new PutItemCommand(putParams); + const response = await client.send(command); + console.log("put command:", response); // Print the response + } catch (err) { + return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; + } + return { statusCode: 200, body: "Connected." }; + }; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs b/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs new file mode 100644 index 000000000..10a8639fe --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs @@ -0,0 +1,20 @@ + import { DynamoDBClient, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; + const client = new DynamoDBClient({ region: process.env.AWS_REGION }); + export const handler = async (event) => { + const connectionId = event.requestContext.connectionId; + const deleteParams = { + "Key": { + "connectionId": { + "S": connectionId + }, + }, + "TableName": process.env.TABLE_NAME + }; + try { + const command = new DeleteItemCommand(deleteParams); + await client.send(command); + } catch (err) { + return { statusCode: 500, body: "Failed to disconnect: " + JSON.stringify(err) }; + } + return { statusCode: 200, body: "Disconnected." }; + }; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs new file mode 100644 index 000000000..86892da2d --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs @@ -0,0 +1,46 @@ + import { DynamoDBClient, ScanCommand, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; + import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; + const ddbClient = new DynamoDBClient({ region: process.env.AWS_REGION }); + + export const handler = async (event) => { + let connectionData; + console.log("event received:", event); //check the event received by Lambda + + //scanning DB table to get the Connection ID + const scanParams = { + TableName: process.env.TABLE_NAME, + ProjectionExpression: "connectionId", + }; + + const scanCommand = new ScanCommand(scanParams); + const responseDynamo = await ddbClient.send(scanCommand); + connectionData = responseDynamo.Items; + const connectionId = connectionData[0].connectionId.S; + console.log("connectionData:", connectionData); // print info about the DB connection + + //building endpoint from env variables, can also be buit from request parameters + const endpoint = "https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"; + + const apigwManagementApi = new ApiGatewayManagementApiClient({ apiVersion: "2018-11-29", + endpoint: endpoint, + }); + + const body = JSON.parse(event.body); + const message = body.data; + console.log("Message sent by client:", message); //print the message received from the request + + //parameters to post response to the connectionId + const postParams = { + ConnectionId: connectionId, + Data: Buffer.from("good job on deploying this template, keep slaying!!"), + }; + try{ + const postCommand = new PostToConnectionCommand(postParams); + const responseApi = await apigwManagementApi.send(postCommand); + console.log("response:", responseApi); //print the response + } catch (err) { + return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; + } + //response returned to API GW + return { statusCode: 200, body: "Data sent" }; + }; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/template.yaml b/apigw-websocket-mapping-template-authorizer/template.yaml new file mode 100644 index 000000000..0bbb147f5 --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/template.yaml @@ -0,0 +1,233 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM template for a websocket API with a mapping template and a Lambda integration + +Globals: + Function: + CodeUri: ./src + Runtime: nodejs22.x + +Resources: + WebsocketApi: + Type: AWS::ApiGatewayV2::Api + Properties: + Name: Websocket-api-mapping-template + ProtocolType: WEBSOCKET + RouteSelectionExpression: $request.body.action + + LambdaAuthorizer: + Type: AWS::ApiGatewayV2::Authorizer + Properties: + ApiId: !Ref WebsocketApi + AuthorizerType: REQUEST + AuthorizerUri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthFunction.Arn}/invocations + Name: lambda-authorizer-request + IdentitySource: + - route.request.header.token + + ConnectRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: + Ref: WebsocketApi + RouteKey: $connect + Target: + Fn::Join: + - / + - - integrations + - Ref: ConnectInteg + AuthorizationType: CUSTOM + AuthorizerId: !Ref LambdaAuthorizer + + ConnectInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: Ref! WebsocketApi + IntegrationType: AWS_PROXY + IntegrationUri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunction.Arn}/invocations + + DisconnectRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: + Ref: WebsocketApi + RouteKey: $disconnect + Target: + Fn::Join: + - / + - - integrations + - Ref: DisconnectInteg + DisconnectInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: + Ref: WebsocketApi + IntegrationType: AWS_PROXY + IntegrationUri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnDisconnectFunction.Arn}/invocations + + SendRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: + Ref: WebsocketApi + RouteKey: sendmessage + Target: + Fn::Join: + - / + - - integrations + - Ref: SendInteg + + SendInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: + Ref: WebsocketApi + Description: Send Integration + IntegrationType: AWS + IntegrationUri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SendMessageFunction.Arn}/invocations + RequestTemplates: + application/json: | + { + "requestContext": { + "routeKey": "$context.routeKey", + "messageId": "$context.messageId", + "auth": "$context.authorizer.principalId", + "test": "$context.authorizer.token", + "eventType": "$context.eventType", + "extendedRequestId": "$context.extendedRequestId", + "requestTime": "$context.requestTime", + "messageDirection": "$context.messageDirection", + "stage": "$context.stage", + "connectedAt": "$context.connectedAt", + "requestTimeEpoch": "$context.requestTimeEpoch", + "identity": { + "sourceIp": "$context.identity.sourceIp" + }, + "requestId": "$context.requestId", + "domainName": "$context.domainName", + "connectionId": "$context.connectionId", + "apiId": "$context.apiId" + }, + "body": "$util.escapeJavaScript($input.body)", + "isBase64Encoded": "$context.isBase64Encoded" + } + + Deployment: + Type: AWS::ApiGatewayV2::Deployment + DependsOn: + - ConnectRoute + - SendRoute + - DisconnectRoute + Properties: + ApiId: + Ref: WebsocketApi + + Stage: + Type: AWS::ApiGatewayV2::Stage + Properties: + StageName: slay + DeploymentId: + Ref: Deployment + ApiId: + Ref: WebsocketApi + + ConnectionsTable: + Type: AWS::Serverless::SimpleTable + Properties: + PrimaryKey: + Name: connectionId + Type: String + TableName: connection_table + + AuthFunction: + Type: AWS::Serverless::Function + Properties: + Handler: authorizer.handler + MemorySize: 256 + + LambdaAuthPermission: + Type: AWS::Lambda::Permission + DependsOn: + - WebsocketApi + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: AuthFunction + Principal: apigateway.amazonaws.com + + OnConnectFunction: + Type: AWS::Serverless::Function + Properties: + Handler: onconnect.handler + Environment: + Variables: + TABLE_NAME: !Ref ConnectionsTable + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ConnectionsTable + + OnConnectPermission: + Type: AWS::Lambda::Permission + DependsOn: + - WebsocketApi + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: OnConnectFunction + Principal: apigateway.amazonaws.com + + OnDisconnectFunction: + Type: AWS::Serverless::Function + Properties: + Handler: ondisconnect.handler + Environment: + Variables: + TABLE_NAME: !Ref ConnectionsTable + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ConnectionsTable + + OnDisconnectPermission: + Type: AWS::Lambda::Permission + DependsOn: + - WebsocketApi + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: OnDisconnectFunction + Principal: apigateway.amazonaws.com + + SendMessageFunction: + Type: AWS::Serverless::Function + DependsOn: + - WebsocketApi + Properties: + Handler: sendmessage.handler + Environment: + Variables: + TABLE_NAME: !Ref ConnectionsTable + API_ID: !Ref WebsocketApi + STAGE: "slay" + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ConnectionsTable + - Statement: + - Effect: Allow + Action: + - execute-api:ManageConnections + Resource: + - Fn::Sub: arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketApi}/* + + SendMessagePermission: + Type: AWS::Lambda::Permission + DependsOn: + - WebsocketApi + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: SendMessageFunction + Principal: apigateway.amazonaws.com From 25c9ba4e36ea9461da9c7e954303790a0be4edf5 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 12:33:35 +0000 Subject: [PATCH 02/15] added the Read.me file and more stuff --- .../README.md | 66 +++++++++++++++++-- .../example-pattern.json | 45 +++++++------ .../template.yaml | 21 +++--- 3 files changed, 99 insertions(+), 33 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 3ca759d23..24d172515 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -1,6 +1,6 @@ -# AWS Service 1 to AWS Service 2 +# AWS Wesocket API to Lambda -This pattern << explain usage >> +This pattern will create a websocket API protected by a Lambda authorizer. The websocket is integrated with a Lambda function through a mapping template that passes the main informations of the request. Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> @@ -38,11 +38,69 @@ Important: this application uses various AWS services and there are costs associ ## How it works -Explain how the service interaction works. +Websocket APIs are commonly used for 2-ways communications between a client and a server (like a chatbot for instance). +I once came across a scenario where a mapping template was needed, so I thought it could help other people if I published it here. + +The routes $connect and $disconnect have a proxy integration with their Lambdas. +The integration for the "sendmessage" route is non-proxy with a Lambda function in the back-end. +The Mapping Template used is this one : +``` + { + "requestContext": { + "routeKey": "$context.routeKey", + "messageId": "$context.messageId", + "auth": "$context.authorizer.principalId", + "token": "$context.authorizer.token", + "eventType": "$context.eventType", + "extendedRequestId": "$context.extendedRequestId", + "requestTime": "$context.requestTime", + "messageDirection": "$context.messageDirection", + "stage": "$context.stage", + "connectedAt": "$context.connectedAt", + "requestTimeEpoch": "$context.requestTimeEpoch", + "sourceIp": "$context.identity.sourceIp", + "requestId": "$context.requestId", + "domainName": "$context.domainName", + "connectionId": "$context.connectionId", + "apiId": "$context.apiId" + }, + "body": "$util.escapeJavaScript($input.body)", + "isBase64Encoded": "$context.isBase64Encoded" + } +``` +So it will pass a bunch of important information like the Body and the connectionId. You can add and remove as many variables as you want. +To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the @connection URL from its environment variables and the connectionId from the DynamoDB which name is also in the environment variables. + +Only the stage name "stage" is hardcodeded in the environment variable of the Lambda, so if you want to change it you would need to change the environment variable or get it from the Mapping Template in the event sent to Lambda. + +The Websocket API is also protected by a Lambda REQUEST Authorizer. This Lambda function will look for the header "token" and will only allow the request if its value is "hello" - else it will throw a 401 Unauthorized response to the client. + +All Lambda functions are written in Node.js 22 with ".mjs" files and implement the ES module import syntax. ## Testing -Provide steps to trigger the integration and show what should be observed if successful. +Once the template deployed, you would need to use a websocket client, I would recommend either Postman ior wscat. + +1. [Install NPM](https://www.npmjs.com/get-npm). + +1. Install wscat: + ``` + $ npm install -g wscat + ``` + +1. Connect to the WebSocket with the following command: + ``` + $ wscat --header token:hello -c wss:// + ``` +If you don't put the header and its value, you will get `Unauthorized` + +You can then send the Json Payload to the `sendmessage` route + + ``` + > {"action": "sendmessage","message" : "hey queen"} + < good job on deploying this template, keep slaying!! + + ``` ## Cleanup diff --git a/apigw-websocket-mapping-template-authorizer/example-pattern.json b/apigw-websocket-mapping-template-authorizer/example-pattern.json index b7012bdfa..34d5d8908 100644 --- a/apigw-websocket-mapping-template-authorizer/example-pattern.json +++ b/apigw-websocket-mapping-template-authorizer/example-pattern.json @@ -1,34 +1,38 @@ { - "title": "Step Functions to Athena", - "description": "Create a Step Functions workflow to query Amazon Athena.", - "language": "Python", + "title": "Websocket API Gateway with an Authorizer and mapping template", + "description": "Create a Websocket API with a Lambda authorizer and a Lambda in the back-end.", + "language": "NodeJs", "level": "200", - "framework": "CDK", + "framework": "SAM", "introBox": { "headline": "How it works", "text": [ - "This sample project demonstrates how to use an AWS Step Functions state machine to query Athena and get the results. This pattern is leveraging the native integration between these 2 services which means only JSON-based, structured language is used to define the implementation.", - "With Amazon Athena you can get up to 1000 results per invocation of the GetQueryResults method and this is the reason why the Step Function has a loop to get more results. The results are sent to a Map which can be configured to handle (the DoSomething state) the items in parallel or one by one by modifying the max_concurrency parameter.", - "This pattern deploys one Step Functions, two S3 Buckets, one Glue table and one Glue database." + "This projects demonstrates how to use a WebSocket API with a Lambda Authorizer", + "The WebSocket API does not have a Proxy integration with the back-end Lambda written NodeJs 22, instead it is using a mapping template that forwards the main information of the request.", + "The Connection ID is stored in a DynamoDB table and the Lambda sends a response to the websocket API through the @connections URL" ] }, "gitHub": { "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/sfn-athena-cdk-python", - "templateURL": "serverless-patterns/sfn-athena-cdk-python", - "projectFolder": "sfn-athena-cdk-python", - "templateFile": "sfn_athena_cdk_python_stack.py" + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-websocket-mapping-template-authorizer", + "templateURL": "serverless-patterns/apigw-websocket-mapping-template-authorizer", + "projectFolder": "apigw-websocket-mapping-template-authorizer", + "templateFile": "apigw-websocket-mapping-template-authorizer/template.yaml" } }, "resources": { "bullets": [ { - "text": "Call Athena with Step Functions", - "link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html" + "text": "Invoke a Webscoket API with wscat", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-wscat.html" }, { - "text": "Amazon Athena - Serverless Interactive Query Service", - "link": "https://aws.amazon.com/athena/" + "text": "Integration request in Websocket API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html" + }, + { + "text": "Control access to WebSocket APIs with AWS Lambda REQUEST authorizers", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html" } ] }, @@ -44,16 +48,15 @@ }, "cleanup": { "text": [ - "Delete the stack: cdk delete." + "Delete the stack: sam delete." ] }, "authors": [ { - "name": "Your name", - "image": "link-to-your-photo.jpg", - "bio": "Your bio.", - "linkedin": "linked-in-ID", - "twitter": "twitter-handle" + "name": "Alice Goumain", + "image": "https://media.licdn.com/dms/image/v2/C4E03AQFu1xnGt76xzg/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1662636937225?e=2147483647&v=beta&t=f4IFRXMLweHD9WieD3X1D3YkZO3Hf-bdTXHAfYcFpbo", + "bio": "Cloud Support Engineer in Serverless @ AWS", + "linkedin": "https://www.linkedin.com/in/alice-goumain/" } ] } diff --git a/apigw-websocket-mapping-template-authorizer/template.yaml b/apigw-websocket-mapping-template-authorizer/template.yaml index 0bbb147f5..142f41f07 100644 --- a/apigw-websocket-mapping-template-authorizer/template.yaml +++ b/apigw-websocket-mapping-template-authorizer/template.yaml @@ -96,7 +96,7 @@ Resources: "routeKey": "$context.routeKey", "messageId": "$context.messageId", "auth": "$context.authorizer.principalId", - "test": "$context.authorizer.token", + "token": "$context.authorizer.token", "eventType": "$context.eventType", "extendedRequestId": "$context.extendedRequestId", "requestTime": "$context.requestTime", @@ -104,9 +104,7 @@ Resources: "stage": "$context.stage", "connectedAt": "$context.connectedAt", "requestTimeEpoch": "$context.requestTimeEpoch", - "identity": { - "sourceIp": "$context.identity.sourceIp" - }, + "sourceIp": "$context.identity.sourceIp", "requestId": "$context.requestId", "domainName": "$context.domainName", "connectionId": "$context.connectionId", @@ -129,7 +127,7 @@ Resources: Stage: Type: AWS::ApiGatewayV2::Stage Properties: - StageName: slay + StageName: stage DeploymentId: Ref: Deployment ApiId: @@ -211,7 +209,7 @@ Resources: Variables: TABLE_NAME: !Ref ConnectionsTable API_ID: !Ref WebsocketApi - STAGE: "slay" + STAGE: "stage" Policies: - DynamoDBCrudPolicy: TableName: !Ref ConnectionsTable @@ -228,6 +226,13 @@ Resources: - WebsocketApi Properties: Action: lambda:InvokeFunction - FunctionName: - Ref: SendMessageFunction + FunctionName: !Ref SendMessageFunction Principal: apigateway.amazonaws.com + +Outputs: + WebSocketCommand: + Description: "The WSS command to use to connect" + Value: !Join [ '', [ 'wscat --header token:hello -c wss://', !Ref WebSocketApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref 'Stage'] ] + PayloadJson: + Description: "The Json payload you can send to try the sendmessage route" + Value: '{"action": "sendmessage","message" : "hey queen"}' From f85e367c8ec3bb09c2116f0d4ae44b45372d1e43 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:02:38 +0000 Subject: [PATCH 03/15] added a few bits --- .../README.md | 12 +++++++----- .../src/sendmessage.mjs | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 24d172515..013629bd7 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -41,7 +41,7 @@ Important: this application uses various AWS services and there are costs associ Websocket APIs are commonly used for 2-ways communications between a client and a server (like a chatbot for instance). I once came across a scenario where a mapping template was needed, so I thought it could help other people if I published it here. -The routes $connect and $disconnect have a proxy integration with their Lambdas. +The routes `$connect` and `$disconnect` have a proxy integration with their Lambdas. The integration for the "sendmessage" route is non-proxy with a Lambda function in the back-end. The Mapping Template used is this one : ``` @@ -68,15 +68,17 @@ The Mapping Template used is this one : "isBase64Encoded": "$context.isBase64Encoded" } ``` -So it will pass a bunch of important information like the Body and the connectionId. You can add and remove as many variables as you want. -To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the @connection URL from its environment variables and the connectionId from the DynamoDB which name is also in the environment variables. +So it will pass a bunch of important information like the `Body` and the `connectionId`. You can add and remove as many variables as you want. +To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the `@connection` URL from its environment variables and the `connectionId` from the DynamoDB which name is also in the environment variables. -Only the stage name "stage" is hardcodeded in the environment variable of the Lambda, so if you want to change it you would need to change the environment variable or get it from the Mapping Template in the event sent to Lambda. +Only the stage name `stage` is hard-codeded in the environment variable of the Lambda, so if you want to change it you would need to change the environment variable or get it from the Mapping Template in the event sent to Lambda. -The Websocket API is also protected by a Lambda REQUEST Authorizer. This Lambda function will look for the header "token" and will only allow the request if its value is "hello" - else it will throw a 401 Unauthorized response to the client. +The Websocket API is also protected by a Lambda REQUEST Authorizer. This Lambda function will look for the header `token` and will only allow the request if its value is `hello` - else it will throw a 401 Unauthorized response to the client. All Lambda functions are written in Node.js 22 with ".mjs" files and implement the ES module import syntax. +To get a response when the client sends a message, the Lambda has to send a request to the Websocket `connectioId`. It does so bu using the endpoint `"https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"` and the command `PostToConnectionCommand` from the client [`ApiGatewayManagementApiClient`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/apigatewaymanagementapi/). + ## Testing Once the template deployed, you would need to use a websocket client, I would recommend either Postman ior wscat. diff --git a/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs index 86892da2d..89639ddd6 100644 --- a/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs +++ b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs @@ -32,7 +32,7 @@ //parameters to post response to the connectionId const postParams = { ConnectionId: connectionId, - Data: Buffer.from("good job on deploying this template, keep slaying!!"), + Data: "good job on deploying this template, keep slaying!!", }; try{ const postCommand = new PostToConnectionCommand(postParams); @@ -41,6 +41,5 @@ } catch (err) { return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; } - //response returned to API GW return { statusCode: 200, body: "Data sent" }; }; \ No newline at end of file From cc79fdcdc600aa164e035664c478b4803aa14a6a Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:03:30 +0000 Subject: [PATCH 04/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 013629bd7..4a736a786 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -69,7 +69,7 @@ The Mapping Template used is this one : } ``` So it will pass a bunch of important information like the `Body` and the `connectionId`. You can add and remove as many variables as you want. -To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the `@connection` URL from its environment variables and the `connectionId` from the DynamoDB which name is also in the environment variables. +To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the endpoint from its environment variables and the `connectionId` from the DynamoDB which name is also in the environment variables. Only the stage name `stage` is hard-codeded in the environment variable of the Lambda, so if you want to change it you would need to change the environment variable or get it from the Mapping Template in the event sent to Lambda. From 3ec1323f43f649a17c07ff9759fad4bef9dbd414 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:04:46 +0000 Subject: [PATCH 05/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 4a736a786..dd27ef734 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -101,7 +101,6 @@ You can then send the Json Payload to the `sendmessage` route ``` > {"action": "sendmessage","message" : "hey queen"} < good job on deploying this template, keep slaying!! - ``` ## Cleanup From 2a7d194c1207fdb149c0d62fdb2a055b0a6814b2 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:05:29 +0000 Subject: [PATCH 06/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index dd27ef734..e9ec15dcc 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -98,7 +98,7 @@ If you don't put the header and its value, you will get `Unauthorized` You can then send the Json Payload to the `sendmessage` route - ``` + ```bash > {"action": "sendmessage","message" : "hey queen"} < good job on deploying this template, keep slaying!! ``` From a67620d8ace12f541b8d1d145e613025b818e103 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:06:15 +0000 Subject: [PATCH 07/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index e9ec15dcc..4c7f91763 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -97,11 +97,10 @@ Once the template deployed, you would need to use a websocket client, I would re If you don't put the header and its value, you will get `Unauthorized` You can then send the Json Payload to the `sendmessage` route - - ```bash - > {"action": "sendmessage","message" : "hey queen"} - < good job on deploying this template, keep slaying!! - ``` +``` +> {"action": "sendmessage","message" : "hey queen"} +< good job on deploying this template, keep slaying!! +``` ## Cleanup From 8336cf24a795cc81f36ce6ec45c3a0d1f3f57022 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 13:07:24 +0000 Subject: [PATCH 08/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 4c7f91763..ff7f5980c 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -21,7 +21,7 @@ Important: this application uses various AWS services and there are costs associ ``` 1. Change directory to the pattern directory: ``` - cd _patterns-model + cd apigw-websocket-mapping-template-authorizer ``` 1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: ``` @@ -96,7 +96,7 @@ Once the template deployed, you would need to use a websocket client, I would re ``` If you don't put the header and its value, you will get `Unauthorized` -You can then send the Json Payload to the `sendmessage` route +You can then send the Json Payload to the `sendmessage` route: ``` > {"action": "sendmessage","message" : "hey queen"} < good job on deploying this template, keep slaying!! From 5a09a170d3e7940ad5b7ed45ef87aebf0be2cb7f Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 14:13:52 +0000 Subject: [PATCH 09/15] added a few bits --- apigw-websocket-mapping-template-authorizer/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/template.yaml b/apigw-websocket-mapping-template-authorizer/template.yaml index 142f41f07..c8d7d7f9b 100644 --- a/apigw-websocket-mapping-template-authorizer/template.yaml +++ b/apigw-websocket-mapping-template-authorizer/template.yaml @@ -43,7 +43,7 @@ Resources: ConnectInteg: Type: AWS::ApiGatewayV2::Integration Properties: - ApiId: Ref! WebsocketApi + ApiId: !Ref WebsocketApi IntegrationType: AWS_PROXY IntegrationUri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunction.Arn}/invocations @@ -232,7 +232,7 @@ Resources: Outputs: WebSocketCommand: Description: "The WSS command to use to connect" - Value: !Join [ '', [ 'wscat --header token:hello -c wss://', !Ref WebSocketApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref 'Stage'] ] + Value: !Join [ '', [ 'wscat --header token:hello -c wss://', !Ref WebsocketApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref 'Stage'] ] PayloadJson: Description: "The Json payload you can send to try the sendmessage route" Value: '{"action": "sendmessage","message" : "hey queen"}' From ff66993bf6f35b128c7217b3cd5e898ec518ab5e Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Wed, 29 Jan 2025 14:20:54 +0000 Subject: [PATCH 10/15] added a few bits --- apigw-websocket-mapping-template-authorizer/README.md | 2 +- .../example-pattern.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index ff7f5980c..4a117dacd 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -1,4 +1,4 @@ -# AWS Wesocket API to Lambda +# AWS Websocket API to Lambda non-proxy This pattern will create a websocket API protected by a Lambda authorizer. The websocket is integrated with a Lambda function through a mapping template that passes the main informations of the request. diff --git a/apigw-websocket-mapping-template-authorizer/example-pattern.json b/apigw-websocket-mapping-template-authorizer/example-pattern.json index 34d5d8908..44a833546 100644 --- a/apigw-websocket-mapping-template-authorizer/example-pattern.json +++ b/apigw-websocket-mapping-template-authorizer/example-pattern.json @@ -9,7 +9,7 @@ "text": [ "This projects demonstrates how to use a WebSocket API with a Lambda Authorizer", "The WebSocket API does not have a Proxy integration with the back-end Lambda written NodeJs 22, instead it is using a mapping template that forwards the main information of the request.", - "The Connection ID is stored in a DynamoDB table and the Lambda sends a response to the websocket API through the @connections URL" + "The Connection ID is stored in a DynamoDB table and the Lambda sends a response to the websocket API through the endpoint URL" ] }, "gitHub": { From a36cbbf8e400abc0ab68b2c8a8ff5981b2647efd Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Tue, 29 Apr 2025 18:39:02 +0100 Subject: [PATCH 11/15] added all the fixes and made some changes to the description --- .../README.md | 36 +++------ .../src/authorizer.mjs | 64 +++++++-------- .../src/onconnect.mjs | 46 ++++++----- .../src/ondisconnect.mjs | 43 +++++----- .../src/sendmessage.mjs | 81 +++++++++---------- .../template.yaml | 18 +++-- 6 files changed, 144 insertions(+), 144 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index 4a117dacd..f2cd98d09 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -1,8 +1,8 @@ -# AWS Websocket API to Lambda non-proxy +# AWS API Gateway Websocket API to AWS Lambda with authorization and mapping template -This pattern will create a websocket API protected by a Lambda authorizer. The websocket is integrated with a Lambda function through a mapping template that passes the main informations of the request. +This pattern will create an AWS API Gateway Websocket API protected by a Lambda authorizer. The websocket is integrated with a Lambda function through a mapping template that passes the main informations of the request. -Learn more about this pattern at Serverless Land Patterns: << Add the live URL here >> +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-websocket-mapping-template-authorizer Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. @@ -12,6 +12,7 @@ Important: this application uses various AWS services and there are costs associ * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed +* [Install NPM](https://www.npmjs.com/get-npm). ## Deployment Instructions @@ -36,13 +37,11 @@ Important: this application uses various AWS services and there are costs associ 1. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for testing. -## How it works +## How it works + +The architecture uses proxy integration for the `$connect` and `$disconnect` routes, each linked to their respective AWS Lambda functions. The `sendmessage` route uses non-proxy integration with a backend Lambda function, incorporating a mapping template to pass essential data such as the message body and connection ID. -Websocket APIs are commonly used for 2-ways communications between a client and a server (like a chatbot for instance). -I once came across a scenario where a mapping template was needed, so I thought it could help other people if I published it here. -The routes `$connect` and `$disconnect` have a proxy integration with their Lambdas. -The integration for the "sendmessage" route is non-proxy with a Lambda function in the back-end. The Mapping Template used is this one : ``` { @@ -68,23 +67,17 @@ The Mapping Template used is this one : "isBase64Encoded": "$context.isBase64Encoded" } ``` -So it will pass a bunch of important information like the `Body` and the `connectionId`. You can add and remove as many variables as you want. -To make it as independant as I could, the back-end Lambda "SendMessageFunction" does not need any of these information to run successfully, because it is getting the endpoint from its environment variables and the `connectionId` from the DynamoDB which name is also in the environment variables. - -Only the stage name `stage` is hard-codeded in the environment variable of the Lambda, so if you want to change it you would need to change the environment variable or get it from the Mapping Template in the event sent to Lambda. +The backend Lambda function `SendMessageFunction` operates independently by retrieving the endpoint and DynamoDB table name from environment variables. The API stage name is defined in the Lambda environment variables. To modify the stage name, you can either update the environment variable or extract it from the mapping template in the event sent to Lambda. -The Websocket API is also protected by a Lambda REQUEST Authorizer. This Lambda function will look for the header `token` and will only allow the request if its value is `hello` - else it will throw a 401 Unauthorized response to the client. +This implementation includes security through a Lambda REQUEST Authorizer. The authorizer function validates the header `token`, granting access only when the token value is "hello". Requests with invalid tokens receive a 401 Unauthorized response. -All Lambda functions are written in Node.js 22 with ".mjs" files and implement the ES module import syntax. - -To get a response when the client sends a message, the Lambda has to send a request to the Websocket `connectioId`. It does so bu using the endpoint `"https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"` and the command `PostToConnectionCommand` from the client [`ApiGatewayManagementApiClient`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/apigatewaymanagementapi/). +All Lambda functions use Node.js 22 with ".mjs" files and implement ES module import syntax. To send responses to clients, the Lambda function constructs the endpoint URL using environment variables: +`"https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"` and the command `PostToConnectionCommand` from the client [`ApiGatewayManagementApiClient`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/apigatewaymanagementapi/). ## Testing Once the template deployed, you would need to use a websocket client, I would recommend either Postman ior wscat. -1. [Install NPM](https://www.npmjs.com/get-npm). - 1. Install wscat: ``` $ npm install -g wscat @@ -106,12 +99,9 @@ You can then send the Json Payload to the `sendmessage` route: 1. Delete the stack ```bash - aws cloudformation delete-stack --stack-name STACK_NAME - ``` -1. Confirm the stack has been deleted - ```bash - aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" + sam deploy ``` + ---- Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs b/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs index 651a6e626..9143e7f7f 100644 --- a/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs +++ b/apigw-websocket-mapping-template-authorizer/src/authorizer.mjs @@ -1,32 +1,32 @@ - export const handler = function(event, context, callback) { - console.log('Received event:', JSON.stringify(event, null, 2)); - // Retrieve request parameters from the Lambda function input: - var headers = event.headers; - - if (headers.token === "hello"){ - callback(null, generateAllow('me', event.methodArn)); - } else { - callback("Unauthorized"); - } - } - // Help function to generate an IAM policy - var generatePolicy = function(principalId, effect, resource) { - // Required output: - var authResponse = {}; - authResponse.principalId = principalId; - if (effect && resource) { - var policyDocument = {}; - policyDocument.Version = '2012-10-17'; // default version - policyDocument.Statement = []; - var statementOne = {}; - statementOne.Action = 'execute-api:Invoke'; // default action - statementOne.Effect = effect; - statementOne.Resource = resource; - policyDocument.Statement[0] = statementOne; - authResponse.policyDocument = policyDocument; - } - return authResponse; - } - var generateAllow = function(principalId, resource) { - return generatePolicy(principalId, 'Allow', resource); - } \ No newline at end of file +export const handler = function(event, context, callback) { + console.log('Received event:', JSON.stringify(event, null, 2)); + // Retrieve request parameters from the Lambda function input: + var headers = event.headers; + if (headers.token === "hello"){ + callback(null, generateAllow('me', event.methodArn)); + } else { + callback("Unauthorized"); + } +} +// Help function to generate an IAM policy +var generatePolicy = function(principalId, effect, resource) { + // Required output: + var authResponse = {}; + authResponse.principalId = principalId; + if (effect && resource) { + var policyDocument = {}; + policyDocument.Version = '2012-10-17'; // default version + policyDocument.Statement = []; + var statementOne = {}; + statementOne.Action = 'execute-api:Invoke'; // default action + statementOne.Effect = effect; + statementOne.Resource = resource; + policyDocument.Statement[0] = statementOne; + authResponse.policyDocument = policyDocument; + } + return authResponse; +} + +var generateAllow = function(principalId, resource) { + return generatePolicy(principalId, 'Allow', resource); +} \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs b/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs index 834ee6a00..cf0808ec2 100644 --- a/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs +++ b/apigw-websocket-mapping-template-authorizer/src/onconnect.mjs @@ -1,22 +1,24 @@ - import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; - const client = new DynamoDBClient({ region: process.env.AWS_REGION }); - export const handler = async (event) => { - const connectionId = event.requestContext.connectionId; - console.log("connection ID:", connectionId ); // Print the connection ID - const putParams = { - "Item": { - "connectionId": { - "S": connectionId - }, - }, - "TableName": process.env.TABLE_NAME - }; - try { - const command = new PutItemCommand(putParams); - const response = await client.send(command); - console.log("put command:", response); // Print the response - } catch (err) { - return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; - } - return { statusCode: 200, body: "Connected." }; - }; \ No newline at end of file +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient({ region: process.env.AWS_REGION }); + +export const handler = async (event) => { + const connectionId = event.requestContext.connectionId; + console.log("connection ID:", JSON.stringify(connectionId, null, 2) ); // Print the connection ID + const putParams = { + "Item": { + "connectionId": { + "S": connectionId + }, + }, + "TableName": process.env.TABLE_NAME + }; + try { + const command = new PutItemCommand(putParams); + const response = await client.send(command); + console.log("put command:", JSON.stringify(response, null, 2)); // Print the response + } catch (err) { + return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; + } + + return { statusCode: 200, body: "Connected." }; +}; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs b/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs index 10a8639fe..95e6761cf 100644 --- a/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs +++ b/apigw-websocket-mapping-template-authorizer/src/ondisconnect.mjs @@ -1,20 +1,23 @@ - import { DynamoDBClient, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; - const client = new DynamoDBClient({ region: process.env.AWS_REGION }); - export const handler = async (event) => { - const connectionId = event.requestContext.connectionId; - const deleteParams = { - "Key": { - "connectionId": { - "S": connectionId - }, - }, - "TableName": process.env.TABLE_NAME - }; - try { - const command = new DeleteItemCommand(deleteParams); - await client.send(command); - } catch (err) { - return { statusCode: 500, body: "Failed to disconnect: " + JSON.stringify(err) }; - } - return { statusCode: 200, body: "Disconnected." }; - }; \ No newline at end of file +import { DynamoDBClient, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient({ region: process.env.AWS_REGION }); + +export const handler = async (event) => { + const connectionId = event.requestContext.connectionId; + const deleteParams = { + "Key": { + "connectionId": { + "S": connectionId + }, + }, + "TableName": process.env.TABLE_NAME + }; + + try { + const command = new DeleteItemCommand(deleteParams); + await client.send(command); + } catch (err) { + return { statusCode: 500, body: "Failed to disconnect: " + JSON.stringify(err) }; + } + + return { statusCode: 200, body: "Disconnected." }; +}; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs index 89639ddd6..2e0a69df1 100644 --- a/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs +++ b/apigw-websocket-mapping-template-authorizer/src/sendmessage.mjs @@ -1,45 +1,42 @@ - import { DynamoDBClient, ScanCommand, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; - import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; - const ddbClient = new DynamoDBClient({ region: process.env.AWS_REGION }); +import { DynamoDBClient, ScanCommand, DeleteItemCommand } from "@aws-sdk/client-dynamodb"; +import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi"; +const ddbClient = new DynamoDBClient({ region: process.env.AWS_REGION }); - export const handler = async (event) => { - let connectionData; - console.log("event received:", event); //check the event received by Lambda +export const handler = async (event) => { + let connectionData; - //scanning DB table to get the Connection ID - const scanParams = { - TableName: process.env.TABLE_NAME, - ProjectionExpression: "connectionId", - }; - - const scanCommand = new ScanCommand(scanParams); - const responseDynamo = await ddbClient.send(scanCommand); - connectionData = responseDynamo.Items; - const connectionId = connectionData[0].connectionId.S; - console.log("connectionData:", connectionData); // print info about the DB connection - - //building endpoint from env variables, can also be buit from request parameters - const endpoint = "https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"; - - const apigwManagementApi = new ApiGatewayManagementApiClient({ apiVersion: "2018-11-29", - endpoint: endpoint, - }); - - const body = JSON.parse(event.body); - const message = body.data; - console.log("Message sent by client:", message); //print the message received from the request + //scanning DB table to get the Connection ID + const scanParams = { + TableName: process.env.TABLE_NAME, + ProjectionExpression: "connectionId", + }; + + const scanCommand = new ScanCommand(scanParams); + const responseDynamo = await ddbClient.send(scanCommand); + connectionData = responseDynamo.Items; + const connectionId = connectionData[0].connectionId.S; + console.log("connectionData:", connectionData); // print info about the DB connection + + //building endpoint from env variables, can also be buit from request parameters + const endpoint = "https://" + process.env.API_ID + ".execute-api." + process.env.AWS_REGION + ".amazonaws.com/" + process.env.STAGE + "/"; + + const apigwManagementApi = new ApiGatewayManagementApiClient({ apiVersion: "2018-11-29", + endpoint: endpoint, + }); - //parameters to post response to the connectionId - const postParams = { - ConnectionId: connectionId, - Data: "good job on deploying this template, keep slaying!!", - }; - try{ - const postCommand = new PostToConnectionCommand(postParams); - const responseApi = await apigwManagementApi.send(postCommand); - console.log("response:", responseApi); //print the response - } catch (err) { - return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; - } - return { statusCode: 200, body: "Data sent" }; - }; \ No newline at end of file + //parameters to post response to the connectionId + const postParams = { + ConnectionId: connectionId, + Data: "good job on deploying this template, keep slaying!!", + }; + + try{ + const postCommand = new PostToConnectionCommand(postParams); + const responseApi = await apigwManagementApi.send(postCommand); + console.log("response:", responseApi); //print the response + } catch (err) { + return { statusCode: 500, body: "Failed to connect: " + JSON.stringify(err) }; + } + + return { statusCode: 200, body: "Data sent" }; +}; \ No newline at end of file diff --git a/apigw-websocket-mapping-template-authorizer/template.yaml b/apigw-websocket-mapping-template-authorizer/template.yaml index c8d7d7f9b..2afa7b0f5 100644 --- a/apigw-websocket-mapping-template-authorizer/template.yaml +++ b/apigw-websocket-mapping-template-authorizer/template.yaml @@ -2,11 +2,6 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: SAM template for a websocket API with a mapping template and a Lambda integration -Globals: - Function: - CodeUri: ./src - Runtime: nodejs22.x - Resources: WebsocketApi: Type: AWS::ApiGatewayV2::Api @@ -146,6 +141,8 @@ Resources: Properties: Handler: authorizer.handler MemorySize: 256 + CodeUri: ./src + Runtime: nodejs22.x LambdaAuthPermission: Type: AWS::Lambda::Permission @@ -156,11 +153,15 @@ Resources: FunctionName: Ref: AuthFunction Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketApi}/*' + OnConnectFunction: Type: AWS::Serverless::Function Properties: Handler: onconnect.handler + CodeUri: ./src + Runtime: nodejs22.x Environment: Variables: TABLE_NAME: !Ref ConnectionsTable @@ -177,11 +178,14 @@ Resources: FunctionName: Ref: OnConnectFunction Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketApi}/*' OnDisconnectFunction: Type: AWS::Serverless::Function Properties: Handler: ondisconnect.handler + CodeUri: ./src + Runtime: nodejs22.x Environment: Variables: TABLE_NAME: !Ref ConnectionsTable @@ -198,6 +202,7 @@ Resources: FunctionName: Ref: OnDisconnectFunction Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketApi}/*' SendMessageFunction: Type: AWS::Serverless::Function @@ -205,6 +210,8 @@ Resources: - WebsocketApi Properties: Handler: sendmessage.handler + CodeUri: ./src + Runtime: nodejs22.x Environment: Variables: TABLE_NAME: !Ref ConnectionsTable @@ -228,6 +235,7 @@ Resources: Action: lambda:InvokeFunction FunctionName: !Ref SendMessageFunction Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebsocketApi}/*' Outputs: WebSocketCommand: From 4caf9247eed1d17edd8e4ed6c087204fd9204534 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Fri, 9 May 2025 13:04:21 +0100 Subject: [PATCH 12/15] added the same delete in README --- apigw-websocket-mapping-template-authorizer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-websocket-mapping-template-authorizer/README.md b/apigw-websocket-mapping-template-authorizer/README.md index f2cd98d09..7e4872e2c 100644 --- a/apigw-websocket-mapping-template-authorizer/README.md +++ b/apigw-websocket-mapping-template-authorizer/README.md @@ -99,7 +99,7 @@ You can then send the Json Payload to the `sendmessage` route: 1. Delete the stack ```bash - sam deploy + sam delete ``` ---- From fa2d1720ffd4f7b57b0ecd98bcbfa7f569486e97 Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Fri, 9 May 2025 15:53:51 +0100 Subject: [PATCH 13/15] modifications to JSON file --- .../example-pattern.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apigw-websocket-mapping-template-authorizer/example-pattern.json b/apigw-websocket-mapping-template-authorizer/example-pattern.json index 44a833546..e1ad9325b 100644 --- a/apigw-websocket-mapping-template-authorizer/example-pattern.json +++ b/apigw-websocket-mapping-template-authorizer/example-pattern.json @@ -1,5 +1,5 @@ { - "title": "Websocket API Gateway with an Authorizer and mapping template", + "title": "AWS Websocket API Gateway with an AWS Lambda Authorizer and mapping template", "description": "Create a Websocket API with a Lambda authorizer and a Lambda in the back-end.", "language": "NodeJs", "level": "200", @@ -7,9 +7,9 @@ "introBox": { "headline": "How it works", "text": [ - "This projects demonstrates how to use a WebSocket API with a Lambda Authorizer", - "The WebSocket API does not have a Proxy integration with the back-end Lambda written NodeJs 22, instead it is using a mapping template that forwards the main information of the request.", - "The Connection ID is stored in a DynamoDB table and the Lambda sends a response to the websocket API through the endpoint URL" + "This projects demonstrates how to use an AWS WebSocket API with an AWS Lambda Authorizer", + "The WebSocket API does not have a Proxy integration with the back-end Lambda Function written NodeJs 22, instead it is using a mapping template that forwards the main information of the request.", + "The Connection ID is stored in an AWS DynamoDB table and the Lambda Function sends a response to the websocket API through the endpoint URL" ] }, "gitHub": { From 41522497dc6705c06db683389210a66dadc892ce Mon Sep 17 00:00:00 2001 From: Alice Goumain Date: Thu, 15 May 2025 16:08:43 +0100 Subject: [PATCH 14/15] added the modifications --- .../apigw-rest-regional-lambda.code-workspace | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apigw-http-api-lambda-python/apigw-rest-regional-lambda.code-workspace diff --git a/apigw-http-api-lambda-python/apigw-rest-regional-lambda.code-workspace b/apigw-http-api-lambda-python/apigw-rest-regional-lambda.code-workspace new file mode 100644 index 000000000..c2e4e8f99 --- /dev/null +++ b/apigw-http-api-lambda-python/apigw-rest-regional-lambda.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../apigw-rest-regional-lambda" + }, + { + "path": ".." + } + ], + "settings": {} +} \ No newline at end of file From ba19357fedb15fa1568f9262e34842bf400c98fc Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 19 May 2025 13:51:04 +0200 Subject: [PATCH 15/15] Create apigw-websocket-mapping-template-authorizer.json added pattern json --- ...websocket-mapping-template-authorizer.json | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 apigw-websocket-mapping-template-authorizer/apigw-websocket-mapping-template-authorizer.json diff --git a/apigw-websocket-mapping-template-authorizer/apigw-websocket-mapping-template-authorizer.json b/apigw-websocket-mapping-template-authorizer/apigw-websocket-mapping-template-authorizer.json new file mode 100644 index 000000000..f1cfa5e05 --- /dev/null +++ b/apigw-websocket-mapping-template-authorizer/apigw-websocket-mapping-template-authorizer.json @@ -0,0 +1,86 @@ +{ + "title": "Websocket API Gateway with an AWS Lambda Authorizer and mapping template", + "description": "Create a Websocket API with a Lambda Authorizer and an AWS Lambda in the back-end.", + "language": "Node.js", + "level": "200", + "framework": "AWS SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This projects demonstrates how to use a WebSocket API with an AWS Lambda Authorizer", + "The WebSocket API does not have a Proxy integration with the back-end Lambda Function written NodeJs 22, instead it is using a mapping template that forwards the main information of the request.", + "The Connection ID is stored in a Amazon DynamoDB table and the Lambda Function sends a response to the websocket API through the endpoint URL" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-websocket-mapping-template-authorizer", + "templateURL": "serverless-patterns/apigw-websocket-mapping-template-authorizer", + "projectFolder": "apigw-websocket-mapping-template-authorizer", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "Invoke a Webscoket API with wscat", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-wscat.html" + }, + { + "text": "Integration request in Websocket API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html" + }, + { + "text": "Control access to WebSocket APIs with AWS Lambda REQUEST authorizers", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html" + } + ] + }, + "deploy": { + "text": ["sam deploy"] + }, + "testing": { + "text": ["See the GitHub repo for detailed testing instructions."] + }, + "cleanup": { + "text": ["Delete the stack: sam delete."] + }, + "authors": [ + { + "name": "Alice Goumain", + "image": "https://media.licdn.com/dms/image/v2/C4E03AQFu1xnGt76xzg/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1662636937225?e=2147483647&v=beta&t=f4IFRXMLweHD9WieD3X1D3YkZO3Hf-bdTXHAfYcFpbo", + "bio": "Cloud Support Engineer in Serverless @ AWS", + "linkedin": "alice-goumain/" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "apigw", + "label": "API Gateway WebSocket" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "dynamodb", + "label": "Amazon DynamoDB" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "" + }, + "line2": { + "from": "icon2", + "to": "icon3", + "label": "" + } + } +}