Skip to content

Commit fb6bc35

Browse files
authored
Merge pull request #13 from bennettsf/implement-cognito-userpool
Implement cognito userpool logic into the construct
2 parents 5feea52 + c02c12a commit fb6bc35

File tree

4 files changed

+160
-16
lines changed

4 files changed

+160
-16
lines changed

packages/rest-api-construct/API.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@
77
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
88
import { Construct } from 'constructs';
99
import { FunctionResources } from '@aws-amplify/backend/types/platform';
10+
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
1011
import { ResourceProvider } from '@aws-amplify/backend/types/platform';
1112

1213
// @public (undocumented)
1314
export type AuthorizerConfig = {
1415
type: 'none';
1516
} | {
1617
type: 'userPool';
17-
} | {
18-
type: 'userPool';
19-
groups: string[];
2018
};
2119

2220
// @public (undocumented)
@@ -28,9 +26,9 @@ export type MethodsProps = {
2826
authorizer?: AuthorizerConfig;
2927
};
3028

31-
// @public
29+
// @public (undocumented)
3230
export class RestApiConstruct extends Construct {
33-
constructor(scope: Construct, id: string, props: RestApiConstructProps);
31+
constructor(scope: Construct, id: string, props: RestApiConstructProps, userPool?: IUserPool);
3432
// (undocumented)
3533
readonly api: apiGateway.RestApi;
3634
}
@@ -39,6 +37,7 @@ export class RestApiConstruct extends Construct {
3937
export type RestApiConstructProps = {
4038
apiName: string;
4139
apiProps: RestApiPathConfig[];
40+
defaultAuthorizer?: AuthorizerConfig;
4241
};
4342

4443
// @public (undocumented)

packages/rest-api-construct/src/construct.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import assert from 'node:assert';
1313
import { RestApiPathConfig } from './types.js';
1414
import { handler } from './test-assets/handler.js';
1515
import { Context } from 'aws-lambda';
16+
import { Template } from 'aws-cdk-lib/assertions';
17+
import * as cognito from 'aws-cdk-lib/aws-cognito';
18+
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
1619

1720
const setupExampleLambda = (stack: Stack) => {
1821
const factory = defineFunction({
@@ -61,6 +64,84 @@ const constructApiWithPath = (path: string, n: number = 1) => {
6164
});
6265
};
6366

67+
void describe('RestApiConstruct User Pool Handling', () => {
68+
void it('attaches a Cognito authorizer when a user pool is provided', () => {
69+
const app = new App();
70+
const stack = new Stack(app);
71+
72+
// Create a test Cognito user pool
73+
const userPool = new cognito.UserPool(stack, 'TestUserPool', {
74+
userPoolName: 'test-pool',
75+
});
76+
77+
// Setup a Lambda to attach to the API
78+
const lambdaResource = setupExampleLambda(stack);
79+
80+
// Create the API with a GET method using user pool authorization
81+
new RestApiConstruct(
82+
stack,
83+
'RestApiWithAuth',
84+
{
85+
apiName: 'RestApiWithAuth',
86+
apiProps: [
87+
{
88+
path: '/test',
89+
lambdaEntry: lambdaResource,
90+
methods: [
91+
{ method: 'GET', authorizer: { type: 'userPool' } },
92+
{ method: 'POST', authorizer: { type: 'userPool' } },
93+
],
94+
},
95+
],
96+
},
97+
userPool,
98+
);
99+
100+
const template = Template.fromStack(stack); // <-- use Template here
101+
102+
// Check that the GET method has the proper Cognito user pool auth
103+
template.hasResourceProperties('AWS::ApiGateway::Method', {
104+
AuthorizationType: apiGateway.AuthorizationType.COGNITO, // this is COGNITO_USER_POOLS
105+
HttpMethod: 'GET',
106+
});
107+
108+
// Check that the POST method also has the proper Cognito user pool auth
109+
template.hasResourceProperties('AWS::ApiGateway::Method', {
110+
AuthorizationType: apiGateway.AuthorizationType.COGNITO,
111+
HttpMethod: 'POST',
112+
});
113+
114+
// Optionally, check that authorizer is correctly attached
115+
template.hasResourceProperties('AWS::ApiGateway::Method', {
116+
AuthorizerId: { Ref: 'RestApiWithAuthDefaultUserPoolAuth0006E5FA' }, // CDK auto-generated ID
117+
});
118+
});
119+
120+
void it('does not create an authorizer if no user pool is passed', () => {
121+
const stack = new Stack(new App());
122+
const lambdaResource = setupExampleLambda(stack);
123+
124+
new RestApiConstruct(stack, 'RestApiWithoutAuth', {
125+
apiName: 'RestApiWithoutAuth',
126+
apiProps: [
127+
{
128+
path: '/test',
129+
lambdaEntry: lambdaResource,
130+
methods: [{ method: 'GET', authorizer: { type: 'none' } }],
131+
},
132+
],
133+
});
134+
135+
const template = Template.fromStack(stack); // <-- use Template here
136+
137+
// GET method should be open (no auth)
138+
template.hasResourceProperties('AWS::ApiGateway::Method', {
139+
AuthorizationType: apiGateway.AuthorizationType.NONE,
140+
HttpMethod: 'GET',
141+
});
142+
});
143+
});
144+
64145
void describe('RestApiConstruct Lambda Handling', () => {
65146
void it('integrates the result of defineFunction into the api', () => {
66147
const app = new App();

packages/rest-api-construct/src/construct.ts

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,105 @@
11
import { Construct } from 'constructs';
22
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
3-
import { RestApiConstructProps } from './types.js';
3+
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
4+
import { MethodsProps, RestApiConstructProps } from './types.js';
45
import { validateRestApiPaths } from './validate_paths.js';
56

67
/**
7-
* Rest API construct for Amplify Backend
8+
*
89
*/
910
export class RestApiConstruct extends Construct {
1011
public readonly api: apiGateway.RestApi;
12+
private readonly userPoolAuthorizer?: apiGateway.CognitoUserPoolsAuthorizer;
13+
1114
/**
12-
* Create a new RestApiConstruct
15+
* Creates a new REST API in API Gateway with optional Cognito User Pool authorization.
16+
*
17+
* If a user pool is provided, all routes default to using it for authentication unless overridden.
18+
* @param scope - The scope in which this construct is defined.
19+
* @param id - The unique identifier for this construct.
20+
* @param props - Configuration options for the REST API, including name and route definitions.
21+
* @param userPool - Optional Cognito User Pool to use as the default authorizer.
1322
*/
14-
constructor(scope: Construct, id: string, props: RestApiConstructProps) {
23+
constructor(
24+
scope: Construct,
25+
id: string,
26+
props: RestApiConstructProps,
27+
userPool?: IUserPool,
28+
) {
1529
super(scope, id);
1630

1731
//check that the paths are valid before creating the API
1832
const paths: string[] = [];
1933
props.apiProps.forEach((value) => paths.push(value.path));
2034
validateRestApiPaths(paths);
2135

22-
// Create a new API Gateway REST API with the specified name
36+
// Create API
2337
this.api = new apiGateway.RestApi(this, 'RestApi', {
2438
restApiName: props.apiName,
2539
});
2640

27-
// Iterate over each path configuration
41+
// If userPool exists, create default authorizer
42+
if (userPool) {
43+
this.userPoolAuthorizer = new apiGateway.CognitoUserPoolsAuthorizer(
44+
this,
45+
'DefaultUserPoolAuth',
46+
{
47+
cognitoUserPools: [userPool],
48+
},
49+
);
50+
}
51+
2852
for (const pathConfig of props.apiProps) {
2953
const { path, methods, lambdaEntry } = pathConfig;
3054
// Add resource and methods for this route
3155
const resource = this.addNestedResource(this.api.root, path);
56+
3257
for (const method of methods) {
33-
resource.addMethod(
34-
method.method,
35-
new apiGateway.LambdaIntegration(lambdaEntry.resources.lambda),
58+
const integration = new apiGateway.LambdaIntegration(
59+
lambdaEntry.resources.lambda,
3660
);
61+
62+
resource.addMethod(method.method, integration, {
63+
authorizer: this.getAuthorizerForMethod(method),
64+
authorizationType: this.getAuthorizationType(method),
65+
});
3766
}
3867
}
3968
}
4069

70+
/**
71+
*
72+
* If the method specifies a user pool authorizer, it returns the default user pool authorizer.
73+
* If no authorizer is specified but a user pool exists, it returns the default user pool authorizer.
74+
* Otherwise, it returns undefined.
75+
*/
76+
77+
private getAuthorizerForMethod(method: MethodsProps) {
78+
if (method.authorizer?.type === 'userPool' && this.userPoolAuthorizer) {
79+
return this.userPoolAuthorizer;
80+
}
81+
if (!method.authorizer && this.userPoolAuthorizer) {
82+
return this.userPoolAuthorizer;
83+
}
84+
return undefined;
85+
}
86+
87+
/**
88+
* Determines the authorization type based on the method's authorizer configuration.
89+
* If a user pool authorizer is set or if no authorizer is specified but a user pool exists, it returns COGNITO.
90+
* Otherwise, it returns NONE.
91+
*/
92+
93+
private getAuthorizationType(method: MethodsProps) {
94+
if (
95+
method.authorizer?.type === 'userPool' ||
96+
(!method.authorizer && this.userPoolAuthorizer)
97+
) {
98+
return apiGateway.AuthorizationType.COGNITO;
99+
}
100+
return apiGateway.AuthorizationType.NONE;
101+
}
102+
41103
/**
42104
* Adds nested resources to the API based on the provided path.
43105
*/

packages/rest-api-construct/src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import {
66
export type RestApiConstructProps = {
77
apiName: string;
88
apiProps: RestApiPathConfig[];
9+
defaultAuthorizer?: AuthorizerConfig;
910
};
1011

1112
export type AuthorizerConfig =
1213
| { type: 'none' } // public
13-
| { type: 'userPool' } // signed-in users
14-
| { type: 'userPool'; groups: string[] }; // signed-in + group restriction
14+
| { type: 'userPool' }; // signed-in users
15+
// TODO: Group Validation
16+
// | { type: 'userPool', groups: string[] } // signed-in users in a group
1517

1618
export type MethodsProps = {
1719
method: HttpMethod;

0 commit comments

Comments
 (0)