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 @@ -426,3 +426,17 @@ webSocketApi.grantManageConnections(lambda);
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.

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.

### 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: WebSocketApiKeySelectionExpression.HEADER_X_API_KEY,
});
...
```

30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ import { WebSocketRoute, WebSocketRouteOptions } from './route';
export interface IWebSocketApi extends IApi {
}

/**
* Represents the currently available API Key Selection Expressions
*/
export class WebSocketApiKeySelectionExpression {

/**
* The API will extract the key value from the `x-api-key` header in the user request.
*/
public static readonly HEADER_X_API_KEY = new WebSocketApiKeySelectionExpression('$request.header.x-api-key');

/**
* The API will extract the key value from the `usageIdentifierKey` attribute in the `context` map,
* returned by the Lambda Authorizer.
* See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
*/
public static readonly AUTHORIZER_USAGE_IDENTIFIER_KEY = new WebSocketApiKeySelectionExpression('$context.authorizer.usageIdentifierKey');

/**
* @param customApiKeySelector The expression used by API Gateway
*/
public constructor(public readonly customApiKeySelector: string) {}
}

/**
* Props for WebSocket API
*/
Expand All @@ -22,6 +45,12 @@ 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.
* @default - Key is not required to access these APIs
*/
readonly apiKeySelectionExpression?: WebSocketApiKeySelectionExpression

/**
* The description of the API.
* @default - none
Expand Down Expand Up @@ -76,6 +105,7 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {

const resource = new CfnApi(this, 'Resource', {
name: this.webSocketApiName,
apiKeySelectionExpression: props?.apiKeySelectionExpression?.customApiKeySelector,
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 @@ -52,6 +52,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 @@ -91,6 +97,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/${config.integrationId}`,
authorizerId: authBindResult.authorizerId,
Expand Down
29 changes: 27 additions & 2 deletions packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import { Match, Template } from '@aws-cdk/assertions';
import { User } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import {
WebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType,
WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig,
WebSocketRouteIntegration,
WebSocketApi,
WebSocketApiKeySelectionExpression,
WebSocketIntegrationType,
WebSocketRouteIntegrationBindOptions,
WebSocketRouteIntegrationConfig,
} from '../../lib';

describe('WebSocketApi', () => {
Expand All @@ -25,6 +29,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: WebSocketApiKeySelectionExpression.AUTHORIZER_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 { WebSocketApiKeySelectionExpression } from '../../lib';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-websockets');

new apigw.WebSocketApi(stack, 'MyWebsocketApi', {
apiKeySelectionExpression: WebSocketApiKeySelectionExpression.HEADER_X_API_KEY,
});

app.synth();
39 changes: 39 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 @@ -42,6 +42,45 @@ describe('WebSocketRoute', () => {
});
});

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: 'RouteDummyIntegrationE40E82B4',
},
],
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', {
ApiId: stack.resolve(webSocketApi.apiId),
IntegrationType: 'AWS_PROXY',
IntegrationUri: 'some-uri',
});
});


test('integration cannot be used across WebSocketApis', () => {
// GIVEN
const integration = new DummyIntegration();
Expand Down