Skip to content

Commit

Permalink
feat(apigateway): lambda token authorizer (#5197)
Browse files Browse the repository at this point in the history
* feat(apigateway): L2 support for lambda token authorizers

* Address PR comments

* More PR feedback

* Restructure binding

* Restructuring classes to allow for Authorizer.token() and Authorizer.iam() experience

* PR feedback

* Authorizer -> Authorization
* drop using Physical Name

* Switch to eslint recommended import style

* chore: proposed refactor for authorizers design (#5584)

* simplify authorizers class design

- rename `AuthorizerBase` to `Authorizer`. This class should actually have the `CfnAuthorizer` instantiation, but will only be introduced when an additional authorizer is included.
- simplify `AuthorizerBase` dramatically
- move logic to cache `restApiId` from `AuthorizerBase` to `TokenAuthorizer`. When an additional authorizer is added, we will refactor.
- remove the usage `Authorizer.token`. It is non-idiomatic in this context since we support one authorizer reused multiple times.

* moved Authorizer to authorizer.ts

* fix broken references and types

Co-authored-by: Niranjan Jayakar <16217941+nija-at@users.noreply.github.com>

* Documentation updates & PR feedback

Co-authored-by: Elad Ben-Israel <benisrae@amazon.com>
2 people authored and mergify[bot] committed Jan 2, 2020

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent 590d2ac commit 5c16744
Showing 14 changed files with 1,216 additions and 6 deletions.
65 changes: 65 additions & 0 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
@@ -332,6 +332,71 @@ const proxy = resource.addProxy({
});
```

### Authorizers

API Gateway [supports several different authorization types](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-control-access-to-api.html)
that can be used for controlling access to your REST APIs.

#### IAM-based authorizer

The following CDK code provides 'excecute-api' permission to an IAM user, via IAM policies, for the 'GET' method on the `books` resource:

```ts
const getBooks = books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), {
authorizationType: apigateway.AuthorizationType.IAM
});

iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', {
statements: [
new iam.PolicyStatement({
actions: [ 'execute-api:Invoke' ],
effect: iam.Effect.Allow,
resources: [ getBooks.methodArn() ]
})
]
}))
```

#### Lambda-based token authorizer

API Gateway also allows [lambda functions to be used as authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html).

This module provides support for token-based Lambda authorizers. When a client makes a request to an API's methods configured with such
an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output.
A token-based Lambda authorizer (also called a token authorizer) receives the caller's identity in a bearer token, such as
a JSON Web Token (JWT) or an OAuth token.

API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format.
The event object that the handler is called with contains the `authorizationToken` and the `methodArn` from the request to the
API Gateway endpoint. The handler is expected to return the `principalId` (i.e. the client identifier) and a `policyDocument` stating
what the client is authorizer to perform.
See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for a detailed specification on
inputs and outputs of the lambda handler.

The following code attaches a token-based Lambda authorizer to the 'GET' Method of the Book resource:

```ts
const authFn = new lambda.Function(this, 'booksAuthorizerLambda', {
// ...
// ...
});

const auth = new lambda.TokenAuthorizer(this, 'booksAuthorizer', {
function: authFn
});

