Skip to content

Enable API Gateway authorizers #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ stepFunctions:
```

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.

To enable the Access-Control-Max-Age preflight response header, set the maxAge property in the cors object:

```yml
Expand All @@ -272,6 +271,73 @@ stepFunctions:
maxAge: 86400
```

#### HTTP Endpoints with AWS_IAM Authorizers

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:

```yml
stepFunctions:
stateMachines:
hello:
events:
- http:
path: posts/create
method: POST
authorizer: aws_iam
definition:
```

#### HTTP Endpoints with Custom Authorizers

[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.

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:

```yml
stepFunctions:
stateMachines:
hello:
- http:
path: posts/create
method: post
authorizer: authorizerFunc
definition:
```

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:

```yml
stepFunctions:
stateMachines:
hello:
- http:
path: posts/create
method: post
authorizer: xxx:xxx:Lambda-Name
definition:
```

### Share Authorizer

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.

```yml
stepFunctions:
stateMachines:
createUser:
...
events:
- http:
path: /users
...
authorizer:
# Provide both type and authorizerId
type: COGNITO_USER_POOLS # TOKEN, CUSTOM or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
authorizerId:
Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID
```


#### Customizing request body mapping templates

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`:
Expand Down
51 changes: 50 additions & 1 deletion lib/deploy/events/apiGateway/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const BbPromise = require('bluebird');
const _ = require('lodash');
const awsArnRegExs = require('../../../utils/arnRegularExpressions');

module.exports = {

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

_.merge(template,
this.getMethodIntegration(event.stateMachineName, stateMachineObj, event.http),
this.getMethodResponses(event.http)
this.getMethodResponses(event.http),
this.getMethodAuthorization(event.http)
);

const methodLogicalId = this.provider.naming
Expand Down Expand Up @@ -179,4 +181,51 @@ module.exports = {

return methodResponse;
},

getMethodAuthorization(http) {
if (_.get(http, 'authorizer.type') === 'AWS_IAM') {
return {
Properties: {
AuthorizationType: 'AWS_IAM',
},
};
}

if (http.authorizer) {
if (http.authorizer.type && http.authorizer.authorizerId) {
return {
Properties: {
AuthorizationType: http.authorizer.type,
AuthorizerId: http.authorizer.authorizerId,
},
};
}

const authorizerLogicalId = this.provider.naming
.getAuthorizerLogicalId(http.authorizer.name || http.authorizer);

let authorizationType;
const authorizerArn = http.authorizer.arn;
if (typeof authorizerArn === 'string'
&& awsArnRegExs.cognitoIdpArnExpr.test(authorizerArn)) {
authorizationType = 'COGNITO_USER_POOLS';
} else {
authorizationType = 'CUSTOM';
}

return {
Properties: {
AuthorizationType: authorizationType,
AuthorizerId: { Ref: authorizerLogicalId },
},
DependsOn: authorizerLogicalId,
};
}

return {
Properties: {
AuthorizationType: 'NONE',
},
};
},
};
71 changes: 71 additions & 0 deletions lib/deploy/events/apiGateway/methods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,75 @@ describe('#methods()', () => {
.to.equal('\'*\'');
});
});

describe('#getMethodAuthorization()', () => {
it('should return properties with AuthorizationType: NONE if no authorizer provided', () => {
const event = {
path: 'foo/bar1',
method: 'post',
};

expect(serverlessStepFunctions.getMethodAuthorization(event)
.Properties.AuthorizationType).to.equal('NONE');
});

it('should return resource properties with AuthorizationType: AWS_IAM', () => {
const event = {
authorizer: {
type: 'AWS_IAM',
authorizerId: 'foo12345',
},
};

expect(serverlessStepFunctions.getMethodAuthorization(event)
.Properties.AuthorizationType).to.equal('AWS_IAM');
});

it('should return properties with AuthorizationType: CUSTOM and authotizerId', () => {
const event = {
authorizer: {
type: 'CUSTOM',
authorizerId: 'foo12345',
},
};

expect(serverlessStepFunctions.getMethodAuthorization(event)
.Properties.AuthorizationType).to.equal('CUSTOM');
expect(serverlessStepFunctions.getMethodAuthorization(event)
.Properties.AuthorizerId).to.equal('foo12345');
});

it('should return properties with AuthorizationType: CUSTOM and resource reference', () => {
const event = {
authorizer: {
name: 'authorizer',
arn: { 'Fn::GetAtt': ['SomeLambdaFunction', 'Arn'] },
resultTtlInSeconds: 300,
identitySource: 'method.request.header.Authorization',
},
};

const autorization = serverlessStepFunctions.getMethodAuthorization(event);
expect(autorization.Properties.AuthorizationType)
.to.equal('CUSTOM');

expect(autorization.Properties.AuthorizerId)
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' });
});

it('should return properties with AuthorizationType: COGNITO_USER_POOLS', () => {
const event = {
authorizer: {
name: 'authorizer',
arn: 'arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ',
},
};

const autorization = serverlessStepFunctions.getMethodAuthorization(event);
expect(autorization.Properties.AuthorizationType)
.to.equal('COGNITO_USER_POOLS');
expect(autorization.Properties.AuthorizerId)
.to.deep.equal({ Ref: 'AuthorizerApiGatewayAuthorizer' });
});
});
});
128 changes: 128 additions & 0 deletions lib/deploy/events/apiGateway/validate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const NOT_FOUND = -1;
const _ = require('lodash');
const awsArnRegExs = require('../../../utils/arnRegularExpressions');

