diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index cc6c6f48c5827..16f6bc06ca309 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -423,3 +423,17 @@ stage.grantManageConnections(lambda); // for all the stages permission webSocketApi.grantManageConnections(lambda); ``` + +### API Keys + +Websocket APIs also support usage of API Keys. An API Key is a key that is used to grant access to an API. These are useful for controlling and tracking access to an API, when used together with [usage plans](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html). These together allow you to configure controls around API access such as quotas and throttling, along with per-API Key metrics on usage. + +To require an API Key when accessing the Websocket API: + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi',{ + apiKeySelectionExpression: ApiKeySelectionExpression.X_API_KEY, + }); +... +``` + diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts index fdcfbdbce6d30..9240260141cbe 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -18,6 +18,24 @@ export interface IWebSocketApi extends IApi { _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration } +/** + * Represents the currently available API Key Selection Expressions + */ +export enum ApiKeySelectionExpression { + /** + * x-api-key type + * This represents an API Key that is provided via an `x-api-key` header in the user request. + */ + X_API_KEY = '$request.header.x-api-key', + + /** + * usageIdentifierKey type + * This represents an API Key that is provided via the context of an Lambda Authorizer + * See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html + */ + USAGE_IDENTIFIER_KEY = '$context.authorizer.usageIdentifierKey' +} + /** * Props for WebSocket API */ @@ -28,6 +46,13 @@ export interface WebSocketApiProps { */ readonly apiName?: string; + /** + * An API key selection expression. Providing this option will require an API Key be provided to access the API. + * Currently only supports '$request.header.x-api-key' and '$context.authorizer.usageIdentifierKey' + * @default - none + */ + readonly apiKeySelectionExpression?: ApiKeySelectionExpression + /** * The description of the API. * @default - none @@ -82,6 +107,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { const resource = new CfnApi(this, 'Resource', { name: this.webSocketApiName, + apiKeySelectionExpression: props?.apiKeySelectionExpression, protocolType: 'WEBSOCKET', description: props?.description, routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts index 0588889a603bc..ebaf874963268 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -45,6 +45,12 @@ export interface WebSocketRouteProps extends WebSocketRouteOptions { * The key to this route. */ readonly routeKey: string; + + /** + * Whether the route requires an API Key to be provided + * @default - false + */ + readonly apiKeyRequired?: boolean; } /** @@ -76,6 +82,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { const route = new CfnRoute(this, 'Resource', { apiId: props.webSocketApi.apiId, + apiKeyRequired: props.apiKeyRequired, routeKey: props.routeKey, target: `integrations/${integration.integrationId}`, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts index 24337a3f7c3f2..33b37e5b5bd1e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -2,6 +2,7 @@ import { Match, Template } from '@aws-cdk/assertions'; import { User } from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { + ApiKeySelectionExpression, IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, } from '../../lib'; @@ -25,6 +26,27 @@ describe('WebSocketApi', () => { Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); }); + test('apiKeySelectionExpression: given a value', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api', { + apiKeySelectionExpression: ApiKeySelectionExpression.USAGE_IDENTIFIER_KEY, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Api', { + ApiKeySelectionExpression: '$context.authorizer.usageIdentifierKey', + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Stage', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Route', 0); + Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0); + }); + test('addRoute: adds a route with passed key', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json new file mode 100644 index 0000000000000..bc0b6f740acc8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.expected.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "MyWebsocketApiEBAC53DF": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "ApiKeySelectionExpression": "$request.header.x-api-key", + "Name": "MyWebsocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts new file mode 100644 index 0000000000000..fe6205fb6c79c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-apikey.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; +import { ApiKeySelectionExpression } from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets'); + +new apigw.WebSocketApi(stack, 'MyWebsocketApi', { + apiKeySelectionExpression: ApiKeySelectionExpression.X_API_KEY, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json new file mode 100644 index 0000000000000..bda5b8ee08af4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.expected.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "MyWebsocketApiEBAC53DF": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyWebsocketApi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts new file mode 100644 index 0000000000000..e9acb82402cfa --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/integ.api-default.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from '@aws-cdk/core'; +import * as apigw from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets'); + +new apigw.WebSocketApi(stack, 'MyWebsocketApi'); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts index 07eadc5300a85..63e82bc579552 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -41,6 +41,44 @@ describe('WebSocketRoute', () => { IntegrationUri: 'some-uri', }); }); + + test('Api Key is required for route when apiKeyIsRequired is true', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + apiKeyRequired: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.apiId), + ApiKeyRequired: true, + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'RouteWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f20338431E', + }, + ], + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.apiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); });