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): Lambda authorizer for WebSocket API #16886

Merged
merged 16 commits into from
Dec 14, 2021
Merged
49 changes: 42 additions & 7 deletions packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
- [HTTP APIs](#http-apis)
- [Default Authorization](#default-authorization)
- [Route Authorization](#route-authorization)
- [JWT Authorizers](#jwt-authorizers)
- [User Pool Authorizer](#user-pool-authorizer)
- [Lambda Authorizers](#lambda-authorizers)
- [JWT Authorizers](#jwt-authorizers)
- [User Pool Authorizer](#user-pool-authorizer)
- [Lambda Authorizers](#lambda-authorizers)
- [WebSocket APIs](#websocket-apis)
- [Lambda Authorizer](#lambda-authorizer)

## Introduction

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

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

Authorizers, and scopes can either be applied to the api, or specifically for each route.
Authorizers and scopes can either be applied to the api, or specifically for each route.

### Default Authorization

Expand Down Expand Up @@ -110,7 +112,7 @@ api.addRoutes({
});
```

## JWT Authorizers
### JWT Authorizers

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.

Expand Down Expand Up @@ -144,7 +146,7 @@ api.addRoutes({
});
```

### User Pool Authorizer
#### User Pool Authorizer

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.

Expand All @@ -170,7 +172,7 @@ api.addRoutes({
});
```

## Lambda Authorizers
### Lambda Authorizers

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.

Expand All @@ -196,3 +198,36 @@ api.addRoutes({
authorizer,
});
```

## WebSocket APIs

You can set an authorizer to your WebSocket API's `$connect` route to control access to your API.

### Lambda Authorizer

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.

```ts
import { WebSocketLambdaAuthorizer } from '@aws-cdk/aws-apigatewayv2-authorizers';
import { WebSocketLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations';

// This function handles your auth logic
declare const authHandler: lambda.Function;

// This function handles your WebSocket requests
declare const handler: lambda.Function;

const authorizer = new WebSocketLambdaAuthorizer('Authorizer', authHandler);

const integration = new WebSocketLambdaIntegration(
'Integration',
handler,
);

new apigwv2.WebSocketApi(this, 'WebSocketApi', {
connectRouteOptions: {
integration,
authorizer,
},
});
```
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './http';
export * from './websocket';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lambda';
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
WebSocketAuthorizer,
WebSocketAuthorizerType,
WebSocketRouteAuthorizerBindOptions,
WebSocketRouteAuthorizerConfig,
IWebSocketRouteAuthorizer,
IWebSocketApi,
} from '@aws-cdk/aws-apigatewayv2';
import { ServicePrincipal } from '@aws-cdk/aws-iam';
import { IFunction } from '@aws-cdk/aws-lambda';
import { Stack, Names } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct as CoreConstruct } from '@aws-cdk/core';

/**
* Properties to initialize WebSocketTokenAuthorizer.
*/
export interface WebSocketLambdaAuthorizerProps {

/**
* The name of the authorizer
* @default - same value as `id` passed in the constructor.
*/
readonly authorizerName?: string;

/**
* The identity source for which authorization is requested.
*
* @default ['$request.header.Authorization']
*/
readonly identitySource?: string[];
}

/**
* Authorize WebSocket Api routes via a lambda function
*/
export class WebSocketLambdaAuthorizer implements IWebSocketRouteAuthorizer {
private authorizer?: WebSocketAuthorizer;
private webSocketApi?: IWebSocketApi;

constructor(
private readonly id: string,
private readonly handler: IFunction,
private readonly props: WebSocketLambdaAuthorizerProps = {}) {
}

public bind(options: WebSocketRouteAuthorizerBindOptions): WebSocketRouteAuthorizerConfig {
if (this.webSocketApi && (this.webSocketApi.apiId !== options.route.webSocketApi.apiId)) {
throw new Error('Cannot attach the same authorizer to multiple Apis');
}

if (!this.authorizer) {
this.webSocketApi = options.route.webSocketApi;
this.authorizer = new WebSocketAuthorizer(options.scope, this.id, {
webSocketApi: options.route.webSocketApi,
identitySource: this.props.identitySource ?? [
'$request.header.Authorization',
],
type: WebSocketAuthorizerType.LAMBDA,
authorizerName: this.props.authorizerName ?? this.id,
authorizerUri: lambdaAuthorizerArn(this.handler),
});

this.handler.addPermission(`${Names.nodeUniqueId(this.authorizer.node)}-Permission`, {
scope: options.scope as CoreConstruct,
principal: new ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: Stack.of(options.route).formatArn({
service: 'execute-api',
resource: options.route.webSocketApi.apiId,
resourceName: `authorizers/${this.authorizer.authorizerId}`,
}),
});
}

return {
authorizerId: this.authorizer.authorizerId,
authorizationType: 'CUSTOM',
};
}
}

/**
* constructs the authorizerURIArn.
*/
function lambdaAuthorizerArn(handler: IFunction) {
return `arn:${Stack.of(handler).partition}:apigateway:${Stack.of(handler).region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Template } from '@aws-cdk/assertions';
import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2';
import { WebSocketLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations';
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';
import { Stack } from '@aws-cdk/core';
import { WebSocketLambdaAuthorizer } from '../../lib';

describe('WebSocketLambdaAuthorizer', () => {
test('default', () => {
// GIVEN
const stack = new Stack();

const handler = new Function(stack, 'auth-function', {
runtime: Runtime.NODEJS_12_X,
code: Code.fromInline('exports.handler = () => {return true}'),
handler: 'index.handler',
});
const integration = new WebSocketLambdaIntegration(
'Integration',
handler,
);

const authorizer = new WebSocketLambdaAuthorizer('default-authorizer', handler);

// WHEN
new WebSocketApi(stack, 'WebSocketApi', {
connectRouteOptions: {
integration,
authorizer,
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
Name: 'default-authorizer',
AuthorizerType: 'REQUEST',
IdentitySource: [
'$request.header.Authorization',
],
});

Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Route', {
AuthorizationType: 'CUSTOM',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
"Ref": "mywsapi32E6CE11"
},
"RouteKey": "$connect",
"AuthorizationType": "NONE",
"Target": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -373,6 +374,7 @@
"Ref": "mywsapi32E6CE11"
},
"RouteKey": "$disconnect",
"AuthorizationType": "NONE",
"Target": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -462,6 +464,7 @@
"Ref": "mywsapi32E6CE11"
},
"RouteKey": "$default",
"AuthorizationType": "NONE",
"Target": {
"Fn::Join": [
"",
Expand Down Expand Up @@ -551,6 +554,7 @@
"Ref": "mywsapi32E6CE11"
},
"RouteKey": "sendmessage",
"AuthorizationType": "NONE",
"Target": {
"Fn::Join": [
"",
Expand Down
13 changes: 10 additions & 3 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ Higher level constructs for Websocket APIs | ![Experimental](https://img.shields
- [Publishing HTTP APIs](#publishing-http-apis)
- [Custom Domain](#custom-domain)
- [Mutual TLS](#mutual-tls-mtls)
- [Managing access](#managing-access)
- [Managing access to HTTP APIs](#managing-access-to-http-apis)
- [Metrics](#metrics)
- [VPC Link](#vpc-link)
- [Private Integration](#private-integration)
- [WebSocket API](#websocket-api)
- [Manage Connections Permission](#manage-connections-permission)
- [Managing access to WebSocket APIs](#managing-access-to-websocket-apis)

## Introduction

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

## Mutual TLS (mTLS)
### Mutual TLS (mTLS)

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.

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

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

### Managing access
### Managing access to HTTP APIs

API Gateway supports multiple mechanisms for [controlling and managing access to your HTTP
API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html) through authorizers.
Expand Down Expand Up @@ -419,3 +420,9 @@ stage.grantManageConnections(lambda);
// for all the stages permission
webSocketApi.grantManageConnections(lambda);
```

### Managing access to WebSocket APIs

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.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer {
}

/**
* This check is required because Cloudformation will fail stack creation is this property
* This check is required because Cloudformation will fail stack creation if this property
* is set for the JWT authorizer. AuthorizerPayloadFormatVersion can only be set for REQUEST authorizer
*/
if (props.type === HttpAuthorizerType.LAMBDA && typeof authorizerPayloadFormatVersion === 'undefined') {
Expand Down
Loading