module.exports = {
httpValidate() {
Expand All @@ -15,6 +16,10 @@ module.exports = {
http.path = this.getHttpPath(http, stateMachineName);
http.method = this.getHttpMethod(http, stateMachineName);

if (http.authorizer) {
http.authorizer = this.getAuthorizer(http, stateMachineName);
}

if (http.cors) {
http.cors = this.getCors(http);

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

getIntegration(http, stateMachineName) {
if (http.integration) {
// normalize the integration for further processing
const normalizedIntegration = http.integration.toUpperCase().replace('-', '_');
const allowedIntegrations = [
'LAMBDA_PROXY', 'LAMBDA', 'AWS', 'AWS_PROXY', 'HTTP', 'HTTP_PROXY', 'MOCK',
];

// check if the user has entered a non-valid integration
if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) {
const errorMessage = [
`Invalid APIG integration "${http.integration}"`,
` in function "${stateMachineName}".`,
' Supported integrations are:',
' lambda, lambda-proxy, aws, aws-proxy, http, http-proxy, mock.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (normalizedIntegration === 'LAMBDA') {
return 'AWS';
} else if (normalizedIntegration === 'LAMBDA_PROXY') {
return 'AWS_PROXY';
}
return normalizedIntegration;
}
return 'AWS_PROXY';
},

getAuthorizer(http, functionName) {
const authorizer = http.authorizer;

let type;
let name;
let arn;
let identitySource;
let resultTtlInSeconds;
let identityValidationExpression;
let claims;
let authorizerId;

if (typeof authorizer === 'string') {
if (authorizer.toUpperCase() === 'AWS_IAM') {
type = 'AWS_IAM';
} else if (authorizer.indexOf(':') === -1) {
name = authorizer;
arn = this.getLambdaArn(authorizer);
} else {
arn = authorizer;
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
}
} else if (typeof authorizer === 'object') {
if (authorizer.type && authorizer.authorizerId) {
type = authorizer.type;
authorizerId = authorizer.authorizerId;
} else if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') {
type = 'AWS_IAM';
} else if (authorizer.arn) {
arn = authorizer.arn;
if (_.isString(authorizer.name)) {
name = authorizer.name;
} else {
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
}
} else if (authorizer.name) {
name = authorizer.name;
arn = this.getLambdaArn(name);
} else {
throw new this.serverless.classes.Error('Please provide either an authorizer name or ARN');
}

if (!type) {
type = authorizer.type;
}

resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10);
resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds;
claims = authorizer.claims || [];

identitySource = authorizer.identitySource;
identityValidationExpression = authorizer.identityValidationExpression;
} else {
const errorMessage = [
`authorizer property in function ${functionName} is not an object nor a string.`,
' The correct format is: authorizer: functionName',
' OR an object containing a name property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}

if (typeof identitySource === 'undefined') {
identitySource = 'method.request.header.Authorization';
}

const integration = this.getIntegration(http);
if (integration === 'AWS_PROXY'
&& typeof arn === 'string'
&& awsArnRegExs.cognitoIdpArnExpr.test(arn)
&& authorizer.claims) {
const errorMessage = [
'Cognito claims can only be filtered when using the lambda integration type',
];
throw new this.serverless.classes.Error(errorMessage);
}

return {
type,
name,
arn,
authorizerId,
resultTtlInSeconds,
identitySource,
identityValidationExpression,
claims,
};
},

getCors(http) {
const headers = [
'Content-Type',
Expand Down Expand Up @@ -164,4 +286,10 @@ module.exports = {

return cors;
},

getLambdaArn(name) {
this.serverless.service.getFunction(name);
const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(name);
return { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] };
},
};
Loading