Skip to content

Commit

Permalink
feat(apigateway): add lambda request authorizer construct
Browse files Browse the repository at this point in the history
This creates a common LambdaAuthorizer base class so that the
token and request authorizers can share common functionality.
  • Loading branch information
Adam Plumer committed Jan 17, 2020
1 parent 6969562 commit 9616f63
Show file tree
Hide file tree
Showing 9 changed files with 1,011 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ cdk.out/

# Yarn error log
yarn-error.log

.nycrc
45 changes: 42 additions & 3 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,8 @@ API Gateway interacts with the authorizer Lambda function handler by passing inp
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.
See [here](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:

Expand All @@ -397,6 +397,44 @@ Authorizers can also be passed via the `defaultMethodOptions` property within th
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.

#### Lambda-based request authorizer

This module provides support for request-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 request-based Lambda authorizer (also called a request authorizer) receives the caller's identity in a series of values pulled from
the request, from the headers, query strings, etc.

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 body of the request 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 [here](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 request-based Lambda authorizer to the 'GET' Method of the Book resource:

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

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

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

By default, the `RequestAuthorizer` does not pass any kind of information from the request. This can,
however, be modified by changing the `identitySource` property, and is required when specifying a value for caching.

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
Expand Down Expand Up @@ -539,7 +577,8 @@ running at one origin, access to selected resources from a different origin. A
web application executes a cross-origin HTTP request when it requests a resource
that has a different origin (domain, protocol, or port) from its own.

You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource.
You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS
HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource.

The following example will enable CORS for all methods and all origins on all resources of the API:

Expand Down
149 changes: 105 additions & 44 deletions packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { CfnAuthorizer } from '../apigateway.generated';
import { Authorizer, IAuthorizer } from '../authorizer';
import { RestApi } from '../restapi';

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

export interface LambdaAuthorizerProps {
/**
* An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer.
*
Expand All @@ -27,14 +23,6 @@ export interface TokenAuthorizerProps {
*/
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.
Expand All @@ -43,14 +31,6 @@ export interface TokenAuthorizerProps {
*/
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'.
Expand All @@ -60,14 +40,7 @@ export interface TokenAuthorizerProps {
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 {
abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer {

/**
* The id of the authorizer.
Expand All @@ -77,12 +50,13 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer {

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

private restApiId?: string;
protected restApiId?: string;

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

if (props.resultsCacheTtl && props.resultsCacheTtl.toSeconds() > 3600) {
Expand All @@ -91,18 +65,7 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer {

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

const resource = new CfnAuthorizer(this, 'Resource', {
name: props.authorizerName ?? this.node.uniqueId,
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.authorizerId = this.getResource(props).ref;

this.authorizerArn = Stack.of(this).formatArn({
service: 'execute-api',
Expand Down Expand Up @@ -138,4 +101,102 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer {

this.restApiId = restApi.restApiId;
}
}

protected abstract getResource<T extends LambdaAuthorizerProps>(props: T): CfnAuthorizer;
}

/**
* Properties for TokenAuthorizer
*/
export interface TokenAuthorizerProps extends LambdaAuthorizerProps {
/**
* 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;

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

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

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

protected getResource(props: TokenAuthorizerProps): CfnAuthorizer {
const restApiId = Lazy.stringValue({ produce: () => this.restApiId });

return new CfnAuthorizer(this, 'Resource', {
name: props.authorizerName ?? this.node.uniqueId,
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,
});
}
}

/**
* Properties for RequestAuthorizerProps
*/
export interface RequestAuthorizerProps extends LambdaAuthorizerProps {
/**
* An array of request header mapping expressions for identities. 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 no identity sources
*/
readonly identitySource?: string[];
}

/**
* Request-based lambda authorizer that recognizes the caller's identity via request parameters,
* such as headers, paths, query strings, stage variables, or context variables.
* Based on the request, authorization is performed by a lambda function.
*
* @resource AWS::ApiGateway::Authorizer
*/
export class RequestAuthorizer extends LambdaAuthorizer {

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

if (props.resultsCacheTtl && props.identitySource?.length === 0) {
throw new Error(`At least one Identity Source is required for a REQUEST-based Lambda authorizer.`);
}
}

protected getResource(props: RequestAuthorizerProps): CfnAuthorizer {
const restApiId = Lazy.stringValue({ produce: () => this.restApiId });

return new CfnAuthorizer(this, 'Resource', {
name: props.authorizerName ?? this.node.uniqueId,
restApiId,
type: 'REQUEST',
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?.join(','),
});
}
}
Loading

0 comments on commit 9616f63

Please sign in to comment.