books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), {
authorizer: auth
});
```

By default, the `TokenAuthorizer` looks for the authorization token in the request header with the key 'Authorization'. This can,
however, be modified by changing the `identitySource` property.

Authorizers can also be passed via the `defaultMethodOptions` property within the `RestApi` construct or the `Method` construct. Unless
explicitly overridden, the specified defaults will be applied across all `Method`s across the `RestApi` or across all `Resource`s,
depending on where the defaults were specified.

### Deployments

By default, the `RestApi` construct will automatically create an API Gateway
26 changes: 25 additions & 1 deletion packages/@aws-cdk/aws-apigateway/lib/authorizer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import { Resource } from '@aws-cdk/core';
import { AuthorizationType } from './method';
import { RestApi } from './restapi';

/**
* Base class for all custom authorizers
*/
export abstract class Authorizer extends Resource implements IAuthorizer {
public readonly abstract authorizerId: string;
public readonly authorizationType?: AuthorizationType = AuthorizationType.CUSTOM;

/**
* Called when the authorizer is used from a specific REST API.
* @internal
*/
public abstract _attachToApi(restApi: RestApi): void;
}

/**
* Represents an API Gateway authorizer.
*/
export interface IAuthorizer {
/**
* The authorizer ID.
* @attribute
*/
readonly authorizerId: string;
}

/**
* The authorization type of this authorizer.
*/
readonly authorizationType?: AuthorizationType;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lambda';
141 changes: 141 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, Duration, Lazy, Stack } from '@aws-cdk/core';
import { CfnAuthorizer } from '../apigateway.generated';
import { Authorizer, IAuthorizer } from '../authorizer';
import { RestApi } from '../restapi';

/**
* Properties for TokenAuthorizer
*/
export interface TokenAuthorizerProps {

/**
* An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer.
*
* @default - none
*/
readonly authorizerName?: string;

/**
* The handler for the authorizer lambda function.
*
* The handler must follow a very specific protocol on the input it receives and the output it needs to produce.
* API Gateway has documented the handler's input specification
* {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html | here} and output specification
* {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html | here}.
*/
readonly handler: lambda.IFunction;

/**
* The request header mapping expression for the bearer token. This is typically passed as part of the header, in which case
* this should be `method.request.header.Authorizer` where Authorizer is the header containing the bearer token.
* @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource
* @default 'method.request.header.Authorization'
*/
readonly identitySource?: string;

/**
* How long APIGateway should cache the results. Max 1 hour.
* Disable caching by setting this to 0.
*
* @default Duration.minutes(5)
*/
readonly resultsCacheTtl?: Duration;

/**
* An optional regex to be matched against the authorization token. When matched the authorizer lambda is invoked,
* otherwise a 401 Unauthorized is returned to the client.
*
* @default - no regex filter will be applied.
*/
readonly validationRegex?: string;

/**
* An optional IAM role for APIGateway to assume before calling the Lambda-based authorizer. The IAM role must be
* assumable by 'apigateway.amazonaws.com'.
*
* @default - A resource policy is added to the Lambda function allowing apigateway.amazonaws.com to invoke the function.
*/
readonly assumeRole?: iam.IRole;
}

/**
* Token based lambda authorizer that recognizes the caller's identity as a bearer token,
* such as a JSON Web Token (JWT) or an OAuth token.
* Based on the token, authorization is performed by a lambda function.
*
* @resource AWS::ApiGateway::Authorizer
*/
export class TokenAuthorizer extends Authorizer implements IAuthorizer {

/**
* The id of the authorizer.
* @attribute
*/
public readonly authorizerId: string;

/**
* The ARN of the authorizer to be used in permission policies, such as IAM and resource-based grants.
*/
public readonly authorizerArn: string;

private restApiId?: string;

constructor(scope: Construct, id: string, props: TokenAuthorizerProps) {
super(scope, id);

if (props.resultsCacheTtl && props.resultsCacheTtl.toSeconds() > 3600) {
throw new Error(`Lambda authorizer property 'resultsCacheTtl' must not be greater than 3600 seconds (1 hour)`);
}

const restApiId = Lazy.stringValue({ produce: () => this.restApiId });

const resource = new CfnAuthorizer(this, 'Resource', {
name: props.authorizerName,
restApiId,
type: 'TOKEN',
authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`,
authorizerCredentials: props.assumeRole ? props.assumeRole.roleArn : undefined,
authorizerResultTtlInSeconds: props.resultsCacheTtl && props.resultsCacheTtl.toSeconds(),
identitySource: props.identitySource || 'method.request.header.Authorization',
identityValidationExpression: props.validationRegex,
});

this.authorizerId = resource.ref;

