Using the V2 Golang SDK for AWS and trying to call PostToConnection
from a Lambda I was consistently getting this 403 error result:
operation error ApiGatewayManagementApi: PostToConnection, https response error StatusCode: 403, RequestID: 965ae9ea-c537-43d0-ae5c-3a458ccbc069, ForbiddenException
I was trying to make the call from a different stack than the websocket connection was on so I thought that might be the problem. After much research the issue actually turned out to be simpler than that. There is nothing special about the stack that requires extra permission (unless the stages are in different regions, I haven’t done that yet but it looks like there is more work required if you are). The issue for me is that I had the endpoint wrong for the PostToConnection
call. Calling the AWS websocket API requires that you know the right endpoint for the socket and that the AWS client uses that endpoint. There are two parts to the endpoint, the domain
and the stage
. The client needs to be configured properly so that it resolves to that endpoint. Here’s an example of how to do that. Note that this client only resolves properly for the API Gateway websocket endpoint so it shouldn’t be used as a general purpose AWS client.
// newAPIGatewayManagementClient creates a new API Gateway Management Client instance from the provided parameters. The
// new client will have a custom endpoint that resolves to the application's deployed API.
func newAPIGatewayManagementClient(cfg *aws.Config, endpointHost, endpointStage string) *apigatewaymanagementapi.Client {
cp := cfg.Copy()
cp.EndpointResolver = aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
var endpoint url.URL
endpoint.Path = endpointStage
endpoint.Host = endpointHost
endpoint.Scheme = "https"
return aws.Endpoint{
SigningRegion: region,
URL: endpoint.String(),
}, nil
})
return apigatewaymanagementapi.NewFromConfig(cp)
}
The two interesting arguments to this function are endpointHost
and endpointStage
. These values should be captured when the websocket connection handler is first called to open a new socket. Here’s example code that captures these values. This function is attached to the websocket connection handler.
// invoked from API Gateway when a WebSocket connection is opened
func handler(ctx context.Context, request events.APIGatewayWebsocketProxyRequest) (events.APIGatewayProxyResponse, error) {
err := initHost(ctx, request.RequestContext.DomainName, request.RequestContext.Stage)
if nil != err {
return handleError(ctx, err), nil
}
err = recordConnection(ctx, request.RequestContext.ConnectionID)
if nil != err {
return handleError(ctx, err), nil
}
return successResponse(ctx), nil
}
func initHost( ctx context.Context, endpointHost, endpointStage string ) error {
// store the endpointHost and endpointStage for later use
}
func recordConnection( ctx context.Context, connectionID string ) error {
// store the connectionID for later use
}
There are two interesting functions in this code initHost
and recordConnection
. I split them into different functions because the scope of the information is different. The endpoint information is the same across all websocket calls while the connectionID
is unique for each websocket. I only store the endpoint host and stage the first time a connection is opened. The connectionID
has to be stored for each connection and connected to the user account that opened the connection so that functions that want to message that user can look up their current connection. The details of how you want to store that information will vary depending on your app implementation. I store the information in DynamoDB, the endpoint host and stage in a single entry with a static key to make it easy to find from anywhere, while each connectionID
is associated with a particular user identity in the DB.
The 403 error was caused because I was using the stage of the stack that was sending the message instead of the stage of the stack where the websocket connection was created. In particular the websocket handler was in a lambda called from a stage named dev
while the message was originating from a lambda in a different stack with a stage named $default
. When I switched the endpoint resolver to use the endpointHost
and endpointStage
that were stored when the connection was created the 403 went away.
As far as I can tell the only IAM permission required is that the connection handler have a Lambda websocket authorizer and that the policy returned by that authorizer provides permission to any stacks that are required. There are other ways to provide permissions including using IAM role based permissions but I’m currently using the Lambda authorizer.