Skip to content

Commit

Permalink
feat(appsync): allow configuring API key authorization mode
Browse files Browse the repository at this point in the history
By default, the AppSync L2 constructs use API key authorization, but it
doesn't allow configuring the API key.

Fix that by allowing a default authorization mode to be specified.
Currently, the supported modes are Cognito user pools and API keys. When
specifying API key authorization, allow configuring it.

BREAKING CHANGE:
    Configuration the user pool authorization is now done through the
    authorizationConfig property. This allows us to specify a default
    authorization mode out of the supported ones, currently limited to
    Cognito user pools and API keys.

Fixes aws#6246

Signed-off-by: Duarte Nunes <duarte@uma.ni>
  • Loading branch information
duarten committed Feb 13, 2020
1 parent fb3fc48 commit 8db2d68
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 23 deletions.
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ export class ApiStack extends Stack {
logConfig: {
fieldLogLevel: FieldLogLevel.ALL,
},
userPoolConfig: {
userPool,
defaultAction: UserPoolDefaultAction.ALLOW,
authorizationConfig: {
defaultAuthorization: {
userPool,
defaultAction: UserPoolDefaultAction.ALLOW,
},
},
schemaDefinitionFile: './schema.graphql',
});
Expand Down
103 changes: 83 additions & 20 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { IUserPool } from "@aws-cdk/aws-cognito";
import { Table } from '@aws-cdk/aws-dynamodb';
import { IGrantable, IPrincipal, IRole, ManagedPolicy, Role, ServicePrincipal } from "@aws-cdk/aws-iam";
import { IFunction } from "@aws-cdk/aws-lambda";
import { Construct, IResolvable } from "@aws-cdk/core";
import { Construct, Duration, IResolvable } from "@aws-cdk/core";
import { readFileSync } from "fs";
import { CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from "./appsync.generated";
import { CfnApiKey, CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from "./appsync.generated";

/**
* enum with all possible values for Cognito user-pool default actions
Expand Down Expand Up @@ -43,6 +43,45 @@ export interface UserPoolConfig {
readonly defaultAction?: UserPoolDefaultAction;
}

function isUserPoolConfig(obj: unknown): obj is UserPoolConfig {
return (obj as UserPoolConfig).userPool !== undefined;
}

/**
* Configuration for API Key authorization in AppSync
*/
export interface ApiKeyConfig {
/**
* Unique description of the API key.
*/
readonly apiKeyDesc: string;

/**
* Expiration time of the API key with a minimum of 1 day
* and a maximum of 365 days.
* @default - 7 days.
*/
readonly expires?: Duration;
}

function isApiKeyConfig(obj: unknown): obj is ApiKeyConfig {
return (obj as ApiKeyConfig).apiKeyDesc !== undefined;
}

type AuthModes = UserPoolConfig | ApiKeyConfig;

/**
* Marker interface for the different authorization modes.
*/
export interface AuthorizationConfig {
/**
* Optional authorization configuration
*
* @default - API Key authorization
*/
readonly defaultAuthorization?: AuthModes;
}

/**
* log-level for fields in AppSync
*/
Expand Down Expand Up @@ -90,11 +129,11 @@ export interface GraphQLApiProps {
readonly name: string;

/**
* Optional user pool authorizer configuration
* Optional authorization configuration
*
* @default - Do not use Cognito auth
* @default - API Key authorization
*/
readonly userPoolConfig?: UserPoolConfig;
readonly authorizationConfig?: AuthorizationConfig;

/**
* Logging configuration for this api
Expand Down Expand Up @@ -145,7 +184,6 @@ export class GraphQLApi extends Construct {
public readonly schema: CfnGraphQLSchema;

private api: CfnGraphQLApi;
private authenticationType: string;

constructor(scope: Construct, id: string, props: GraphQLApiProps) {
super(scope, id);
Expand All @@ -156,22 +194,9 @@ export class GraphQLApi extends Construct {
apiLogsRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs'));
}

if (props.userPoolConfig) {
this.authenticationType = 'AMAZON_COGNITO_USER_POOLS';
} else {
this.authenticationType = 'API_KEY';
}

this.api = new CfnGraphQLApi(this, 'Resource', {
name: props.name,
authenticationType: this.authenticationType,
...props.userPoolConfig && {
userPoolConfig: {
userPoolId: props.userPoolConfig.userPool.userPoolId,
awsRegion: props.userPoolConfig.userPool.stack.region,
defaultAction: props.userPoolConfig.defaultAction ? props.userPoolConfig.defaultAction.toString() : 'ALLOW',
},
},
authenticationType: 'API_KEY',
...props.logConfig && {
logConfig: {
cloudWatchLogsRoleArn: apiLogsRole ? apiLogsRole.roleArn : undefined,
Expand All @@ -186,6 +211,10 @@ export class GraphQLApi extends Construct {
this.graphQlUrl = this.api.attrGraphQlUrl;
this.name = this.api.name;

if (props.authorizationConfig) {
this.setupAuth(props.authorizationConfig);
}

let definition;
if (props.schemaDefinition) {
definition = props.schemaDefinition;
Expand Down Expand Up @@ -230,6 +259,40 @@ export class GraphQLApi extends Construct {
});
}

private setupAuth(auth: AuthorizationConfig) {
if (isUserPoolConfig(auth.defaultAuthorization)) {
const { authenticationType, userPoolConfig } = this.userPoolDescFrom(auth.defaultAuthorization);
this.api.authenticationType = authenticationType;
this.api.userPoolConfig = userPoolConfig;
} else if (isApiKeyConfig(auth.defaultAuthorization)) {
this.api.authenticationType = this.apiKeyDesc(auth.defaultAuthorization).authenticationType;
}
}

private userPoolDescFrom(upConfig: UserPoolConfig): { authenticationType: string; userPoolConfig: CfnGraphQLApi.UserPoolConfigProperty } {
return {
authenticationType: 'AMAZON_COGNITO_USER_POOLS',
userPoolConfig: {
appIdClientRegex: upConfig.appIdClientRegex,
userPoolId: upConfig.userPool.userPoolId,
awsRegion: upConfig.userPool.stack.region,
defaultAction: upConfig.defaultAction ? upConfig.defaultAction.toString() : 'ALLOW',
}
};
}

private apiKeyDesc(akConfig: ApiKeyConfig): { authenticationType: string } {
const expires = akConfig.expires || Duration.days(7);
if (expires.toDays() < 1 || expires.toDays() > 366) {
throw Error("API key expiration must be between 1 and 365 days.");
}
new CfnApiKey(this, `${akConfig.apiKeyDesc || ''}ApiKey`, {
expires: Math.round(Date.now() / 1000) + expires.toSeconds(),
description: akConfig.apiKeyDesc,
apiId: this.apiId,
});
return { authenticationType: 'API_KEY' };
}
}

/**
Expand Down

0 comments on commit 8db2d68

Please sign in to comment.