Skip to content

Commit 67cce37

Browse files
authoredDec 14, 2021
feat(apigatewayv2): Lambda authorizer for WebSocket API (#16886)
closes #13869 By this PR, you will be able to enable WebSocket authorizer as the below code: ```ts const integration = new LambdaWebSocketIntegration({ handler, }); const authorizer = new WebSocketLambdaAuthorizer('Authorizer', authHandler); new WebSocketApi(stack, 'WebSocketApi', { connectRouteOptions: { integration, authorizer, }, }); ``` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 499ba85 commit 67cce37

File tree

12 files changed

+418
-13
lines changed

12 files changed

+418
-13
lines changed
 

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

+42-7
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
- [HTTP APIs](#http-apis)
2323
- [Default Authorization](#default-authorization)
2424
- [Route Authorization](#route-authorization)
25-
- [JWT Authorizers](#jwt-authorizers)
26-
- [User Pool Authorizer](#user-pool-authorizer)
27-
- [Lambda Authorizers](#lambda-authorizers)
25+
- [JWT Authorizers](#jwt-authorizers)
26+
- [User Pool Authorizer](#user-pool-authorizer)
27+
- [Lambda Authorizers](#lambda-authorizers)
28+
- [WebSocket APIs](#websocket-apis)
29+
- [Lambda Authorizer](#lambda-authorizer)
2830

2931
## Introduction
3032

@@ -37,7 +39,7 @@ API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-acces
3739

3840
Access control for Http Apis is managed by restricting which routes can be invoked via.
3941

40-
Authorizers, and scopes can either be applied to the api, or specifically for each route.
42+
Authorizers and scopes can either be applied to the api, or specifically for each route.
4143

4244
### Default Authorization
4345

@@ -110,7 +112,7 @@ api.addRoutes({
110112
});
111113
```
112114

113-
## JWT Authorizers
115+
### JWT Authorizers
114116

115117
JWT authorizers allow the use of JSON Web Tokens (JWTs) as part of [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) and [OAuth 2.0](https://oauth.net/2/) frameworks to allow and restrict clients from accessing HTTP APIs.
116118

@@ -144,7 +146,7 @@ api.addRoutes({
144146
});
145147
```
146148

147-
### User Pool Authorizer
149+
#### User Pool Authorizer
148150

149151
User Pool Authorizer is a type of JWT Authorizer that uses a Cognito user pool and app client to control who can access your Api. After a successful authorization from the app client, the generated access token will be used as the JWT.
150152

@@ -170,7 +172,7 @@ api.addRoutes({
170172
});
171173
```
172174

173-
## Lambda Authorizers
175+
### Lambda Authorizers
174176

175177
Lambda authorizers use a Lambda function to control access to your HTTP API. When a client calls your API, API Gateway invokes your Lambda function and uses the response to determine whether the client can access your API.
176178

@@ -196,3 +198,36 @@ api.addRoutes({
196198
authorizer,
197199
});
198200
```
201+
202+
## WebSocket APIs
203+
204+
You can set an authorizer to your WebSocket API's `$connect` route to control access to your API.
205+
206+
### Lambda Authorizer
207+
208+
Lambda authorizers use a Lambda function to control access to your WebSocket API. When a client connects to your API, API Gateway invokes your Lambda function and uses the response to determine whether the client can access your API.
209+
210+
```ts
211+
import { WebSocketLambdaAuthorizer } from '@aws-cdk/aws-apigatewayv2-authorizers';
212+
import { WebSocketLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations';
213+
214+
// This function handles your auth logic
215+
declare const authHandler: lambda.Function;
216+
217+
// This function handles your WebSocket requests
218+
declare const handler: lambda.Function;
219+
220+
const authorizer = new WebSocketLambdaAuthorizer('Authorizer', authHandler);
221+
222+
const integration = new WebSocketLambdaIntegration(
223+
'Integration',
224+
handler,
225+
);
226+
227+
new apigwv2.WebSocketApi(this, 'WebSocketApi', {
228+
connectRouteOptions: {
229+
integration,
230+
authorizer,
231+
},
232+
});
233+
```
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './http';
2+
export * from './websocket';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lambda';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
WebSocketAuthorizer,
3+
WebSocketAuthorizerType,
4+
WebSocketRouteAuthorizerBindOptions,
5+
WebSocketRouteAuthorizerConfig,
6+
IWebSocketRouteAuthorizer,
7+
IWebSocketApi,
8+
} from '@aws-cdk/aws-apigatewayv2';
9+
import { ServicePrincipal } from '@aws-cdk/aws-iam';
10+
import { IFunction } from '@aws-cdk/aws-lambda';
11+
import { Stack, Names } from '@aws-cdk/core';
12+
13+
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
14+
// eslint-disable-next-line no-duplicate-imports, import/order
15+
import { Construct as CoreConstruct } from '@aws-cdk/core';
16+
17+
/**
18+
* Properties to initialize WebSocketTokenAuthorizer.
19+
*/
20+
export interface WebSocketLambdaAuthorizerProps {
21+
22+
/**
23+
* The name of the authorizer
24+
* @default - same value as `id` passed in the constructor.
25+
*/
26+
readonly authorizerName?: string;
27+
28+
/**
29+
* The identity source for which authorization is requested.
30+
*
31+
* @default ['$request.header.Authorization']
32+
*/
33+
readonly identitySource?: string[];
34+
}
35+
36+
/**
37+
* Authorize WebSocket Api routes via a lambda function
38+
*/
39+
export class WebSocketLambdaAuthorizer implements IWebSocketRouteAuthorizer {
40+
private authorizer?: WebSocketAuthorizer;
41+
private webSocketApi?: IWebSocketApi;
42+
43+
constructor(
44+
private readonly id: string,
45+
private readonly handler: IFunction,
46+
private readonly props: WebSocketLambdaAuthorizerProps = {}) {
47+
}
48+
49+
public bind(options: WebSocketRouteAuthorizerBindOptions): WebSocketRouteAuthorizerConfig {
50+
if (this.webSocketApi && (this.webSocketApi.apiId !== options.route.webSocketApi.apiId)) {
51+
throw new Error('Cannot attach the same authorizer to multiple Apis');
52+
}
53+
54+
if (!this.authorizer) {
55+
this.webSocketApi = options.route.webSocketApi;
56+
this.authorizer = new WebSocketAuthorizer(options.scope, this.id, {
57+
webSocketApi: options.route.webSocketApi,
58+
identitySource: this.props.identitySource ?? [
59+
'$request.header.Authorization',
60+
],
61+
type: WebSocketAuthorizerType.LAMBDA,
62+
authorizerName: this.props.authorizerName ?? this.id,
63+
authorizerUri: lambdaAuthorizerArn(this.handler),
64+
});
65+
66+
this.handler.addPermission(`${Names.nodeUniqueId(this.authorizer.node)}-Permission`, {
67+
scope: options.scope as CoreConstruct,
68+
principal: new ServicePrincipal('apigateway.amazonaws.com'),
69+
sourceArn: Stack.of(options.route).formatArn({
70+
service: 'execute-api',
71+
resource: options.route.webSocketApi.apiId,
72+
resourceName: `authorizers/${this.authorizer.authorizerId}`,
73+
}),
74+
});
75+
}
76+
77+
return {
78+
authorizerId: this.authorizer.authorizerId,
79+
authorizationType: 'CUSTOM',
80+
};
81+
}
82+
}
83+
84+
/**
85+
* constructs the authorizerURIArn.
86+
*/
87+
function lambdaAuthorizerArn(handler: IFunction) {
88+
return `arn:${Stack.of(handler).partition}:apigateway:${Stack.of(handler).region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`;
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2';
3+
import { WebSocketLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations';
4+
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';
5+
import { Stack } from '@aws-cdk/core';
6+
import { WebSocketLambdaAuthorizer } from '../../lib';
7+
8+
describe('WebSocketLambdaAuthorizer', () => {
9+
test('default', () => {
10+
// GIVEN
11+
const stack = new Stack();
12+
13+
const handler = new Function(stack, 'auth-function', {
14+
runtime: Runtime.NODEJS_12_X,
15+
code: Code.fromInline('exports.handler = () => {return true}'),
16+
handler: 'index.handler',
17+
});
18+
const integration = new WebSocketLambdaIntegration(
19+
'Integration',
20+
handler,
21+
);
22+
23+
const authorizer = new WebSocketLambdaAuthorizer('default-authorizer', handler);
24+
25+
// WHEN
26+
new WebSocketApi(stack, 'WebSocketApi', {
27+
connectRouteOptions: {
28+
integration,
29+
authorizer,
30+
},
31+
});
32+
33+
// THEN
34+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
35+
Name: 'default-authorizer',
36+
AuthorizerType: 'REQUEST',
37+
IdentitySource: [
38+
'$request.header.Authorization',
39+
],
40+
});
41+
42+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
43+
AuthorizationType: 'CUSTOM',
44+
});
45+
});
46+
});

‎packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json

+4
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
"Ref": "mywsapi32E6CE11"
285285
},
286286
"RouteKey": "$connect",
287+
"AuthorizationType": "NONE",
287288
"Target": {
288289
"Fn::Join": [
289290
"",
@@ -373,6 +374,7 @@
373374
"Ref": "mywsapi32E6CE11"
374375
},
375376
"RouteKey": "$disconnect",
377+
"AuthorizationType": "NONE",
376378
"Target": {
377379
"Fn::Join": [
378380
"",
@@ -462,6 +464,7 @@
462464
"Ref": "mywsapi32E6CE11"
463465
},
464466
"RouteKey": "$default",
467+
"AuthorizationType": "NONE",
465468
"Target": {
466469
"Fn::Join": [
467470
"",
@@ -551,6 +554,7 @@
551554
"Ref": "mywsapi32E6CE11"
552555
},
553556
"RouteKey": "sendmessage",
557+
"AuthorizationType": "NONE",
554558
"Target": {
555559
"Fn::Join": [
556560
"",

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ Higher level constructs for Websocket APIs | ![Experimental](https://img.shields
3535
- [Publishing HTTP APIs](#publishing-http-apis)
3636
- [Custom Domain](#custom-domain)
3737
- [Mutual TLS](#mutual-tls-mtls)
38-
- [Managing access](#managing-access)
38+
- [Managing access to HTTP APIs](#managing-access-to-http-apis)
3939
- [Metrics](#metrics)
4040
- [VPC Link](#vpc-link)
4141
- [Private Integration](#private-integration)
4242
- [WebSocket API](#websocket-api)
4343
- [Manage Connections Permission](#manage-connections-permission)
44+
- [Managing access to WebSocket APIs](#managing-access-to-websocket-apis)
4445

4546
## Introduction
4647

@@ -254,7 +255,7 @@ declare const apiDemo: apigwv2.HttpApi;
254255
const demoDomainUrl = apiDemo.defaultStage?.domainUrl; // returns "https://example.com/demo"
255256
```
256257

257-
## Mutual TLS (mTLS)
258+
### Mutual TLS (mTLS)
258259

259260
Mutual TLS can be configured to limit access to your API based by using client certificates instead of (or as an extension of) using authorization headers.
260261

@@ -277,7 +278,7 @@ new DomainName(stack, 'DomainName', {
277278

278279
Instructions for configuring your trust store can be found [here](https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/)
279280

280-
### Managing access
281+
### Managing access to HTTP APIs
281282

282283
API Gateway supports multiple mechanisms for [controlling and managing access to your HTTP
283284
API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html) through authorizers.
@@ -419,3 +420,9 @@ stage.grantManageConnections(lambda);
419420
// for all the stages permission
420421
webSocketApi.grantManageConnections(lambda);
421422
```
423+
424+
### Managing access to WebSocket APIs
425+
426+
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.
427+
428+
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.

‎packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer {
166166
}
167167

168168
/**
169-
* This check is required because Cloudformation will fail stack creation is this property
169+
* This check is required because Cloudformation will fail stack creation if this property
170170
* is set for the JWT authorizer. AuthorizerPayloadFormatVersion can only be set for REQUEST authorizer
171171
*/
172172
if (props.type === HttpAuthorizerType.LAMBDA && typeof authorizerPayloadFormatVersion === 'undefined') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Resource } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
import { CfnAuthorizer } from '../apigatewayv2.generated';
4+
5+
import { IAuthorizer } from '../common';
6+
import { IWebSocketApi } from './api';
7+
import { IWebSocketRoute } from './route';
8+
9+
/**
10+
* Supported Authorizer types
11+
*/
12+
export enum WebSocketAuthorizerType {
13+
/** Lambda Authorizer */
14+
LAMBDA = 'REQUEST',
15+
}
16+
17+
/**
18+
* Properties to initialize an instance of `WebSocketAuthorizer`.
19+
*/
20+
export interface WebSocketAuthorizerProps {
21+
/**
22+
* Name of the authorizer
23+
* @default - id of the WebSocketAuthorizer construct.
24+
*/
25+
readonly authorizerName?: string
26+
27+
/**
28+
* WebSocket Api to attach the authorizer to
29+
*/
30+
readonly webSocketApi: IWebSocketApi
31+
32+
/**
33+
* The type of authorizer
34+
*/
35+
readonly type: WebSocketAuthorizerType;
36+
37+
/**
38+
* The identity source for which authorization is requested.
39+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-authorizer.html#cfn-apigatewayv2-authorizer-identitysource
40+
*/
41+
readonly identitySource: string[];
42+
43+
/**
44+
* The authorizer's Uniform Resource Identifier (URI).
45+
*
46+
* For REQUEST authorizers, this must be a well-formed Lambda function URI.
47+
*
48+
* @default - required for Request authorizer types
49+
*/
50+
readonly authorizerUri?: string;
51+
}
52+
53+
/**
54+
* An authorizer for WebSocket APIs
55+
*/
56+
export interface IWebSocketAuthorizer extends IAuthorizer {
57+
}
58+
59+
/**
60+
* Reference to an WebSocket authorizer
61+
*/
62+
export interface WebSocketAuthorizerAttributes {
63+
/**
64+
* Id of the Authorizer
65+
*/
66+
readonly authorizerId: string
67+
68+
/**
69+
* Type of authorizer
70+
*
71+
* Possible values are:
72+
* - CUSTOM - Lambda Authorizer
73+
* - NONE - No Authorization
74+
*/
75+
readonly authorizerType: string
76+
}
77+
78+
/**
79+
* An authorizer for WebSocket Apis
80+
* @resource AWS::ApiGatewayV2::Authorizer
81+
*/
82+
export class WebSocketAuthorizer extends Resource implements IWebSocketAuthorizer {
83+
/**
84+
* Import an existing WebSocket Authorizer into this CDK app.
85+
*/
86+
public static fromWebSocketAuthorizerAttributes(scope: Construct, id: string, attrs: WebSocketAuthorizerAttributes): IWebSocketRouteAuthorizer {
87+
class Import extends Resource implements IWebSocketRouteAuthorizer {
88+
public readonly authorizerId = attrs.authorizerId;
89+
public readonly authorizerType = attrs.authorizerType;
90+
91+
public bind(): WebSocketRouteAuthorizerConfig {
92+
return {
93+
authorizerId: attrs.authorizerId,
94+
authorizationType: attrs.authorizerType,
95+
};
96+
}
97+
}
98+
return new Import(scope, id);
99+
}
100+
101+
public readonly authorizerId: string;
102+
103+
constructor(scope: Construct, id: string, props: WebSocketAuthorizerProps) {
104+
super(scope, id);
105+
106+
if (props.type === WebSocketAuthorizerType.LAMBDA && !props.authorizerUri) {
107+
throw new Error('authorizerUri is mandatory for Lambda authorizers');
108+
}
109+
110+
const resource = new CfnAuthorizer(this, 'Resource', {
111+
name: props.authorizerName ?? id,
112+
apiId: props.webSocketApi.apiId,
113+
authorizerType: props.type,
114+
identitySource: props.identitySource,
115+
authorizerUri: props.authorizerUri,
116+
});
117+
118+
this.authorizerId = resource.ref;
119+
}
120+
}
121+
122+
/**
123+
* Input to the bind() operation, that binds an authorizer to a route.
124+
*/
125+
export interface WebSocketRouteAuthorizerBindOptions {
126+
/**
127+
* The route to which the authorizer is being bound.
128+
*/
129+
readonly route: IWebSocketRoute;
130+
/**
131+
* The scope for any constructs created as part of the bind.
132+
*/
133+
readonly scope: Construct;
134+
}
135+
136+
/**
137+
* Results of binding an authorizer to an WebSocket route.
138+
*/
139+
export interface WebSocketRouteAuthorizerConfig {
140+
/**
141+
* The authorizer id
142+
*
143+
* @default - No authorizer id (useful for AWS_IAM route authorizer)
144+
*/
145+
readonly authorizerId?: string;
146+
147+
/**
148+
* The type of authorization
149+
*
150+
* Possible values are:
151+
* - CUSTOM - Lambda Authorizer
152+
* - NONE - No Authorization
153+
*/
154+
readonly authorizationType: string;
155+
}
156+
157+
/**
158+
* An authorizer that can attach to an WebSocket Route.
159+
*/
160+
export interface IWebSocketRouteAuthorizer {
161+
/**
162+
* Bind this authorizer to a specified WebSocket route.
163+
*/
164+
bind(options: WebSocketRouteAuthorizerBindOptions): WebSocketRouteAuthorizerConfig;
165+
}
166+
167+
/**
168+
* Explicitly configure no authorizers on specific WebSocket API routes.
169+
*/
170+
export class WebSocketNoneAuthorizer implements IWebSocketRouteAuthorizer {
171+
public bind(_: WebSocketRouteAuthorizerBindOptions): WebSocketRouteAuthorizerConfig {
172+
return {
173+
authorizationType: 'NONE',
174+
};
175+
}
176+
}

‎packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './api';
22
export * from './route';
33
export * from './stage';
44
export * from './integration';
5+
export * from './authorizer';

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

+21-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Construct } from 'constructs';
33
import { CfnRoute } from '../apigatewayv2.generated';
44
import { IRoute } from '../common';
55
import { IWebSocketApi } from './api';
6+
import { IWebSocketRouteAuthorizer, WebSocketNoneAuthorizer } from './authorizer';
67
import { WebSocketRouteIntegration } from './integration';
78

89
/**
@@ -29,15 +30,21 @@ export interface WebSocketRouteOptions {
2930
* The integration to be configured on this route.
3031
*/
3132
readonly integration: WebSocketRouteIntegration;
32-
}
3333

34+
/**
35+
* The authorize to this route. You can only set authorizer to a $connect route.
36+
*
37+
* @default - No Authorizer
38+
*/
39+
readonly authorizer?: IWebSocketRouteAuthorizer;
40+
}
3441

3542
/**
3643
* Properties to initialize a new Route
3744
*/
3845
export interface WebSocketRouteProps extends WebSocketRouteOptions {
3946
/**
40-
* the API the route is associated with
47+
* The API the route is associated with.
4148
*/
4249
readonly webSocketApi: IWebSocketApi;
4350

@@ -64,6 +71,10 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute {
6471
constructor(scope: Construct, id: string, props: WebSocketRouteProps) {
6572
super(scope, id);
6673

74+
if (props.routeKey != '$connect' && props.authorizer) {
75+
throw new Error('You can only set a WebSocket authorizer to a $connect route.');
76+
}
77+
6778
this.webSocketApi = props.webSocketApi;
6879
this.routeKey = props.routeKey;
6980

@@ -72,10 +83,18 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute {
7283
scope: this,
7384
});
7485

86+
const authorizer = props.authorizer ?? new WebSocketNoneAuthorizer(); // must be explicitly NONE (not undefined) for stack updates to work correctly
87+
const authBindResult = authorizer.bind({
88+
route: this,
89+
scope: this.webSocketApi instanceof Construct ? this.webSocketApi : this, // scope under the API if it's not imported
90+
});
91+
7592
const route = new CfnRoute(this, 'Resource', {
7693
apiId: props.webSocketApi.apiId,
7794
routeKey: props.routeKey,
7895
target: `integrations/${config.integrationId}`,
96+
authorizerId: authBindResult.authorizerId,
97+
authorizationType: authBindResult.authorizationType,
7998
});
8099
this.routeId = route.ref;
81100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import { Stack } from '@aws-cdk/core';
3+
import {
4+
WebSocketApi, WebSocketAuthorizer, WebSocketAuthorizerType,
5+
} from '../../lib';
6+
7+
describe('WebSocketAuthorizer', () => {
8+
describe('lambda', () => {
9+
it('default', () => {
10+
const stack = new Stack();
11+
const webSocketApi = new WebSocketApi(stack, 'WebSocketApi');
12+
13+
new WebSocketAuthorizer(stack, 'WebSocketAuthorizer', {
14+
webSocketApi,
15+
identitySource: ['identitysource.1', 'identitysource.2'],
16+
type: WebSocketAuthorizerType.LAMBDA,
17+
authorizerUri: 'arn:cool-lambda-arn',
18+
});
19+
20+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
21+
AuthorizerType: 'REQUEST',
22+
AuthorizerUri: 'arn:cool-lambda-arn',
23+
});
24+
});
25+
});
26+
});

0 commit comments

Comments
 (0)
Please sign in to comment.