this.authorizerArn = Stack.of(this).formatArn({
service: 'execute-api',
resource: restApiId,
resourceName: `authorizers/${this.authorizerId}`
});

if (!props.assumeRole) {
props.handler.addPermission(`${this.node.uniqueId}:Permissions`, {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: this.authorizerArn
});
} else if (props.assumeRole instanceof iam.Role) { // i.e., not imported
props.assumeRole.attachInlinePolicy(new iam.Policy(this, 'authorizerInvokePolicy', {
statements: [
new iam.PolicyStatement({
resources: [ props.handler.functionArn ],
actions: [ 'lambda:InvokeFunction' ],
})
]
}));
}
}

/**
* Attaches this authorizer to a specific REST API.
* @internal
*/
public _attachToApi(restApi: RestApi) {
if (this.restApiId && this.restApiId !== restApi.restApiId) {
throw new Error(`Cannot attach authorizer to two different rest APIs`);
}

this.restApiId = restApi.restApiId;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export * from './json-schema';
export * from './domain-name';
export * from './base-path-mapping';
export * from './cors';
export * from './authorizers';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
30 changes: 26 additions & 4 deletions packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct, Resource, Stack } from '@aws-cdk/core';
import { CfnMethod, CfnMethodProps } from './apigateway.generated';
import { IAuthorizer } from './authorizer';
import { Authorizer, IAuthorizer } from './authorizer';
import { ConnectionType, Integration } from './integration';
import { MockIntegration } from './integrations/mock';
import { MethodResponse } from './methodresponse';
@@ -19,13 +19,21 @@ export interface MethodOptions {

/**
* Method authorization.
* @default None open access
* If the value is set of `Custom`, an `authorizer` must also be specified.
*
* If you're using one of the authorizers that are available via the {@link Authorizer} class, such as {@link Authorizer#token()},
* it is recommended that this option not be specified. The authorizer will take care of setting the correct authorization type.
* However, specifying an authorization type using this property that conflicts with what is expected by the {@link Authorizer}
* will result in an error.
*
* @default - open access unless `authorizer` is specified
*/
readonly authorizationType?: AuthorizationType;

/**
* If `authorizationType` is `Custom`, this specifies the ID of the method
* authorizer resource.
* If specified, the value of `authorizationType` must be set to `Custom`
*/
readonly authorizer?: IAuthorizer;

@@ -117,15 +125,29 @@ export class Method extends Resource {

const defaultMethodOptions = props.resource.defaultMethodOptions || {};
const authorizer = options.authorizer || defaultMethodOptions.authorizer;
const authorizerId = authorizer?.authorizerId;

const authorizationTypeOption = options.authorizationType || defaultMethodOptions.authorizationType;
const authorizationType = authorizer?.authorizationType || authorizationTypeOption || AuthorizationType.NONE;

// if the authorizer defines an authorization type and we also have an explicit option set, check that they are the same
if (authorizer?.authorizationType && authorizationTypeOption && authorizer?.authorizationType !== authorizationTypeOption) {
throw new Error(`${this.resource}/${this.httpMethod} - Authorization type is set to ${authorizationTypeOption} ` +
`which is different from what is required by the authorizer [${authorizer.authorizationType}]`);
}

if (authorizer instanceof Authorizer) {
authorizer._attachToApi(this.restApi);
}

const methodProps: CfnMethodProps = {
resourceId: props.resource.resourceId,
restApiId: this.restApi.restApiId,
httpMethod: this.httpMethod,
operationName: options.operationName || defaultMethodOptions.operationName,
apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired,
authorizationType: options.authorizationType || defaultMethodOptions.authorizationType || AuthorizationType.NONE,
authorizerId: authorizer && authorizer.authorizerId,
authorizationType,
authorizerId,
requestParameters: options.requestParameters || defaultMethodOptions.requestParameters,
integration: this.renderIntegration(props.integration),
methodResponses: this.renderMethodResponses(options.methodResponses),
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-apigateway/package.json
Original file line number Diff line number Diff line change
@@ -285,7 +285,8 @@
"props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.operationName",
"props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestModels",
"props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestValidator",
"docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase.url"
"docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase.url",
"attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn"
]
},
"stability": "stable"
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
{
"Resources": {
"MyAuthorizerFunctionServiceRole8A34C19E": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"MyAuthorizerFunction70F1223E": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4"
},
"S3Key": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E"
}
]
}
]
}
]
]
}
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"MyAuthorizerFunctionServiceRole8A34C19E",
"Arn"
]
},
"Runtime": "nodejs10.x"
},
"DependsOn": [
"MyAuthorizerFunctionServiceRole8A34C19E"
]
},
"authorizerRole06E70703": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"MyAuthorizer6575980E": {
"Type": "AWS::ApiGateway::Authorizer",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"Type": "TOKEN",
"AuthorizerCredentials": {
"Fn::GetAtt": [
"authorizerRole06E70703",
"Arn"
]
},
"AuthorizerUri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region"
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"MyAuthorizerFunction70F1223E",
"Arn"
]
},
"/invocations"
]
]
},
"IdentitySource": "method.request.header.Authorization"
}
},
"MyAuthorizerauthorizerInvokePolicy0F88B8E1": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "lambda:InvokeFunction",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"MyAuthorizerFunction70F1223E",
"Arn"
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "MyAuthorizerauthorizerInvokePolicy0F88B8E1",
"Roles": [
{
"Ref": "authorizerRole06E70703"
}
]
}
},
"MyRestApi2D1F47A9": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "MyRestApi"
}
},
"MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"Description": "Automatically created by the RestApi construct"
},
"DependsOn": [
"MyRestApiANY05143F93"
]
},
"MyRestApiDeploymentStageprodC33B8E5F": {
"Type": "AWS::ApiGateway::Stage",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"DeploymentId": {
"Ref": "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca"
},
"StageName": "prod"
}
},
"MyRestApiCloudWatchRoleD4042E8E": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
]
]
}
]
}
},
"MyRestApiAccount2FB6DB7A": {
"Type": "AWS::ApiGateway::Account",
"Properties": {
"CloudWatchRoleArn": {
"Fn::GetAtt": [
"MyRestApiCloudWatchRoleD4042E8E",
"Arn"
]
}
},
"DependsOn": [
"MyRestApi2D1F47A9"
]
},
"MyRestApiANY05143F93": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "ANY",
"ResourceId": {
"Fn::GetAtt": [
"MyRestApi2D1F47A9",
"RootResourceId"
]
},
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"AuthorizationType": "CUSTOM",
"AuthorizerId": {
"Ref": "MyAuthorizer6575980E"
},
"Integration": {
"IntegrationResponses": [
{
"StatusCode": "200"
}
],
"PassthroughBehavior": "NEVER",
"RequestTemplates": {
"application/json": "{ \"statusCode\": 200 }"
},
"Type": "MOCK"
},
"MethodResponses": [
{
"StatusCode": "200"
}
]
}
}
},
"Parameters": {
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": {
"Type": "String",
"Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
},
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": {
"Type": "String",
"Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
},
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": {
"Type": "String",
"Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
}
},
"Outputs": {
"MyRestApiEndpoint4C55E4CB": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "MyRestApi2D1F47A9"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".",
{
"Ref": "AWS::URLSuffix"
},
"/",
{
"Ref": "MyRestApiDeploymentStageprodC33B8E5F"
},
"/"
]
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { App, Stack } from '@aws-cdk/core';
import * as path from 'path';
import { AuthorizationType, MockIntegration, PassthroughBehavior, RestApi, TokenAuthorizer } from '../../lib';

