Skip to content

Commit

Permalink
feat(apigatewayv2): websocket api: api keys (#16636)
Browse files Browse the repository at this point in the history
----
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*
  • Loading branch information
alpacamybags118 authored Jan 11, 2022
1 parent beb5706 commit 24f8f74
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 2 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 @@ -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.

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;
}

/**
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

0 comments on commit 24f8f74

Please sign in to comment.