Skip to content

Commit 24f8f74

Browse files
feat(apigatewayv2): websocket api: api keys (#16636)
---- This PR adds support for requiring an API Key on Websocket API routes. Specifically, it does the following: * Exposes `apiKeyRequired` on route object (defaults to false) * Exposes `apiKeySelectionExpression` on api object In addition, the following has been added: * Logic to ensure `apiKeySelectionExpression` falls within the two currently supported values * Created a few basic integration tests for the api and route objects for websockets *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent beb5706 commit 24f8f74

File tree

7 files changed

+144
-2
lines changed

7 files changed

+144
-2
lines changed

packages/@aws-cdk/aws-apigatewayv2/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,17 @@ webSocketApi.grantManageConnections(lambda);
426426
API Gateway supports multiple mechanisms for [controlling and managing access to a WebSocket API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-control-access.html) through authorizers.
427427

428428
These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library.
429+
430+
### API Keys
431+
432+
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.
433+
434+
To require an API Key when accessing the Websocket API:
435+
436+
```ts
437+
const webSocketApi = new WebSocketApi(stack, 'mywsapi',{
438+
apiKeySelectionExpression: WebSocketApiKeySelectionExpression.HEADER_X_API_KEY,
439+
});
440+
...
441+
```
442+

packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts

+30
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ import { WebSocketRoute, WebSocketRouteOptions } from './route';
1212
export interface IWebSocketApi extends IApi {
1313
}
1414

15+
/**
16+
* Represents the currently available API Key Selection Expressions
17+
*/
18+
export class WebSocketApiKeySelectionExpression {
19+
20+
/**
21+
* The API will extract the key value from the `x-api-key` header in the user request.
22+
*/
23+
public static readonly HEADER_X_API_KEY = new WebSocketApiKeySelectionExpression('$request.header.x-api-key');
24+
25+
/**
26+
* The API will extract the key value from the `usageIdentifierKey` attribute in the `context` map,
27+
* returned by the Lambda Authorizer.
28+
* See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
29+
*/
30+
public static readonly AUTHORIZER_USAGE_IDENTIFIER_KEY = new WebSocketApiKeySelectionExpression('$context.authorizer.usageIdentifierKey');
31+
32+
/**
33+
* @param customApiKeySelector The expression used by API Gateway
34+
*/
35+
public constructor(public readonly customApiKeySelector: string) {}
36+
}
37+
1538
/**
1639
* Props for WebSocket API
1740
*/
@@ -22,6 +45,12 @@ export interface WebSocketApiProps {
2245
*/
2346
readonly apiName?: string;
2447

48+
/**
49+
* An API key selection expression. Providing this option will require an API Key be provided to access the API.
50+
* @default - Key is not required to access these APIs
51+
*/
52+
readonly apiKeySelectionExpression?: WebSocketApiKeySelectionExpression
53+
2554
/**
2655
* The description of the API.
2756
* @default - none
@@ -76,6 +105,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {
76105

77106
const resource = new CfnApi(this, 'Resource', {
78107
name: this.webSocketApiName,
108+
apiKeySelectionExpression: props?.apiKeySelectionExpression?.customApiKeySelector,
79109
protocolType: 'WEBSOCKET',
80110
description: props?.description,
81111
routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action',

packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export interface WebSocketRouteProps extends WebSocketRouteOptions {
5252
* The key to this route.
5353
*/
5454
readonly routeKey: string;
55+
56+
/**
57+
* Whether the route requires an API Key to be provided
58+
* @default false
59+
*/
60+
readonly apiKeyRequired?: boolean;
5561
}
5662

5763
/**
@@ -91,6 +97,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute {
9197

9298
const route = new CfnRoute(this, 'Resource', {
9399
apiId: props.webSocketApi.apiId,
100+
apiKeyRequired: props.apiKeyRequired,
94101
routeKey: props.routeKey,
95102
target: `integrations/${config.integrationId}`,
96103
authorizerId: authBindResult.authorizerId,

packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { Match, Template } from '@aws-cdk/assertions';
22
import { User } from '@aws-cdk/aws-iam';
33
import { Stack } from '@aws-cdk/core';
44
import {
5-
WebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType,
6-
WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig,
5+
WebSocketRouteIntegration,
6+
WebSocketApi,
7+
WebSocketApiKeySelectionExpression,
8+
WebSocketIntegrationType,
9+
WebSocketRouteIntegrationBindOptions,
10+
WebSocketRouteIntegrationConfig,
711
} from '../../lib';
812

913
describe('WebSocketApi', () => {
@@ -25,6 +29,27 @@ describe('WebSocketApi', () => {
2529
Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0);
2630
});
2731

32+
test('apiKeySelectionExpression: given a value', () => {
33+
// GIVEN
34+
const stack = new Stack();
35+
36+
// WHEN
37+
new WebSocketApi(stack, 'api', {
38+
apiKeySelectionExpression: WebSocketApiKeySelectionExpression.AUTHORIZER_USAGE_IDENTIFIER_KEY,
39+
});
40+
41+
// THEN
42+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Api', {
43+
ApiKeySelectionExpression: '$context.authorizer.usageIdentifierKey',
44+
Name: 'api',
45+
ProtocolType: 'WEBSOCKET',
46+
});
47+
48+
Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Stage', 0);
49+
Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Route', 0);
50+
Template.fromStack(stack).resourceCountIs('AWS::ApiGatewayV2::Integration', 0);
51+
});
52+
2853
test('addRoute: adds a route with passed key', () => {
2954
// GIVEN
3055
const stack = new Stack();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"Resources": {
3+
"MyWebsocketApiEBAC53DF": {
4+
"Type": "AWS::ApiGatewayV2::Api",
5+
"Properties": {
6+
"ApiKeySelectionExpression": "$request.header.x-api-key",
7+
"Name": "MyWebsocketApi",
8+
"ProtocolType": "WEBSOCKET",
9+
"RouteSelectionExpression": "$request.body.action"
10+
}
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
import * as cdk from '@aws-cdk/core';
3+
import * as apigw from '../../lib';
4+
import { WebSocketApiKeySelectionExpression } from '../../lib';
5+
6+
const app = new cdk.App();
7+
8+
const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets');
9+
10+
new apigw.WebSocketApi(stack, 'MyWebsocketApi', {
11+
apiKeySelectionExpression: WebSocketApiKeySelectionExpression.HEADER_X_API_KEY,
12+
});
13+
14+
app.synth();

packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,45 @@ describe('WebSocketRoute', () => {
4242
});
4343
});
4444

45+
test('Api Key is required for route when apiKeyIsRequired is true', () => {
46+
// GIVEN
47+
const stack = new Stack();
48+
const webSocketApi = new WebSocketApi(stack, 'Api');
49+
50+
// WHEN
51+
new WebSocketRoute(stack, 'Route', {
52+
webSocketApi,
53+
integration: new DummyIntegration(),
54+
routeKey: 'message',
55+
apiKeyRequired: true,
56+
});
57+
58+
// THEN
59+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
60+
ApiId: stack.resolve(webSocketApi.apiId),
61+
ApiKeyRequired: true,
62+
RouteKey: 'message',
63+
Target: {
64+
'Fn::Join': [
65+
'',
66+
[
67+
'integrations/',
68+
{
69+
Ref: 'RouteDummyIntegrationE40E82B4',
70+
},
71+
],
72+
],
73+
},
74+
});
75+
76+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', {
77+
ApiId: stack.resolve(webSocketApi.apiId),
78+
IntegrationType: 'AWS_PROXY',
79+
IntegrationUri: 'some-uri',
80+
});
81+
});
82+
83+
4584
test('integration cannot be used across WebSocketApis', () => {
4685
// GIVEN
4786
const integration = new DummyIntegration();

0 commit comments

Comments
 (0)