// Against the RestApi endpoint from the stack output, run
// `curl -s -o /dev/null -w "%{http_code}" <url>` should return 401
// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' <url>` should return 403
// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' <url>` should return 200

const app = new App();
const stack = new Stack(app, 'TokenAuthorizerIAMRoleInteg');

const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'index.handler',
code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.token-authorizer.handler'))
});

const role = new iam.Role(stack, 'authorizerRole', {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com')
});

const authorizer = new TokenAuthorizer(stack, 'MyAuthorizer', {
handler: authorizerFn,
assumeRole: role,
});

const restapi = new RestApi(stack, 'MyRestApi');

restapi.root.addMethod('ANY', new MockIntegration({
integrationResponses: [
{ statusCode: '200' }
],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
'application/json': '{ "statusCode": 200 }',
},
}), {
methodResponses: [
{ statusCode: '200' }
],
authorizer,
authorizationType: AuthorizationType.CUSTOM
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
{
"Resources": {
"MyAuthorizerFunctionServiceRole8A34C19E": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"MyAuthorizerFunction70F1223E": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4"
},
"S3Key": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E"
}
]
}
]
}
]
]
}
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"MyAuthorizerFunctionServiceRole8A34C19E",
"Arn"
]
},
"Runtime": "nodejs10.x"
},
"DependsOn": [
"MyAuthorizerFunctionServiceRole8A34C19E"
]
},
"MyRestApi2D1F47A9": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "MyRestApi"
}
},
"MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"Description": "Automatically created by the RestApi construct"
},
"DependsOn": [
"MyRestApiANY05143F93"
]
},
"MyRestApiDeploymentStageprodC33B8E5F": {
"Type": "AWS::ApiGateway::Stage",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"DeploymentId": {
"Ref": "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca"
},
"StageName": "prod"
}
},
"MyRestApiCloudWatchRoleD4042E8E": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
]
]
}
]
}
},
"MyRestApiAccount2FB6DB7A": {
"Type": "AWS::ApiGateway::Account",
"Properties": {
"CloudWatchRoleArn": {
"Fn::GetAtt": [
"MyRestApiCloudWatchRoleD4042E8E",
"Arn"
]
}
},
"DependsOn": [
"MyRestApi2D1F47A9"
]
},
"MyRestApiANY05143F93": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "ANY",
"ResourceId": {
"Fn::GetAtt": [
"MyRestApi2D1F47A9",
"RootResourceId"
]
},
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"AuthorizationType": "CUSTOM",
"AuthorizerId": {
"Ref": "MyAuthorizer6575980E"
},
"Integration": {
"IntegrationResponses": [
{
"StatusCode": "200"
}
],
"PassthroughBehavior": "NEVER",
"RequestTemplates": {
"application/json": "{ \"statusCode\": 200 }"
},
"Type": "MOCK"
},
"MethodResponses": [
{
"StatusCode": "200"
}
]
}
},
"MyAuthorizer6575980E": {
"Type": "AWS::ApiGateway::Authorizer",
"Properties": {
"RestApiId": {
"Ref": "MyRestApi2D1F47A9"
},
"Type": "TOKEN",
"AuthorizerUri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region"
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"MyAuthorizerFunction70F1223E",
"Arn"
]
},
"/invocations"
]
]
},
"IdentitySource": "method.request.header.Authorization"
}
},
"MyAuthorizerFunctionTokenAuthorizerIntegMyAuthorizer793B1D5FPermissions7557AE26": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"MyAuthorizerFunction70F1223E",
"Arn"
]
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":execute-api:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":",
{
"Ref": "MyRestApi2D1F47A9"
},
"/authorizers/",
{
"Ref": "MyAuthorizer6575980E"
}
]
]
}
}
}
},
"Parameters": {
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": {
"Type": "String",
"Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
},
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": {
"Type": "String",
"Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
},
"AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": {
"Type": "String",
"Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\""
}
},
"Outputs": {
"MyRestApiEndpoint4C55E4CB": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "MyRestApi2D1F47A9"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".",
{
"Ref": "AWS::URLSuffix"
},
"/",
{
"Ref": "MyRestApiDeploymentStageprodC33B8E5F"
},
"/"
]
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// tslint:disable:no-console

