Skip to content

Commit

Permalink
feat(apigatewayv2): Lambda authorizer for WebSocket API (#16886)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
tmokmss authored Dec 14, 2021
1 parent 499ba85 commit 67cce37
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 13 deletions.
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

0 comments on commit 67cce37

Please sign in to comment.