diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/README.md b/jwt_authorizer_websocket_api_lambda_authorizer/README.md new file mode 100644 index 000000000..5c3d17430 --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/README.md @@ -0,0 +1,81 @@ +# Serverless JWT Authorizer for Websocket Amazon API Gateway with AWS Lambda Authorizer +![Concept](./images/flow.png) + +This serverless pattern demonstrates how to authenticate and authorize users via a Lambda authorizer for an API Gateway WebSocket API using Amazon Cognito user pool tokens. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/jwt_authorizer_websocket_api_lambda_authorizer + +Important: this ready-to-use 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) +* [SAM Installed](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) +* [Wscat Installed](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-wscat.html) +* [Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) + +## 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-pattern + ``` +1. Change directory to the pattern directory: + ``` + cd jwt_authorizer_websocket_api_lambda_authorizer + ``` +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 the Stack Name + - Enter a value: + + Enter the region in which you want to deploy the stack + - Enter a value: *{enter the region for deployment}* + + Enter User Pool Id + - Enter a value: + + Enter Client Id: + - Enter a value: *{Client id of the app created in user pool}* + + +1. Note the outputs from the deployment process. It contain the full url link which can be used for testing. + +## Testing +1. Run the below commands to get the Authorization token for your user pool. +``` +echo -n "[username][app client ID]" | openssl dgst -sha256 -hmac [app client secret] -binary | openssl enc -base64 + +aws cognito-idp initiate-auth --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME=,PASSWORD=,SECRET_HASH= --client-id +``` + +1. Copy the Access Token and save in a notepad + +1. Run the following websocket command to invoke the websocket Rest API with the retrieved access token: +``` +wscat -c wss://.execute-api..amazonaws.com/prod/ -H Authorization:Xyz +``` + +2. Observe the output of the above command, if it shows connected then you are able to successfully connected to the websocket API using JWT token. + +## Cleanup + +1. Change directory to the pattern directory: + ``` + cd serverless-patterns/jwt_authorizer_websocket_api_lambda_authorizer + ``` +1. Delete all created resources in same account. + ``` + sam delete --stack-name + ``` + +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/images/flow.png b/jwt_authorizer_websocket_api_lambda_authorizer/images/flow.png new file mode 100644 index 000000000..80b7b917d Binary files /dev/null and b/jwt_authorizer_websocket_api_lambda_authorizer/images/flow.png differ diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/src/authLambda/index.mjs b/jwt_authorizer_websocket_api_lambda_authorizer/src/authLambda/index.mjs new file mode 100644 index 000000000..9c269d948 --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/src/authLambda/index.mjs @@ -0,0 +1,58 @@ +import { CognitoJwtVerifier } from "aws-jwt-verify"; + +let userPoolId = process.env.userPoolId; +let clientId = process.env.clientId; + +// Verifier that expects valid access tokens: +const verifier = CognitoJwtVerifier.create({ + userPoolId: userPoolId, + tokenUse: "access", + clientId: clientId, +}); + +function generatePolicy(principalId, effect, resource) { + const authResponse = { + principalId, + }; + + if (effect && resource) { + const policyDocument = { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: effect, + Resource: resource, + }, + ], + }; + authResponse.policyDocument = policyDocument; + } + + return JSON.stringify(authResponse); +} + +function generateAllow(principalId, resource) { + return generatePolicy(principalId, 'Allow', resource); +} + +function generateDeny(principalId, resource) { + return generatePolicy(principalId, 'Deny', resource); +} + +export const handler = async (event) => { + // TODO implement + let Authtoken = event.headers.Authorization + try { + const payload = await verifier.verify( + Authtoken + ); + console.log("Token is valid. Payload:", payload); + const response = generateAllow('me', event.methodArn); + return JSON.parse(response); + } catch { + console.log('unauthorized'); + const response = generateDeny('me', event.methodArn); + return JSON.parse(response); + } +}; diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/src/jwt-layer.zip b/jwt_authorizer_websocket_api_lambda_authorizer/src/jwt-layer.zip new file mode 100644 index 000000000..0abe394bf Binary files /dev/null and b/jwt_authorizer_websocket_api_lambda_authorizer/src/jwt-layer.zip differ diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/src/onConnect/index.py b/jwt_authorizer_websocket_api_lambda_authorizer/src/onConnect/index.py new file mode 100644 index 000000000..6e8781402 --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/src/onConnect/index.py @@ -0,0 +1,5 @@ +import json + +def lambda_handler(event, context): + print(event) + return {"statusCode": 200, "body":"hello!!"} \ No newline at end of file diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/src/onDisconnect/index.py b/jwt_authorizer_websocket_api_lambda_authorizer/src/onDisconnect/index.py new file mode 100644 index 000000000..96a6b292f --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/src/onDisconnect/index.py @@ -0,0 +1,5 @@ +import json + +def lambda_handler(event, context): + print(event) + return {"statusCode": 200, "body":"Disconnecting, bye!"} \ No newline at end of file diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/src/sendMessage/index.py b/jwt_authorizer_websocket_api_lambda_authorizer/src/sendMessage/index.py new file mode 100644 index 000000000..381cfd4d0 --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/src/sendMessage/index.py @@ -0,0 +1,5 @@ +import json + +def lambda_handler(event, context): + print(event) + return {"statusCode": 200, "body":"how are you doing?"} \ No newline at end of file diff --git a/jwt_authorizer_websocket_api_lambda_authorizer/template.yaml b/jwt_authorizer_websocket_api_lambda_authorizer/template.yaml new file mode 100644 index 000000000..982187e4c --- /dev/null +++ b/jwt_authorizer_websocket_api_lambda_authorizer/template.yaml @@ -0,0 +1,259 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' + +Parameters: + UserPoolId: + Type: String + Description: User Pool Id + + ClientId: + Type: String + NoEcho: true + + +Resources: + SimpleChatWebSocket: + Type: AWS::ApiGatewayV2::Api + DependsOn: OnConnectFunction + Properties: + Name: !Sub ${AWS::StackName}-SimpleChatWebSocket + ProtocolType: WEBSOCKET + RouteSelectionExpression: "$request.body.action" + ApiKeySelectionExpression: $request.header.x-api-key + + LambdaAuthorizerAPIGW: + Type: AWS::ApiGatewayV2::Authorizer + Properties: + ApiId: !Ref SimpleChatWebSocket + AuthorizerType: REQUEST + AuthorizerUri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizerLambdaFunction.Arn}/invocations' + IdentitySource: + - route.request.header.Authorization + Name: LambdaAuthorizer + + + ConnectRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: !Ref SimpleChatWebSocket + RouteKey: $connect + AuthorizationType: CUSTOM + AuthorizerId: !Ref LambdaAuthorizerAPIGW + OperationName: ConnectRoute + Target: !Join + - "/" + - - "integrations" + - !Ref ConnectInteg + ConnectInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: !Ref SimpleChatWebSocket + Description: Connect Integration + IntegrationType: AWS_PROXY + IntegrationUri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnConnectFunction.Arn}/invocations' + + DisconnectRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: !Ref SimpleChatWebSocket + RouteKey: $disconnect + AuthorizationType: NONE + OperationName: DisconnectRoute + Target: !Join + - "/" + - - "integrations" + - !Ref DisconnectInteg + + DisconnectInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: !Ref SimpleChatWebSocket + Description: Disconnect Integration + IntegrationType: AWS_PROXY + IntegrationUri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OnDisconnectFunction.Arn}/invocations' + + SendRoute: + Type: AWS::ApiGatewayV2::Route + Properties: + ApiId: !Ref SimpleChatWebSocket + RouteKey: sendmessage + AuthorizationType: NONE + OperationName: SendRoute + Target: !Join + - "/" + - - "integrations" + - !Ref SendInteg + SendInteg: + Type: AWS::ApiGatewayV2::Integration + Properties: + ApiId: !Ref SimpleChatWebSocket + Description: Send Integration + IntegrationType: AWS_PROXY + IntegrationUri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SendMessageFunction.Arn}/invocations' + + Deployment: + Type: AWS::ApiGatewayV2::Deployment + DependsOn: + - ConnectRoute + - SendRoute + - DisconnectRoute + Properties: + ApiId: !Ref SimpleChatWebSocket + + Stage: + Type: AWS::ApiGatewayV2::Stage + Properties: + StageName: prod + Description: Prod Stage + DeploymentId: !Ref Deployment + ApiId: !Ref SimpleChatWebSocket + + APIGatewayLambdaInvokePermission5: + Type: AWS::Lambda::Permission + DependsOn: SimpleChatWebSocket + Properties: + FunctionName: !Ref OnConnectFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SimpleChatWebSocket}/*' + + APIGatewayLambdaInvokePermission6: + Type: AWS::Lambda::Permission + DependsOn: SimpleChatWebSocket + Properties: + FunctionName: !Ref OnDisconnectFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SimpleChatWebSocket}/*' + + APIGatewayLambdaInvokePermission7: + Type: AWS::Lambda::Permission + DependsOn: SimpleChatWebSocket + Properties: + FunctionName: !Ref SendMessageFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SimpleChatWebSocket}/*' + + APIGatewayLambdaInvokePermission8: + Type: AWS::Lambda::Permission + DependsOn: SimpleChatWebSocket + Properties: + FunctionName: !Ref AuthorizerLambdaFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SimpleChatWebSocket}/*' + + OnConnectFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/onConnect + Description: AWS Lambda function + MemorySize: 256 + Timeout: 5 + Handler: index.lambda_handler + Runtime: python3.12 + Architectures: + - x86_64 + EphemeralStorage: + Size: 512 + PackageType: Zip + Policies: + - Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + + OnDisconnectFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/onDisconnect + Description: AWS Lambda function + MemorySize: 256 + Timeout: 5 + Handler: index.lambda_handler + Runtime: python3.12 + Architectures: + - x86_64 + EphemeralStorage: + Size: 512 + PackageType: Zip + Policies: + - Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + + SendMessageFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/sendMessage + Description: AWS Lambda function + MemorySize: 256 + Timeout: 5 + Handler: index.lambda_handler + Runtime: python3.12 + Architectures: + - x86_64 + EphemeralStorage: + Size: 512 + PackageType: Zip + Policies: + - Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + + AuthorizerLambdaFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/authLambda + Description: AWS Lambda function + MemorySize: 256 + Timeout: 5 + Handler: index.handler + Runtime: nodejs20.x + Architectures: + - x86_64 + EphemeralStorage: + Size: 512 + Environment: + Variables: + clientId: !Ref ClientId + userPoolId: !Ref UserPoolId + Layers: + - !Ref MyLambdaLayer + PackageType: Zip + Policies: + - Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + + MyLambdaLayer: + Type: AWS::Serverless::LayerVersion + Properties: + LayerName: jwt-cognito-layer + Description: My jwt cognito Layer + ContentUri: src/jwt-layer.zip + CompatibleRuntimes: + - nodejs20.x + +Outputs: + ApiGwUrl: + Description: URL to call the websocket API, replace the xyz with cognito token. + Value: !Sub + - "wscat -c wss://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/prod -H Authorization:xyz" + - ApiId: !Ref SimpleChatWebSocket \ No newline at end of file