export const handler = async (event: any, _context: any = {}): Promise<any> => {
const authToken: string = event.authorizationToken;
console.log(`event.authorizationToken = ${authToken}`);
if (authToken === 'allow' || authToken === 'deny') {
return {
principalId: 'user',
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: authToken,
Resource: event.methodArn
}
]
}
};
} else {
throw new Error('Unauthorized');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as lambda from '@aws-cdk/aws-lambda';
import { App, Stack } from '@aws-cdk/core';
import * as path from 'path';
import { MockIntegration, PassthroughBehavior, RestApi, TokenAuthorizer } from '../../lib';

// Against the RestApi endpoint from the stack output, run
// `curl -s -o /dev/null -w "%{http_code}" <url>` should return 401
// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' <url>` should return 403
// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' <url>` should return 200

const app = new App();
const stack = new Stack(app, 'TokenAuthorizerInteg');

const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'index.handler',
code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.token-authorizer.handler'))
});

const restapi = new RestApi(stack, 'MyRestApi');

const authorizer = new TokenAuthorizer(stack, 'MyAuthorizer', {
handler: authorizerFn,
});

restapi.root.addMethod('ANY', new MockIntegration({
integrationResponses: [
{ statusCode: '200' }
],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
'application/json': '{ "statusCode": 200 }',
},
}), {
methodResponses: [
{ statusCode: '200' }
],
authorizer
});
130 changes: 130 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Duration, Stack } from '@aws-cdk/core';
import { Test } from 'nodeunit';
import { AuthorizationType, RestApi, TokenAuthorizer } from '../../lib';

