I have an ApiGateway RestApi with a custom authoriser in AWS CDK v2. Now I want to create a WebSocket with an authoriser.
I started by following this guide Stack 3: Api Gateway Websocket API AWS CDK Stack Walk-thru, which has got me as far as creating the ApiGatewayV2 WebSocket. I'm struggling to figure out how to create a custom authoriser for that.
Some of the questions I have:
- Can I use the same authoriser function with ApiGatewayV2 CfnAuthoriser?
- Does the authorisation need to happen in some kind of websocket style?
- How do I authorise with the WebSocket from the front-end application? Is it just an authentication header like in an HTTP request?
I'm having a hard time googling it, keep getting CDK v1 articles. If anyone has some time to point me in the right direction I'd really appreciate it.
Main stack
export class ThingCdkStack extends Stack {
private authoriserLogicalId: string;
constructor(scope: Construct, id: string, props: StackProps, private envs: Environment) {
super(scope, id, props);
const api = new RestApi(this, 'ThingApi');
const role = new Role(this, 'ThingRole', {
roleName: 'thing-role',
assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
inlinePolicies: {
allowLambdaInvocation: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['lambda:InvokeFunction', 'lambda:InvokeAsync'],
Resource: `arn:aws:lambda:${envs.REGION}:${envs.ACCOUNT}:function:*`,
},
],
}),
},
});
const authorizerHandler = new NodejsFunction(this, 'ThingCustomAuthorizer', {
entry: 'lambda/handlers/auth/auth0-authoriser.ts',
runtime: Runtime.NODEJS_18_X,
environment: {
AUTH0_ISSUER: 'https://my-auth.eu.auth0.com/',
AUTH0_AUDIENCE: 'https://my-demo.com',
REGION: envs.REGION,
ACCOUNT: envs.ACCOUNT,
}
});
const authorizer = new CfnAuthorizer(this, 'ThingAuthoriser', {
restApiId: api.restApiId,
type: 'TOKEN',
name: 'thing-authoriser',
identitySource: 'method.request.header.Authorization',
authorizerUri: `arn:aws:apigateway:${envs.REGION}:lambda:path/2015-03-31/functions/${authorizerHandler.functionArn}/invocations`,
authorizerCredentials: role.roleArn
});
this.authoriserLogicalId = authorizer.logicalId;
const createThingHandler = new NodejsFunction(this, 'CreateThingLambda', {
entry: 'lambda/handlers/thing/create-thing.ts',
runtime: Runtime.NODEJS_18_X,
});
this.addAuthMethod('post', api.addResource('thing'), createThingHandler);
this.addWebsocket(envs, authorizer);
}
private addAuthMethod(method: string, resource: Resource, handler: NodejsFunction, integrationOptions?: LambdaIntegrationOptions) {
const route = resource.addMethod(
method,
new LambdaIntegration(handler, integrationOptions),
{
authorizationType: AuthorizationType.CUSTOM,
}
);
const childResource = route.node.findChild('Resource');
(childResource as CfnResource).addPropertyOverride('AuthorizationType', AuthorizationType.CUSTOM);
(childResource as CfnResource).addPropertyOverride('AuthorizerId', {Ref: this.authoriserLogicalId});
}
private addWebsocket(environment: Environment, authorizer: CfnAuthorizer) {
const connectionsTable = new Table(this, 'ConnectionsTable', {
partitionKey: {name: 'connectionId', type: AttributeType.STRING},
readCapacity: 2,
writeCapacity: 1,
timeToLiveAttribute: "ttl"
});
const commonHandlerProps: NodejsFunctionProps = {
bundling: {minify: true, sourceMap: true, target: 'es2019'},
runtime: Runtime.NODEJS_18_X,
logRetention: RetentionDays.THREE_DAYS
};
const connectHandler = new NodejsFunction(this, 'ConnectHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/connect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const defaultHandler = new NodejsFunction(this, 'defaultHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/default.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const disconnectHandler = new NodejsFunction(this, 'DisconnectHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/disconnect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const websocketApi = new WebsocketApi(this, "CompletionWebsocketApi", {
apiName: "completions-api",
apiDescription: "Web Socket API for Completions",
stageName: environment.STAGE,
connectHandler,
disconnectHandler,
defaultHandler,
connectionsTable
});
const CONNECTION_URL = `https://${websocketApi.api.ref}.execute-api.${Aws.REGION}.amazonaws.com/${environment.STAGE}`;
const completionHandler = new NodejsFunction(this, 'CompletionHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/completions.ts',
environment: {
CONNECTION_TBL: connectionsTable.tableName,
CONNECTION_URL: CONNECTION_URL
},
});
websocketApi.addLambdaIntegration(completionHandler, 'completions', 'CompletionsRoute')
const managementApiPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ["execute-api:ManageConnections"],
resources: [`arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${websocketApi.api.ref}/*`]
})
defaultHandler.addToRolePolicy(managementApiPolicyStatement);
completionHandler.addToRolePolicy(managementApiPolicyStatement);
new CfnOutput(this, 'WebsocketConnectionUrl', {value: CONNECTION_URL});
const websocketApiUrl = `${websocketApi.api.attrApiEndpoint}/${environment.STAGE}`
new CfnOutput(this, "websocketUrl", {
value: websocketApiUrl
});
}
}
WebsocketApi construct
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { IFunction } from "aws-cdk-lib/aws-lambda";
import { CfnApi, CfnIntegration, CfnRoute, CfnStage, CfnDeployment } from "aws-cdk-lib/aws-apigatewayv2";
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Aws, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface WebsocketApiProps {
readonly apiName: string;
readonly apiDescription: string;
readonly stageName: string;
readonly connectHandler: IFunction;
readonly disconnectHandler: IFunction;
readonly connectionsTable: ITable;
readonly defaultHandler?: IFunction;
}
export class WebsocketApi extends Construct {
readonly props: WebsocketApiProps;
readonly api: CfnApi;
readonly deployment: CfnDeployment;
constructor(parent: Stack, name: string, props: WebsocketApiProps) {
super(parent, name);
this.props = props;
this.api = new CfnApi(this, 'CompletionsWebSocketApi', {
name: props.apiName,
description: props.apiDescription,
protocolType: "WEBSOCKET",
routeSelectionExpression: "$request.body.action",
});
this.deployment = new CfnDeployment(this, "WebsocketDeployment", {
apiId: this.api.ref,
});
new CfnStage(this, "WebsocketStage", {
stageName: props.stageName,
apiId: this.api.ref,
deploymentId: this.deployment.ref,
});
props.connectionsTable.grantWriteData(props.connectHandler);
props.connectionsTable.grantWriteData(props.disconnectHandler);
this.addLambdaIntegration(props.connectHandler, "$connect", "ConnectionRoute");
this.addLambdaIntegration(props.disconnectHandler, "$disconnect", "DisconnectRoute");
if(props.defaultHandler) {
props.connectionsTable.grantWriteData(props.defaultHandler);
this.addLambdaIntegration(props.defaultHandler, "$default", "DefaultRoute");
}
}
addLambdaIntegration(handler: IFunction, routeKey: string, operationName: string, apiKeyRequired?: boolean, authorizationType?: string) {
const integration = new CfnIntegration(this, `${operationName}Integration`, {
apiId: this.api.ref,
integrationType: "AWS_PROXY",
integrationUri: `arn:aws:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`
});
handler.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com', {
conditions: {
"ArnLike": {
"aws:SourceArn": `arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${this.api.ref}/*/*`
}
}
}));
this.deployment.addDependency(new CfnRoute(this, `${operationName}Route`, {
apiId: this.api.ref,
routeKey,
apiKeyRequired,
authorizationType: authorizationType || "NONE",
operationName,
target: `integrations/${integration.ref}`
}));
}
}
A full working stack of what I have so far can be found here: https://github.com/OrderAndCh4oS/aws-cdk-v2-apigatewayv2-websocket-demo
Update
I've found some useful documentation that I'm working now through:
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html
Have also found a sample project using Cognito: https://github.com/aws-samples/websocket-api-cognito-auth-sample