Skip to content

Commit

Permalink
feat(appsync): Lambda Authorizer for AppSync GraphqlApi (#16743)
Browse files Browse the repository at this point in the history
Adds the relatively new [Lambda Authorizer for GraphqlApi](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/).

Closes: #16380. 

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
kaizencc authored and njlynch committed Oct 11, 2021
1 parent 00d99f3 commit dcad780
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 1 deletion.
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;
}

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

0 comments on commit dcad780

Please sign in to comment.