export = {
'default token authorizer'(test: Test) {
const stack = new Stack();

const func = new lambda.Function(stack, 'myfunction', {
handler: 'handler',
code: lambda.Code.fromInline('foo'),
runtime: lambda.Runtime.NODEJS_8_10,
});

const auth = new TokenAuthorizer(stack, 'myauthorizer', {
handler: func
});

const restApi = new RestApi(stack, 'myrestapi');
restApi.root.addMethod('ANY', undefined, {
authorizer: auth,
authorizationType: AuthorizationType.CUSTOM
});

expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', {
Type: 'TOKEN',
RestApiId: stack.resolve(restApi.restApiId),
IdentitySource: 'method.request.header.Authorization'
}));

expect(stack).to(haveResource('AWS::Lambda::Permission', {
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
}));

test.ok(auth.authorizerArn.endsWith(`/authorizers/${auth.authorizerId}`), 'Malformed authorizer ARN');

test.done();
},

'token authorizer with all parameters specified'(test: Test) {
const stack = new Stack();

const func = new lambda.Function(stack, 'myfunction', {
handler: 'handler',
code: lambda.Code.fromInline('foo'),
runtime: lambda.Runtime.NODEJS_8_10,
});

const auth = new TokenAuthorizer(stack, 'myauthorizer', {
handler: func,
identitySource: 'method.request.header.whoami',
validationRegex: 'a-hacker',
authorizerName: 'myauthorizer',
resultsCacheTtl: Duration.minutes(1),
});

const restApi = new RestApi(stack, 'myrestapi');
restApi.root.addMethod('ANY', undefined, {
authorizer: auth,
authorizationType: AuthorizationType.CUSTOM
});

expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', {
Type: 'TOKEN',
RestApiId: stack.resolve(restApi.restApiId),
IdentitySource: 'method.request.header.whoami',
IdentityValidationExpression: 'a-hacker',
Name: 'myauthorizer',
AuthorizerResultTtlInSeconds: 60
}));

test.done();
},

