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: {
functionArn: authFunction.functionArn,
// can also specify `resultsCacheTtl` and `validationRegex`.
},
},
},
});
```

## Permissions

When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role
Expand Down
55 changes: 55 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export enum AuthorizationType {
* OpenID Connect authorization type
*/
OIDC = 'OPENID_CONNECT',
/**
* Lambda authorization type
*/
LAMBDA = 'AWS_LAMBDA',
}

/**
Expand Down Expand Up @@ -58,6 +62,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 +159,38 @@ export interface OpenIdConnectConfig {
readonly oidcProvider: string;
}

/**
* Configuration for Lambda authorization in AppSync
*/
export interface lambdaAuthorizerConfig {
/**
* The ARN for the authorizer lambda function. This may be a standard Lambda ARN, a version ARN (.../v3) or alias ARN.
* 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 functionArn: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is most similar to what CFN wants, which is an authorizerUri. However, I feel like it is a CDK pattern to ask for readonly handler: lambda.IFunction instead and then generate the ARN under the hood. Wanted to bring it up to make sure a second pair of eyes looks at this decision.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it's even in the design guidelines:

Use strong types (and specifically, construct interfaces) instead of physical attributes when referencing other resources. For example, instead of keyArn, use kms.IKey [awslint:props-no-arn-refs].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see. I'll reread the design guidelines and change this up to be IFunction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@otaviomacedo, I made the change, so this should be ready for primetime whenever you have a chance.


/**
* 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 +459,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 @@ -497,6 +539,9 @@ export class GraphqlApi extends GraphqlApiBase {
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 +596,23 @@ export class GraphqlApi extends GraphqlApiBase {
};
}

private setupLambdaAuthorizerConfig(config?: lambdaAuthorizerConfig) {
if (!config) return undefined;
return {
authorizerResultTtlInSeconds: config.resultsCacheTtl?.toSeconds(),
authorizerUri: config.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
215 changes: 215 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,217 @@ 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: {
functionArn: fn.functionArn,
},
},
},
});

// 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: {
functionArn: fn.functionArn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'abc',
},
},
},
});

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

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: {
functionArn: fn.functionArn,
},
}],
},
});

// 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: {
functionArn: fn.functionArn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'abc',
},
}],
},
});

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

test('Lambda authorization configurable in with multiple 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: {
functionArn: fn.functionArn,
},
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
functionArn: fn.functionArn,
resultsCacheTtl: cdk.Duration.seconds(300),
validationRegex: 'abc',
},
},
{
authorizationType: appsync.AuthorizationType.LAMBDA,
lambdaAuthorizerConfig: {
functionArn: fn.functionArn,
resultsCacheTtl: cdk.Duration.minutes(0),
validationRegex: 'abc',
},
},
],
},
});

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