Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apigatewayv2): websocket api: api keys #16636

Merged
merged 9 commits into from
Jan 11, 2022
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.
otaviomacedo marked this conversation as resolved.
Show resolved Hide resolved

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module holds both http api and websocket api, so name this WebSocketApiKeySelection

/**
* 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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call this HEADER_X_API_KEY and the other one AUTHORIZER_USAGE_IDENTIFIER_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
alpacamybags118 marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
otaviomacedo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need an integ test for this change. Drop.

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