This a continuation of Passwordless Phone Number Authentication using AWS Amplify & Cognito, where we added authentication to a serverless application.

In this article, we will add authenticated WebSocket connections in our serverless application.

Overview: How will it work?

First, we will create an API Gateway with the WebSocket protocol. Then we can open a connection to it and API Gateway will keep the connection alive for us.

While opening the connection we will send the user’s auth token (received on login from AWS Cognito) as a query parameter to authenticate the WebSocket connection. Once authenticated, we will map the WebSocket connection ID to User ID and store it in DynamoDB.

If some message needs to be pushed to a user (say a user sent a message to another user), we will look up DynamoDB for the recipient user ID to retrieve all their open WebSocket connections. Then we will push the data to each of those connections using API Gateway’s PostToConnection method.

Now let’s jump into the actual implementation.

API Gateway

Create a new API Gateway from the AWS Management Console. Select “WebSocket” as the protocol. Give a name for the WebSocket API.

Route Selection Expression

We may want our API to behave differently based on the type of the received message. For example, let us assume we have two types of messages:

Message Type 1:

{

type: "ping",

payload: { }

} Message Type 2:

{

type: "message",

payload: { }

}

These messages will by default be available in $request.body . Now to tell API Gateway to evaluate a route based on the type field in our message, we will simply set the Route Selection Expression to $request.body.type .

You can read more about Route Selection Expression on the official documentation here.

DynamoDB Table to Store ConnectionID

We will create the table with id (string) as primary key and connectionId (string) as the sort key.

IAM Role for Lambda

The lambda needs access to DynamoDB and API Gateway that we just created. We will create a separate policy for each service that our lambda needs to access.

DynamoDB Policy

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "VisualEditor0",

"Effect": "Allow",

"Action": [

"dynamodb:DescribeReservedCapacityOfferings",

"dynamodb:ListGlobalTables",

"dynamodb:ListTables",

"dynamodb:DescribeReservedCapacity",

"dynamodb:ListBackups",

"dynamodb:PurchaseReservedCapacityOfferings",

"dynamodb:DescribeLimits",

"dynamodb:ListStreams"

],

"Resource": "*"

},

{

"Sid": "VisualEditor1",

"Effect": "Allow",

"Action": "dynamodb:*",

"Resource": "arn:aws:dynamodb:us-east-1:119384653119:table/go-ws-demo-sockets"

}

]

}

API Gateway Policy

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "VisualEditor0",

"Effect": "Allow",

"Action": "apigateway:*",

"Resource": "arn:aws:apigateway:us-east-1::ybnvxa5lqf"

}

]

}

Finally, we will create the IAM Role for the Lambda and attach the policies that we just created.

The Lambda Function

We will create a lambda function with Go 1.x as runtime and choose the IAM role that we just created as the execution role.

Struct for Proxy Request

At the time of writing, aws-lambda-go package didn’t have a struct for the WebSocket Proxy Request. So, I printed the entire object as JSON and used it as a reference to create the struct apiGatewayWebsocketProxyRequest .

Routing based on Route Key

Since we are going to use the same lambda function for all the API Gateway routes, we will handle the routing inside the lambda. So, we will first read the route key from the request context and call methods based on the route key.

Authorizer

This function is responsible to verify the JWT sent by the client, which can be retrieved from the query parameters.

The public keys are made available at an address in this format:

https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json

We will use go-jwk library to verify the JWT. Once verified and the claims are extracted, we will return a APIGatewayCustomAuthorizerResponse .

Read more about decoding and verifying the JWT token on the official documentation here.

Connect

Since we are going to add a custom authorizer to the $connect route, the lambda will be called twice with $connect route key — pre-authentication and post-authentication.

On the first call, request.RequestContext.Authorizer will be nil . In this case, we will simply forward the request to our authorizer function.

On the second call, request.RequestContext.Authorizer will contain a map with the user information. Once the authentication is successful, we will store the connectionId against the userId in DynamoDB. Finally, we will return a ApiGatewayProxyResponse with the StatusCode as 200.

Message

When a message is sent, it will contain the recipient userId. We will retrieve all the open connections of the recipient userId from DynamoDB. Finally, we will send the message to all the connections using PostToConnection method.

Disconnect

On disconnect, we will remove the connectionId from the DynamoDB and return a 200 response.

Revisiting API Gateway

We skipped some configuration that needed the lambda. Now that we have created the lambda lets revisit and finish the configuration.

Create an Authorizer

Navigate to “Authorizers” and click “Create New Authorizer”. Provide a name and choose the lambda function. For identity source, select “Query String” and parameter name as “token”.

Create Routes

$connect and disconnect are predefined route keys. Click on the routes to open “Route Overview” on the right pane. Enable “Use Lambda Proxy Integration” and select the Lambda function.

Now in “Route Overview”, click “Route Request”. Click our lambda as the authorizer.

Deploy the API

Finally, deploy the API to get the WebSocket URL. This is the URL to which you should open a connection from your client application.

That being said on the client-side, I manage the WebSocket connections using RxJS and Redux-Observables to connect, auto-reconnect and disconnect.

That’s all folk. If you are interested in seeing all of this in action, there is a serverless messenger application I built some time ago. Hope it was helpful :)