-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAPIGatewayWithCognitoUserPoolConstruct.ts
More file actions
380 lines (355 loc) · 12.3 KB
/
APIGatewayWithCognitoUserPoolConstruct.ts
File metadata and controls
380 lines (355 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import * as cognito from "aws-cdk-lib/aws-cognito";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as logs from "aws-cdk-lib/aws-logs";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as certificatemanager from "aws-cdk-lib/aws-certificatemanager";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import { Duration, RemovalPolicy } from "aws-cdk-lib";
/**
* !!! CRITICAL WARNING !!!
*
* COGNITO USER POOL REQUIRES APEX DOMAIN TO HAVE AN A RECORD FOR ADDING CUSTOM DOMAIN.
* ADD A PLACEHOLDER A RECORD OR YOUR FRONTEND DOMAIN TO THE HOSTED ZONE BEFORE DEPLOYING.
* OTHERWISE, USER POOL DOMAIN CREATION WILL FAIL AND CDK WILL ROLLBACK.
*
*/
export interface CognitoConfig {
selfSignUpEnabled?: boolean;
userVerification?: {
emailSubject: string;
emailBody: string;
emailStyle: cognito.VerificationEmailStyle;
};
passwordPolicy?: {
minLength?: number;
requireLowercase?: boolean;
requireUppercase?: boolean;
requireDigits?: boolean;
requireSymbols?: boolean;
};
callbackUrls: string[];
logoutUrls: string[];
userGroups?: { groupName: string }[];
flows?: {
authCodeGrant?: boolean;
implicitCodeGrant?: boolean;
};
}
/**
* Properties for configuring the APIGatewayConstruct with Cognito authentication.
*/
export interface APIGatewayWithCognitoUserPoolConstructProps
extends apigw.RestApiProps {
appName: string;
apiSubDomain: string;
authSubdomain: string;
domain: string;
hostedZoneId: string;
cognitoConfig?: CognitoConfig;
}
/**
* Creates an Edge-optimized REST API Gateway with custom domain, certificate,
* access logging, Route 53 alias record configuration, and Cognito User Pool authentication.
*/
export class APIGatewayWithCognitoUserPoolConstruct extends Construct {
public readonly restApi: apigw.RestApi;
public readonly authorizer: apigw.CognitoUserPoolsAuthorizer;
private readonly domain: string;
/**
* Constructs a new instance of the APIGatewayConstruct.
*/
constructor(
scope: Construct,
id: string,
props: APIGatewayWithCognitoUserPoolConstructProps
) {
super(scope, id);
/* Store domain for later use in other methods */
this.domain = props.domain;
/* Cognito User Pool setup */
const userPool = new cognito.UserPool(this, `${props.appName}-userpool`, {
userPoolName: `${props.appName}-userpool`,
deletionProtection: false,
removalPolicy: RemovalPolicy.DESTROY,
selfSignUpEnabled: props.cognitoConfig?.selfSignUpEnabled || false,
signInAliases: { username: true, email: true },
autoVerify: props.cognitoConfig?.userVerification
? { email: true }
: undefined,
userVerification: props.cognitoConfig?.userVerification,
passwordPolicy: props.cognitoConfig?.passwordPolicy || {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: false,
},
});
/* Add User Pool id to SSM */
new ssm.StringParameter(this, `${props.appName}-userpool-id`, {
parameterName: `/${props.appName}/userpool-id`,
stringValue: userPool.userPoolId,
});
/* Cognito User Pool Client */
const userPoolClient = new cognito.UserPoolClient(
this,
`${props.appName}-userpool-client`,
{
userPool,
authFlows: { userPassword: true, userSrp: true },
...(props.cognitoConfig?.callbackUrls &&
props.cognitoConfig.callbackUrls.length > 0 &&
props.cognitoConfig.logoutUrls &&
props.cognitoConfig.logoutUrls.length > 0
? {
oAuth: {
callbackUrls: props.cognitoConfig.callbackUrls,
logoutUrls: props.cognitoConfig.logoutUrls,
flows: {
authorizationCodeGrant:
props.cognitoConfig.flows?.authCodeGrant !== false,
implicitCodeGrant:
props.cognitoConfig.flows?.implicitCodeGrant || false,
},
scopes: [
cognito.OAuthScope.OPENID,
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.PROFILE,
],
},
}
: {}),
idTokenValidity: Duration.minutes(15),
accessTokenValidity: Duration.minutes(15),
refreshTokenValidity: Duration.days(1),
}
);
/* Store User Pool Client ID in SSM (required for registering new users / getting the idtokens of existing users) */
new ssm.StringParameter(this, `${props.appName}-userpool-client-id`, {
parameterName: `/${props.appName}/userpool-client-id`,
stringValue: userPoolClient.userPoolClientId,
});
/* Cognito User Pool Groups */
if (props.cognitoConfig?.userGroups) {
for (const group of props.cognitoConfig.userGroups) {
new cognito.CfnUserPoolGroup(this, `${group.groupName}-group`, {
groupName: group.groupName,
userPoolId: userPool.userPoolId,
});
}
}
/* Access Logging Setup */
const accessLogGroup = new logs.LogGroup(
this,
`${props.appName}-access-log-group`,
{
removalPolicy: RemovalPolicy.DESTROY,
}
);
/* API Gateway Setup */
this.restApi = new apigw.RestApi(this, `${props.appName}-api-gw`, {
restApiName: `${props.appName}-api-gw`,
endpointConfiguration: { types: [apigw.EndpointType.EDGE] },
defaultCorsPreflightOptions: {
allowOrigins: [
`https://${props.domain}`,
`https://www.${props.domain}`,
`https://*.${props.domain}`,
`http://localhost:3000` /* TO-DO: Remove this in production once it's not needed anymore */,
],
allowMethods: apigw.Cors.ALL_METHODS,
allowHeaders: [
"Content-Type",
"X-Amz-Date",
"Authorization",
"X-Api-Key",
"X-Amz-Security-Token",
],
},
deployOptions: {
loggingLevel: apigw.MethodLoggingLevel.INFO,
dataTraceEnabled: true,
metricsEnabled: true,
accessLogDestination: new apigw.LogGroupLogDestination(accessLogGroup),
accessLogFormat: apigw.AccessLogFormat.jsonWithStandardFields(),
description: `API Gateway for ${props.appName}`,
/* Rate limiting configuration to prevent abuse */
throttlingRateLimit: 5 /* 5 requests per second */,
throttlingBurstLimit: 10 /* 10 concurrent requests */,
},
cloudWatchRole: true,
cloudWatchRoleRemovalPolicy: RemovalPolicy.DESTROY,
});
/* API Gateway Cognito Authorizer */
this.authorizer = new apigw.CognitoUserPoolsAuthorizer(
this,
`${props.appName}-cognito-authorizer`,
{
cognitoUserPools: [userPool],
authorizerName: `${props.appName}-cognito-authorizer`,
identitySource: "method.request.header.Authorization",
}
);
/* Attach authorizer to the REST API */
this.authorizer._attachToApi(this.restApi);
/* Custom Domain Configuration */
const hostedZone = route53.HostedZone.fromHostedZoneId(
this,
`${props.appName}-hosted-zone`,
props.hostedZoneId
);
const certificate = new certificatemanager.Certificate(
this,
`${props.appName}-api-certificate`,
{
domainName: `${props.apiSubDomain}.${props.domain}`,
validation:
certificatemanager.CertificateValidation.fromDns(hostedZone),
}
);
const domainName = new apigw.DomainName(
this,
`${props.appName}-domain-name`,
{
domainName: `${props.apiSubDomain}.${props.domain}`,
certificate,
endpointType: apigw.EndpointType.EDGE,
securityPolicy: apigw.SecurityPolicy.TLS_1_2,
mapping: this.restApi,
}
);
/* Route 53 Alias Record */
new route53.ARecord(this, `${props.appName}-alias-record`, {
zone: route53.HostedZone.fromHostedZoneAttributes(
this,
`${props.appName}-hosted-zone-attributes-for-api`,
{
hostedZoneId: props.hostedZoneId,
zoneName: props.domain,
}
),
recordName: `${props.apiSubDomain}.${props.domain}`,
target: route53.RecordTarget.fromAlias(
new targets.ApiGatewayDomain(domainName)
),
ttl: Duration.minutes(5),
});
/* Hosted UI Custom Domain Certificate */
const hostedUICertificate = new certificatemanager.Certificate(
this,
`${props.appName}-auth-certificate`,
{
domainName: `${props.authSubdomain}.${props.domain}`,
certificateName: `${props.appName}-auth-certificate`,
validation:
certificatemanager.CertificateValidation.fromDns(hostedZone),
}
);
/* Cognito User Pool Domain with Custom Domain */
const userPoolDomain = new cognito.UserPoolDomain(
this,
`${props.appName}-userpool-domain`,
{
userPool,
customDomain: {
domainName: `${props.authSubdomain}.${props.domain}`,
certificate: hostedUICertificate,
},
}
);
/* Route 53 Alias Record for Hosted UI */
new route53.ARecord(this, `${props.appName}-auth-alias-record`, {
zone: route53.HostedZone.fromHostedZoneAttributes(
this,
`${props.appName}-hosted-zone-attributes-for-auth`,
{
hostedZoneId: props.hostedZoneId,
zoneName: props.domain,
}
),
recordName: `${props.authSubdomain}.${props.domain}`,
target: route53.RecordTarget.fromAlias(
new targets.UserPoolDomainTarget(userPoolDomain)
),
ttl: Duration.minutes(5),
});
}
/**
* Adds a new method to the specified API Gateway resource path, integrating it with a Lambda function and optional Cognito authorization.
*
* @param {string} resourcePath - The resource path for the API Gateway endpoint (e.g., '/v1/test').
* @param {string} httpMethod - The HTTP method for the endpoint (e.g., 'GET', 'POST').
* @param {lambda.Function} lambdaFunction - The Lambda function to integrate with the API Gateway method.
* @param {boolean} [isProtected=false] - Whether the endpoint should be protected by Cognito authorization.
*
* @example
* // Adds a protected GET endpoint at '/v1/test' integrated with helloFunction Lambda
* apiGateway.addMethod('/v1/test', 'GET', helloFunction, true);
*
* // Adds an unprotected POST endpoint at '/v1/test2' integrated with helloFunction Lambda
* apiGateway.addMethod('/v1/test2', 'POST', helloFunction, false);
*/
public addMethod(
resourcePath: string,
httpMethod: string,
lambdaFunction: lambda.Function,
isProtected: boolean = false
) {
let resource = this.restApi.root;
const pathParts = resourcePath.split("/").filter(Boolean);
pathParts.forEach((part) => {
resource = resource.getResource(part) ?? resource.addResource(part);
});
/* Enable CORS for this resource if not already enabled */
if (!resource.defaultCorsPreflightOptions) {
resource.addCorsPreflight({
allowOrigins: [
`https://${this.domain}`,
`https://www.${this.domain}`,
`https://*.${this.domain}`,
],
allowMethods: apigw.Cors.ALL_METHODS,
allowHeaders: [
"Content-Type",
"X-Amz-Date",
"Authorization",
"X-Api-Key",
"X-Amz-Security-Token",
],
});
}
resource.addMethod(
httpMethod,
new apigw.LambdaIntegration(lambdaFunction, {
proxy: true,
/* Ensure Lambda returns proper CORS headers */
integrationResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Origin": "'*'",
},
},
],
}),
{
authorizer: isProtected ? this.authorizer : undefined,
authorizationType: isProtected
? apigw.AuthorizationType.COGNITO
: apigw.AuthorizationType.NONE,
/* Configure method response to include CORS headers */
methodResponses: [
{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Origin": true,
},
},
],
}
);
}
}