'token authorizer with assume role'(test: Test) {
const stack = new Stack();

const func = new lambda.Function(stack, 'myfunction', {
handler: 'handler',
code: lambda.Code.fromInline('foo'),
runtime: lambda.Runtime.NODEJS_8_10,
});

const role = new iam.Role(stack, 'authorizerassumerole', {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
roleName: 'authorizerassumerole'
});

const auth = new TokenAuthorizer(stack, 'myauthorizer', {
handler: func,
assumeRole: role
});

const restApi = new RestApi(stack, 'myrestapi');
restApi.root.addMethod('ANY', undefined, {
authorizer: auth,
authorizationType: AuthorizationType.CUSTOM
});

expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', {
Type: 'TOKEN',
RestApiId: stack.resolve(restApi.restApiId),
}));

expect(stack).to(haveResource('AWS::IAM::Role'));

expect(stack).to(haveResource('AWS::IAM::Policy', {
Roles: [
stack.resolve(role.roleName)
],
PolicyDocument: {
Statement: [
{
Resource: stack.resolve(func.functionArn),
Action: 'lambda:InvokeFunction',
Effect: 'Allow',
}
],
}
}, ResourcePart.Properties, true));

expect(stack).notTo(haveResource('AWS::Lambda::Permission'));

test.done();
}
};
86 changes: 86 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/test.method.ts
Original file line number Diff line number Diff line change
@@ -2,10 +2,16 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
import * as apigw from '../lib';

const DUMMY_AUTHORIZER: apigw.IAuthorizer = {
authorizerId: 'dummyauthorizer',
authorizationType: apigw.AuthorizationType.CUSTOM
};

export = {
'default setup'(test: Test) {
// GIVEN
@@ -609,4 +615,84 @@ export = {

test.done();
},

'authorizer is bound correctly'(test: Test) {
const stack = new cdk.Stack();

const restApi = new apigw.RestApi(stack, 'myrestapi');
restApi.root.addMethod('ANY', undefined, {
authorizer: DUMMY_AUTHORIZER
});

expect(stack).to(haveResource('AWS::ApiGateway::Method', {
HttpMethod: 'ANY',
AuthorizationType: 'CUSTOM',
AuthorizerId: DUMMY_AUTHORIZER.authorizerId,
}));

test.done();
},

'authorizer via default method options'(test: Test) {
const stack = new cdk.Stack();

const func = new lambda.Function(stack, 'myfunction', {
handler: 'handler',
code: lambda.Code.fromInline('foo'),
runtime: lambda.Runtime.NODEJS_8_10,
});

const auth = new apigw.TokenAuthorizer(stack, 'myauthorizer1', {
authorizerName: 'myauthorizer1',
handler: func
});

const restApi = new apigw.RestApi(stack, 'myrestapi', {
defaultMethodOptions: {
authorizer: auth
}
});
restApi.root.addMethod('ANY');

expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', {
Name: 'myauthorizer1',
Type: 'TOKEN',
RestApiId: stack.resolve(restApi.restApiId)
}));

test.done();
},

'fails when authorization type does not match the authorizer'(test: Test) {
const stack = new cdk.Stack();

const restApi = new apigw.RestApi(stack, 'myrestapi');

test.throws(() => {
restApi.root.addMethod('ANY', undefined, {
authorizationType: apigw.AuthorizationType.IAM,
authorizer: DUMMY_AUTHORIZER
});
}, /Authorization type is set to AWS_IAM which is different from what is required by the authorizer/);

test.done();
},

'fails when authorization type does not match the authorizer in default method options'(test: Test) {
const stack = new cdk.Stack();

const restApi = new apigw.RestApi(stack, 'myrestapi', {
defaultMethodOptions: {
authorizer: DUMMY_AUTHORIZER
}
});

test.throws(() => {
restApi.root.addMethod('ANY', undefined, {
authorizationType: apigw.AuthorizationType.NONE,
});
}, /Authorization type is set to NONE which is different from what is required by the authorizer/);

test.done();
}
};

0 comments on commit 5c16744

Please sign in to comment.