Skip to content

Commit

Permalink
feat(aws-apigatewayv2): add support for api-key in websocket apis
Browse files Browse the repository at this point in the history
  • Loading branch information
alpacamybags118 committed Nov 9, 2021
1 parent bc10e6f commit 872f428
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 0 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
...
```

26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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}`,
});
Expand Down
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Resources": {
"MyWebsocketApiEBAC53DF": {
"Type": "AWS::ApiGatewayV2::Api",
"Properties": {
"Name": "MyWebsocketApi",
"ProtocolType": "WEBSOCKET",
"RouteSelectionExpression": "$request.body.action"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
38 changes: 38 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});


Expand Down

0 comments on commit 872f428

Please sign in to comment.