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(appsync): Lambda Authorizer for AppSync GraphqlApi #16743

Merged
merged 10 commits into from
Oct 6, 2021
36 changes: 35 additions & 1 deletion packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ APIs that use GraphQL.

### DynamoDB

Example of a GraphQL API with `AWS_IAM` authorization resolving into a DynamoDb
Example of a GraphQL API with `AWS_IAM` [authorization](#authorization) resolving into a DynamoDb
backend data source.

GraphQL schema file `schema.graphql`:
Expand Down Expand Up @@ -345,6 +345,40 @@ If you don't specify `graphqlArn` in `fromXxxAttributes`, CDK will autogenerate
the expected `arn` for the imported api, given the `apiId`. For creating data
sources and resolvers, an `apiId` is sufficient.

## Authorization

There are multiple authorization types available for GraphQL API to cater to different
access use cases. They are:

- API Keys (`AuthorizationType.API_KEY`)
- Amazon Cognito User Pools (`AuthorizationType.USER_POOL`)
- OpenID Connect (`AuthorizationType.OPENID_CONNECT`)
- AWS Identity and Access Management (`AuthorizationType.AWS_IAM`)
- AWS Lambda (`AuthorizationType.AWS_LAMBDA`)

These types can be used simultaneously in a single API, allowing different types of clients to
access data. When you specify an authorization type, you can also specify the corresponding
authorization mode to finish defining your authorization. For example, this is a GraphQL API
with AWS Lambda Authorization.

```ts
authFunction = new lambda.Function(stack, 'auth-function', {});

new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: authFunction,
// can also specify `resultsCacheTtl` and `validationRegex`.
},
},
},
});
```

## Permissions

When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role
Expand Down
59 changes: 59 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IUserPool } from '@aws-cdk/aws-cognito';
import { ManagedPolicy, Role, IRole, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam';
import { IFunction } from '@aws-cdk/aws-lambda';
import { CfnResource, Duration, Expiration, IResolvable, Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated';
Expand Down Expand Up @@ -29,6 +30,10 @@ export enum AuthorizationType {
* OpenID Connect authorization type
*/
OIDC = 'OPENID_CONNECT',
/**
* Lambda authorization type
*/
LAMBDA = 'AWS_LAMBDA',
}

/**
Expand Down Expand Up @@ -58,6 +63,11 @@ export interface AuthorizationMode {
* @default - none
*/
readonly openIdConnectConfig?: OpenIdConnectConfig;
/**
* If authorizationType is `AuthorizationType.LAMBDA`, this option is required.
* @default - none
*/
readonly lambdaAuthorizerConfig?: LambdaAuthorizerConfig;
}

/**
Expand Down Expand Up @@ -150,6 +160,38 @@ export interface OpenIdConnectConfig {
readonly oidcProvider: string;
}

/**
* Configuration for Lambda authorization in AppSync. Note that you can only have a single AWS Lambda function configured to authorize your API.
*/
export interface LambdaAuthorizerConfig {
/**
* The authorizer lambda function.
* Note: This Lambda function must have the following resource-based policy assigned to it.
* When configuring Lambda authorizers in the console, this is done for you.
* To do so with the AWS CLI, run the following:
*
* `aws lambda add-permission --function-name "arn:aws:lambda:us-east-2:111122223333:function:my-function" --statement-id "appsync" --principal appsync.amazonaws.com --action lambda:InvokeFunction`
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html
*/
readonly handler: IFunction;

/**
* How long the results are cached.
* Disable caching by setting this to 0.
*
* @default Duration.minutes(5)
*/
readonly resultsCacheTtl?: Duration;

/**
* A regular expression for validation of tokens before the Lambda function is called.
*
* @default - no regex filter will be applied.
*/
readonly validationRegex?: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also accept RegEx here? (and convert to .source underneath, checking what to do with flags)

}

/**
* Configuration of the API authorization modes.
*/
Expand Down Expand Up @@ -418,6 +460,7 @@ export class GraphqlApi extends GraphqlApiBase {
logConfig: this.setupLogConfig(props.logConfig),
openIdConnectConfig: this.setupOpenIdConnectConfig(defaultMode.openIdConnectConfig),
userPoolConfig: this.setupUserPoolConfig(defaultMode.userPoolConfig),
lambdaAuthorizerConfig: this.setupLambdaAuthorizerConfig(defaultMode.lambdaAuthorizerConfig),
additionalAuthenticationProviders: this.setupAdditionalAuthorizationModes(additionalModes),
xrayEnabled: props.xrayEnabled,
});
Expand Down Expand Up @@ -490,13 +533,19 @@ export class GraphqlApi extends GraphqlApiBase {
}

