Skip to content

Commit be46b6d

Browse files
authored
Merge pull request #133 from vlewin/add_authorizer_support
Enable API Gateway authorizers
2 parents 366a841 + cb8d515 commit be46b6d

File tree

9 files changed

+997
-312
lines changed

9 files changed

+997
-312
lines changed

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ stepFunctions:
256256
```
257257

258258
Configuring the cors property sets Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods,Access-Control-Allow-Credentials headers in the CORS preflight response.
259-
260259
To enable the Access-Control-Max-Age preflight response header, set the maxAge property in the cors object:
261260

262261
```yml
@@ -272,6 +271,73 @@ stepFunctions:
272271
maxAge: 86400
273272
```
274273

274+
#### HTTP Endpoints with AWS_IAM Authorizers
275+
276+
If you want to require that the caller submit the IAM user's access keys in order to be authenticated to invoke your Lambda Function, set the authorizer to AWS_IAM as shown in the following example:
277+
278+
```yml
279+
stepFunctions:
280+
stateMachines:
281+
hello:
282+
events:
283+
- http:
284+
path: posts/create
285+
method: POST
286+
authorizer: aws_iam
287+
definition:
288+
```
289+
290+
#### HTTP Endpoints with Custom Authorizers
291+
292+
[Custom Authorizers](https://serverless.com/framework/docs/providers/aws/events/apigateway/#http-endpoints-with-custom-authorizers) allow you to run an AWS Lambda Function before your targeted AWS Lambda Function. This is useful for Microservice Architectures or when you simply want to do some Authorization before running your business logic.
293+
294+
You can enable Custom Authorizers for your HTTP endpoint by setting the Authorizer in your http event to another function in the same service, as shown in the following example:
295+
296+
```yml
297+
stepFunctions:
298+
stateMachines:
299+
hello:
300+
- http:
301+
path: posts/create
302+
method: post
303+
authorizer: authorizerFunc
304+
definition:
305+
```
306+
307+
If the Authorizer function does not exist in your service but exists in AWS, you can provide the ARN of the Lambda function instead of the function name, as shown in the following example:
308+
309+
```yml
310+
stepFunctions:
311+
stateMachines:
312+
hello:
313+
- http:
314+
path: posts/create
315+
method: post
316+
authorizer: xxx:xxx:Lambda-Name
317+
definition:
318+
```
319+
320+
### Share Authorizer
321+
322+
Auto-created Authorizer is convenient for conventional setup. However, when you need to define your custom Authorizer, or use COGNITO_USER_POOLS authorizer with shared API Gateway, it is painful because of AWS limitation. Sharing Authorizer is a better way to do.
323+
324+
```yml
325+
stepFunctions:
326+
stateMachines:
327+
createUser:
328+
...
329+
events:
330+
- http:
331+
path: /users
332+
...
333+
authorizer:
334+
# Provide both type and authorizerId
335+
type: COGNITO_USER_POOLS # TOKEN, CUSTOM or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
336+
authorizerId:
337+
Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID
338+
```
339+
340+
275341
#### Customizing request body mapping templates
276342

277343
The plugin generates default body mapping templates for `application/json` and `application/x-www-form-urlencoded` content types. If you'd like to add more content types or customize the default ones, you can do so by including them in `serverless.yml`:

lib/deploy/events/apiGateway/methods.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const BbPromise = require('bluebird');
44
const _ = require('lodash');
5+
const awsArnRegExs = require('../../../utils/arnRegularExpressions');
56

67
module.exports = {
78

@@ -26,7 +27,8 @@ module.exports = {
2627

2728
_.merge(template,
2829
this.getMethodIntegration(event.stateMachineName, stateMachineObj, event.http),
29-
this.getMethodResponses(event.http)
30+
this.getMethodResponses(event.http),
31+
this.getMethodAuthorization(event.http)
3032
);
3133

3234
const methodLogicalId = this.provider.naming
@@ -179,4 +181,51 @@ module.exports = {
179181

180182
return methodResponse;
181183
},
184+
185+
getMethodAuthorization(http) {
186+
if (_.get(http, 'authorizer.type') === 'AWS_IAM') {
187+
return {
188+
Properties: {
189+
AuthorizationType: 'AWS_IAM',
190+
},
191+
};
192+
}
193+
194+
if (http.authorizer) {
195+
if (http.authorizer.type && http.authorizer.authorizerId) {
196+
return {
197+
Properties: {
198+
AuthorizationType: http.authorizer.type,
199+
AuthorizerId: http.authorizer.authorizerId,
200+
},
201+
};
202+
}
203+
204+
const authorizerLogicalId = this.provider.naming
205+
.getAuthorizerLogicalId(http.authorizer.name || http.authorizer);
206+
207+
let authorizationType;
208+
const authorizerArn = http.authorizer.arn;
209+
if (typeof authorizerArn === 'string'
210+
&& awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) {
211+
authorizationType = 'COGNITO_USER_POOLS';
212+
} else {
213+
authorizationType = 'CUSTOM';
214+
}
215+
216+
return {
217+
Properties: {
218+
AuthorizationType: authorizationType,
219+
AuthorizerId: { Ref: authorizerLogicalId },
220+
},
221+
DependsOn: authorizerLogicalId,
222+
};
223+
}
224+
225+
return {
226+
Properties: {
227+
AuthorizationType: 'NONE',
228+
},
229+
};
230+
},
182231
};

lib/deploy/events/apiGateway/methods.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,75 @@ describe('#methods()', () => {
242242
.to.equal('\'*\'');
243243
});
244244
});
245+
246+
describe('#getMethodAuthorization()', () => {
247+
it('should return properties with AuthorizationType: NONE if no authorizer provided', () => {
248+
const event = {
249+
path: 'foo/bar1',
250+
method: 'post',
251+
};
252+
253+
expect(serverlessStepFunctions.getMethodAuthorization(event)
254+
.Properties.AuthorizationType).to.equal('NONE');
255+
});
256+
257+
it('should return resource properties with AuthorizationType: AWS_IAM', () => {
258+
const event = {
259+
authorizer: {
260+
type: 'AWS_IAM',
261+
authorizerId: 'foo12345',
262+
},
263+
};
264+
265+
expect(serverlessStepFunctions.getMethodAuthorization(event)
266+
.Properties.AuthorizationType).to.equal('AWS_IAM');
267+
});
268+
269+
it('should return properties with AuthorizationType: CUSTOM and authotizerId', () => {
270+
const event = {
271+
authorizer: {
272+
type: 'CUSTOM',
273+
authorizerId: 'foo12345',
274+
},
275+
};
276+
277+
expect(serverlessStepFunctions.getMethodAuthorization(event)
278+
.Properties.AuthorizationType).to.equal('CUSTOM');
279+
expect(serverlessStepFunctions.getMethodAuthorization(event)
280+
.Properties.AuthorizerId).to.equal('foo12345');
281+
});
282+
283+
it('should return properties with AuthorizationType: CUSTOM and resource reference', () => {
284+
const event = {
285+
authorizer: {
286+
name: 'authorizer',
287+
arn: { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] },
288+
resultTtlInSeconds: 300,
289+
identitySource: 'method.request.header.Authorization',
290+
},
291+
};
292+
293+
const autorization = serverlessStepFunctions.getMethodAuthorization(event);
294+
expect(autorization.Properties.AuthorizationType)
295+
.to.equal('CUSTOM');
296+
297+
expect(autorization.Properties.AuthorizerId)
298+
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' });
299+
});
300+
301+
it('should return properties with AuthorizationType: COGNITO_USER_POOLS', () => {
302+
const event = {
303+
authorizer: {
304+
name: 'authorizer',
305+
arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ',
306+
},
307+
};
308+
309+
const autorization = serverlessStepFunctions.getMethodAuthorization(event);
310+
expect(autorization.Properties.AuthorizationType)
311+
.to.equal('COGNITO_USER_POOLS');
312+
expect(autorization.Properties.AuthorizerId)
313+
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' });
314+
});
315+
});
245316
});

lib/deploy/events/apiGateway/validate.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const NOT_FOUND = -1;
33
const _ = require('lodash');
4+
const awsArnRegExs = require('../../../utils/arnRegularExpressions');
45

56
module.exports = {
67
httpValidate() {
@@ -15,6 +16,10 @@ module.exports = {
1516
http.path = this.getHttpPath(http, stateMachineName);
1617
http.method = this.getHttpMethod(http, stateMachineName);
1718

19+
if (http.authorizer) {
20+
http.authorizer = this.getAuthorizer(http, stateMachineName);
21+
}
22+
1823
if (http.cors) {
1924
http.cors = this.getCors(http);
2025

@@ -107,6 +112,123 @@ module.exports = {
107112
throw new this.serverless.classes.Error(errorMessage);
108113
},
109114

115+
getIntegration(http, stateMachineName) {
116+
if (http.integration) {
117+
// normalize the integration for further processing
118+
const normalizedIntegration = http.integration.toUpperCase().replace('-', '_');
119+
const allowedIntegrations = [
120+
'LAMBDA_PROXY', 'LAMBDA', 'AWS', 'AWS_PROXY', 'HTTP', 'HTTP_PROXY', 'MOCK',
121+
];
122+
123+
// check if the user has entered a non-valid integration
124+
if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) {
125+
const errorMessage = [
126+
`Invalid APIG integration "${http.integration}"`,
127+
` in function "${stateMachineName}".`,
128+
' Supported integrations are:',
129+
' lambda, lambda-proxy, aws, aws-proxy, http, http-proxy, mock.',
130+
].join('');
131+
throw new this.serverless.classes.Error(errorMessage);
132+
}
133+
if (normalizedIntegration === 'LAMBDA') {
134+
return 'AWS';
135+
} else if (normalizedIntegration === 'LAMBDA_PROXY') {
136+
return 'AWS_PROXY';
137+
}
138+
return normalizedIntegration;
139+
}
140+
return 'AWS_PROXY';
141+
},
142+
143+
getAuthorizer(http, functionName) {
144+
const authorizer = http.authorizer;
145+
146+
let type;
147+
let name;
148+
let arn;
149+
let identitySource;
150+
let resultTtlInSeconds;
151+
let identityValidationExpression;
152+
let claims;
153+
let authorizerId;
154+
155+
if (typeof authorizer === 'string') {
156+
if (authorizer.toUpperCase() === 'AWS_IAM') {
157+
type = 'AWS_IAM';
158+
} else if (authorizer.indexOf(':') === -1) {
159+
name = authorizer;
160+
arn = this.getLambdaArn(authorizer);
161+
} else {
162+
arn = authorizer;
163+
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
164+
}
165+
} else if (typeof authorizer === 'object') {
166+
if (authorizer.type && authorizer.authorizerId) {
167+
type = authorizer.type;
168+
authorizerId = authorizer.authorizerId;
169+
} else if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') {
170+
type = 'AWS_IAM';
171+
} else if (authorizer.arn) {
172+
arn = authorizer.arn;
173+
if (_.isString(authorizer.name)) {
174+
name = authorizer.name;
175+
} else {
176+
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
177+
}
178+
} else if (authorizer.name) {
179+
name = authorizer.name;
180+
arn = this.getLambdaArn(name);
181+
} else {
182+
throw new this.serverless.classes.Error('Please provide either an authorizer name or ARN');
183+
}
184+
185+
if (!type) {
186+
type = authorizer.type;
187+
}
188+
189+
resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10);
190+
resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds;
191+
claims = authorizer.claims || [];
192+
193+
identitySource = authorizer.identitySource;
194+
identityValidationExpression = authorizer.identityValidationExpression;
195+
} else {
196+
const errorMessage = [
197+
`authorizer property in function ${functionName} is not an object nor a string.`,
198+
' The correct format is: authorizer: functionName',
199+
' OR an object containing a name property.',
200+
' Please check the docs for more info.',
201+
].join('');
202+
throw new this.serverless.classes.Error(errorMessage);
203+
}
204+
205+
if (typeof identitySource === 'undefined') {
206+
identitySource = 'method.request.header.Authorization';
207+
}
208+
209+
const integration = this.getIntegration(http);
210+
if (integration === 'AWS_PROXY'
211+
&& typeof arn === 'string'
212+
&& awsArnRegExs.cognitoIdpArnExpr.test(arn)
213+
&& authorizer.claims) {
214+
const errorMessage = [
215+
'Cognito claims can only be filtered when using the lambda integration type',
216+
];
217+
throw new this.serverless.classes.Error(errorMessage);
218+
}
219+
220+
return {
221+
type,
222+
name,
223+
arn,
224+
authorizerId,
225+
resultTtlInSeconds,
226+
identitySource,
227+
identityValidationExpression,
228+
claims,
229+
};
230+
},
231+
110232
getCors(http) {
111233
const headers = [
112234
'Content-Type',
@@ -164,4 +286,10 @@ module.exports = {
164286

165287
return cors;
166288
},
289+
290+
getLambdaArn(name) {
291+
this.serverless.service.getFunction(name);
292+
const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(name);
293+
return { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] };
294+
},
167295
};

0 commit comments

Comments
 (0)