Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@
# Ignore all bundled JS files
files/deployable/dist/*.js
files/deployable/dist/*.js.map

# ignore generated config file
files/deployable/dist/config.json

# ignore node-modules
files/deployable/node_modules
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ enforce Cognito Authentication through a configured Cognito User Pool.

## Requirements
- Terraform version >= 1.0.X
- NodeJS + NPM (compatible with NodeJS 18.X.X)
- NodeJS + NPM (compatible with NodeJS 20.X.X)
- Used for `npm ci` dependency installation for Lambda@Edge Bundle.
- Terraform AWS Provider in `us-east-1`
- Requirement for CloudFront + Lambda@Edge runtime.
Expand Down Expand Up @@ -53,12 +53,23 @@ resource "aws_cloudfront_distribution" "my_cloudfront_distribution" {
}

```
### Cloudwatch logging
If a Lambda@Edge function has IAM permission to `logs:CreateLogGroup` then it will create a Cloudwatch log group called `/aws/lambda/us-east-1.<lambda-name>` in every CloudFront region that serves a request, which can result in logging proliferating across many regions, as described in https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-logs.html#lambda-at-edge-logs . This auto-created log group has no retention policy set, so in addition to Cloudwatch ingest costs, Cloudwatch storage costs increase over time for the life of the function. Setting `cloudwatch_enable_log_group_create = false` will mean that the IAM policies in the module will not grant `logs:CreateLogGroup` to the edge-auth Lambda@Edge function, and if the log group does not exit, no logging happens (so no Cloudwatch ingest or storage costs). Notes:
- If the variable is `false` and logging is required in a specific region, the caller is responsible for ensuring the log group exists in that region.
- Retroactively setting the variable to `false` is not sufficient to disable logging if the log group already exists in a region - manual deletion of the unwanted log groups will be required to prevent further logging.
- If a policy outside the module grants the edge-auth function rights to `logs:CreateLogGroup`, then Lambda@Edge will still create the log group and log.

### Lambda@Edge Destroy Issue
Note that if a destroy action is performed on this terraform module, terraform is unable to delete the Lambda@Edge that was published as a part of this infrastructure (This is noted by this [issue](https://github.com/hashicorp/terraform-provider-aws/issues/1721) on the Terraform provider). It will only be removed from the terraform state as the `skip_destroy` flag is set to true.

In order to properly delete this resource, it should be manually cleaned up, [instructions here](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html).

### Config shipping
As noted in [141](https://github.com/disney/terraform-aws-lambda-at-edge-cognito-authentication/issues/141), in regions far remote from `us-east-1`, the edge-auth lambda regularly throws 503's caused by initialisation taking longer than 5 seconds. The variable `lambda_ship_config` will cause the config for the lambda to be written out as a local file `config.json` that is packaged and shipped with the lambda code. This has some pros:
- it significantly reduces the number of round trips required between the edge region and `us-east-1` so lambda initialisation takes a consistent ~1.25s, whereas the default behaviour with the config in SSM can take anywhere from 1.5 to 5+ seconds, which makes the function both more reliable (less 503's) and cheaper.
and some cons:
- it means that every config change requires a lambda redeploy - which makes deploys take minutes rather than seconds


<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements
Expand All @@ -74,6 +85,7 @@ In order to properly delete this resource, it should be manually cleaned up, [in
|------|---------|
| <a name="provider_archive"></a> [archive](#provider\_archive) | 2.4.0 |
| <a name="provider_aws"></a> [aws](#provider\_aws) | 5.26.0 |
| <a name="provider_local"></a> [local](#provider\_local) | 2.5.2 |
| <a name="provider_null"></a> [null](#provider\_null) | 3.2.2 |

## Modules
Expand All @@ -85,19 +97,29 @@ No modules.
| Name | Type |
|------|------|
| [aws_iam_role.lambda_at_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy.allow_cloudwatch_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.lambda_edge_self_role_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.ssm_parameter_decrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.ssm_parameter_permission_for_lambda_auth](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_kms_key.ssm_kms_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
| [aws_lambda_function.cloudfront_auth_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
| [aws_ssm_parameter.lambda_configuration_parameters](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [local_file.lambda_configuration](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource |
| [null_resource.install_lambda_dependencies](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
| [archive_file.lambda_edge_bundle](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_iam_policy_document.allow_cloudwatch_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.allow_lambda_edge_self_role_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.allow_lambda_service_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.allow_ssm_parameter_decrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.allow_ssm_parameter_permission_for_lambda_auth](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_cloudwatch_enable_log_group_create"></a> [cloudwatch\_enable\_log\_group\_create](#input\_cloudwatch\_enable\_log\_group\_create) | Allow Lambda@Edge to create log groups in cloudwatch, defaults to true. | `bool` | `true` | no |
| <a name="input_cognito_additional_settings"></a> [cognito\_additional\_settings](#input\_cognito\_additional\_settings) | Map of any to configure any additional cognito@edge parameters not handled by this module. | `any` | `{}` | no |
| <a name="input_cognito_cookie_expiration_days"></a> [cognito\_cookie\_expiration\_days](#input\_cognito\_cookie\_expiration\_days) | Number of days to keep the cognito cookie valid. | `number` | `7` | no |
| <a name="input_cognito_disable_cookie_domain"></a> [cognito\_disable\_cookie\_domain](#input\_cognito\_disable\_cookie\_domain) | Sets domain attribute in cookies, defaults to false. | `bool` | `false` | no |
Expand All @@ -110,6 +132,7 @@ No modules.
| <a name="input_cognito_user_pool_name"></a> [cognito\_user\_pool\_name](#input\_cognito\_user\_pool\_name) | Name of the Cognito User Pool to utilize. Required if 'cognito\_user\_pool\_domain' is not set. | `string` | `""` | no |
| <a name="input_cognito_user_pool_region"></a> [cognito\_user\_pool\_region](#input\_cognito\_user\_pool\_region) | AWS region where the cognito user pool was created. | `string` | `"us-west-2"` | no |
| <a name="input_lambda_runtime"></a> [lambda\_runtime](#input\_lambda\_runtime) | Lambda runtime to utilize for Lambda@Edge. | `string` | `"nodejs20.x"` | no |
| <a name="input_lambda_ship_config"></a> [lambda\_ship\_config](#input\_lambda\_ship\_config) | Whether to ship the config in the lambda package, or use SSM | `bool` | `false` | no |
| <a name="input_lambda_timeout"></a> [lambda\_timeout](#input\_lambda\_timeout) | Amount of timeout in seconds to set on for Lambda@Edge. | `number` | `5` | no |
| <a name="input_name"></a> [name](#input\_name) | Name to prefix on all infrastructure created by this module. | `string` | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | Map of tags to attach to all AWS resources created by this module. | `map(string)` | `{}` | no |
Expand All @@ -119,5 +142,6 @@ No modules.
| Name | Description |
|------|-------------|
| <a name="output_arn"></a> [arn](#output\_arn) | ARN for the Lambda@Edge created by this module. |
| <a name="output_function_name"></a> [name](#output\_function\_name) | Name of the Lambda@Edge created by this module. |
| <a name="output_qualified_arn"></a> [qualified\_arn](#output\_qualified\_arn) | Qualified ARN for the Lambda@Edge created by this module. |
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
74 changes: 42 additions & 32 deletions files/deployable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const { IAMClient, GetRolePolicyCommand } = require('@aws-sdk/client-iam');
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts');

const fs = require('fs');
const configFile = './config.json';

const NodeCache = require("node-cache");
const { Authenticator } = require('cognito-at-edge');
const { getLogger } = require('./logger');
Expand All @@ -12,7 +15,7 @@ const POLICY_NAME = "SSM_PARAMETER_PERMISSION_FOR_LAMBDA_AUTH";

const cache = new NodeCache({
stdTTL: 300,
checkperiod: 150,
checkperiod: 150,
deleteOnExpire: true,
useClones: false
});
Expand Down Expand Up @@ -48,39 +51,46 @@ function getRoleNameFromExecutionARN(arn) {
*/
async function createAuthenticatorFromConfiguration() {
const rootLogger = getLogger();
var authConfig;

try {
const ssmClient = new SSMClient({ region: 'us-east-1' });
const stsClient = new STSClient({ region: 'us-east-1' });
const iamClient = new IAMClient({ region: 'us-east-1' });

// Get the IAM role that is currently running this lambda.
rootLogger.info('Attempting to get current execution IAM Role.');
const curIdentity = await stsClient.send(new GetCallerIdentityCommand({}));
const iamRole = curIdentity.Arn;
rootLogger.info(`Running as IAM Role[${iamRole}].`);
const iamRoleName = getRoleNameFromExecutionARN(iamRole, 'role')

// Get the predefined policy which references the SSM Parameter we need to pull
rootLogger.info(`Fetching Policy[${POLICY_NAME}] from IAM Role[${iamRole}].`);
const { PolicyDocument } = await iamClient.send(new GetRolePolicyCommand({ PolicyName: POLICY_NAME, RoleName: iamRoleName }));
rootLogger.info('Successfully fetched Policy document.');

const parsedPolicyDoc = decodeURIComponent(PolicyDocument);
const referencedPolicy = JSON.parse(parsedPolicyDoc);
const ssmParameterArn = referencedPolicy.Statement[0].Resource;
rootLogger.info(`Found SSM Resource[${ssmParameterArn}].`);
const ssmParameterName = `/${getResourceNameFromARN(ssmParameterArn, 'parameter')}`;

// Fetch the data from parameter store
rootLogger.info(`Fetching Parameter[${ssmParameterName}].`);
const { Parameter } = await ssmClient.send(new GetParameterCommand({ Name: ssmParameterName, WithDecryption: true }));
rootLogger.info(`Successfully fetched Parameter[${ssmParameterName}].`);

const authConfig = JSON.parse(Parameter.Value);
rootLogger.info(`Successfully parsed config.`);

// Initialize Authenticator with the config from parameter store
// check if config.json exists in the local file system
if (fs.existsSync(configFile)) {
authConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
rootLogger.info('Successfully read local config.json file.');
} else {
// fetch the configuration from SSM
const ssmClient = new SSMClient({ region: 'us-east-1' });
const stsClient = new STSClient({ region: 'us-east-1' });
const iamClient = new IAMClient({ region: 'us-east-1' });

// Get the IAM role that is currently running this lambda.
rootLogger.info('Attempting to get current execution IAM Role.');
const curIdentity = await stsClient.send(new GetCallerIdentityCommand({}));
const iamRole = curIdentity.Arn;
rootLogger.info(`Running as IAM Role[${iamRole}].`);
const iamRoleName = getRoleNameFromExecutionARN(iamRole, 'role')

// Get the predefined policy which references the SSM Parameter we need to pull
rootLogger.info(`Fetching Policy[${POLICY_NAME}] from IAM Role[${iamRole}].`);
const { PolicyDocument } = await iamClient.send(new GetRolePolicyCommand({ PolicyName: POLICY_NAME, RoleName: iamRoleName }));
rootLogger.info('Successfully fetched Policy document.');

const parsedPolicyDoc = decodeURIComponent(PolicyDocument);
const referencedPolicy = JSON.parse(parsedPolicyDoc);
const ssmParameterArn = referencedPolicy.Statement[0].Resource;
rootLogger.info(`Found SSM Resource[${ssmParameterArn}].`);
const ssmParameterName = `/${getResourceNameFromARN(ssmParameterArn, 'parameter')}`;

// Fetch the data from parameter store
rootLogger.info(`Fetching Parameter[${ssmParameterName}].`);
const { Parameter } = await ssmClient.send(new GetParameterCommand({ Name: ssmParameterName, WithDecryption: true }));
rootLogger.info(`Successfully fetched Parameter[${ssmParameterName}].`);

authConfig = JSON.parse(Parameter.Value);
rootLogger.info(`Successfully parsed config.`);
}
// Initialize Authenticator with the config from parameter store/local file
const authenticator = new Authenticator(authConfig);
if (cache.set(cacheAuthenticatorKey, authenticator)) {
rootLogger.info('Successfully initialized Authenticator.');
Expand Down
Loading