private validateAuthorizationProps(modes: AuthorizationMode[]) {
if (modes.filter((mode) => mode.authorizationType === AuthorizationType.LAMBDA).length > 1) {
throw new Error('You can only have a single AWS Lambda function configured to authorize your API.');
}
modes.map((mode) => {
if (mode.authorizationType === AuthorizationType.OIDC && !mode.openIdConnectConfig) {
throw new Error('Missing OIDC Configuration');
}
if (mode.authorizationType === AuthorizationType.USER_POOL && !mode.userPoolConfig) {
throw new Error('Missing User Pool Configuration');
}
if (mode.authorizationType === AuthorizationType.LAMBDA && !mode.lambdaAuthorizerConfig) {
throw new Error('Missing Lambda Configuration');
}
});
if (modes.filter((mode) => mode.authorizationType === AuthorizationType.API_KEY).length > 1) {
throw new Error('You can\'t duplicate API_KEY configuration. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html');
Expand Down Expand Up @@ -551,13 +600,23 @@ export class GraphqlApi extends GraphqlApiBase {
};
}

private setupLambdaAuthorizerConfig(config?: LambdaAuthorizerConfig) {
if (!config) return undefined;
return {
authorizerResultTtlInSeconds: config.resultsCacheTtl?.toSeconds(),
authorizerUri: config.handler.functionArn,
identityValidationExpression: config.validationRegex,
};
}

private setupAdditionalAuthorizationModes(modes?: AuthorizationMode[]) {
if (!modes || modes.length === 0) return undefined;
return modes.reduce<CfnGraphQLApi.AdditionalAuthenticationProviderProperty[]>((acc, mode) => [
...acc, {
authenticationType: mode.authorizationType,
userPoolConfig: this.setupUserPoolConfig(mode.userPoolConfig),
openIdConnectConfig: this.setupOpenIdConnectConfig(mode.openIdConnectConfig),
lambdaAuthorizerConfig: this.setupLambdaAuthorizerConfig(mode.lambdaAuthorizerConfig),
},
], []);
}
Expand Down
204 changes: 204 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import { Template } from '@aws-cdk/assertions';
import * as cognito from '@aws-cdk/aws-cognito';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as appsync from '../lib';

Expand Down Expand Up @@ -630,3 +631,206 @@ describe('AppSync OIDC Authorization', () => {
});
});
});

describe('AppSync Lambda Authorization', () => {
let fn: lambda.Function;
beforeEach(() => {
fn = new lambda.Function(stack, 'auth-function', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromInline('/* lambda authentication code here.*/'),
});
});

test('Lambda authorization configurable in default authorization has default configuration', () => {
// WHEN
new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
},
},
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', {
AuthenticationType: 'AWS_LAMBDA',
LambdaAuthorizerConfig: {
AuthorizerUri: {
'Fn::GetAtt': [
'authfunction96361832',
'Arn',
],
},
},
});
});

test('Lambda authorization configurable in default authorization', () => {
// WHEN
new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'custom-.*',
},
},
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', {
AuthenticationType: 'AWS_LAMBDA',
LambdaAuthorizerConfig: {
AuthorizerUri: {
'Fn::GetAtt': [
'authfunction96361832',
'Arn',
],
},
AuthorizerResultTtlInSeconds: 300,
IdentityValidationExpression: 'custom-.*',
},
});
});

test('Lambda authorization configurable in additional authorization has default configuration', () => {
// WHEN
new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
additionalAuthorizationModes: [{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
},
}],
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', {
AdditionalAuthenticationProviders: [{
AuthenticationType: 'AWS_LAMBDA',
LambdaAuthorizerConfig: {
AuthorizerUri: {
'Fn::GetAtt': [
'authfunction96361832',
'Arn',
],
},
},
}],
});
});

test('Lambda authorization configurable in additional authorization', () => {
// WHEN
new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
additionalAuthorizationModes: [{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'custom-.*',
},
}],
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', {
AdditionalAuthenticationProviders: [{
AuthenticationType: 'AWS_LAMBDA',
LambdaAuthorizerConfig: {
AuthorizerUri: {
'Fn::GetAtt': [
'authfunction96361832',
'Arn',
],
},
AuthorizerResultTtlInSeconds: 300,
IdentityValidationExpression: 'custom-.*',
},
}],
});
});

test('Lambda authorization throws with multiple lambda authorization', () => {
expect(() => new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
},
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'custom-.*',
},
},
],
},
})).toThrow('You can only have a single AWS Lambda function configured to authorize your API.');

expect(() => new appsync.GraphqlApi(stack, 'api2', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: { authorizationType: appsync.AuthorizationType.IAM },
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'custom-.*',
},
},
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
handler: fn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'custom-.*',
},
},
],
},
})).toThrow('You can only have a single AWS Lambda function configured to authorize your API.');
});

test('throws if authorization type and mode do not match', () => {
expect(() => new appsync.GraphqlApi(stack, 'api', {
name: 'api',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.LAMBDA,
openIdConnectConfig: { oidcProvider: 'test' },
},
},
})).toThrow('Missing Lambda Configuration');
});
});