From 004c876205b753206724aec3f8c6f8485f96e705 Mon Sep 17 00:00:00 2001 From: Lavanya0513 <145364950+Lavanya0513@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:11:26 -0400 Subject: [PATCH 01/10] added new pattern for using Lambda Authorizer to map API Keys to tenant --- README.md | 93 ++- apigw-dynamodb-apikey-cdk.drawio | 56 ++ cdk.json | 88 +++ deploy_dynamodb.sh | 8 + example-pattern-dynamodb.json | 61 ++ package-lock.json | 1163 ++++++++++++++++++++++++++++++ package.json | 23 + tsconfig.json | 31 + 8 files changed, 1509 insertions(+), 14 deletions(-) create mode 100644 apigw-dynamodb-apikey-cdk.drawio create mode 100644 cdk.json create mode 100644 deploy_dynamodb.sh create mode 100644 example-pattern-dynamodb.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 40d509c880..9a49e4d9bc 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,96 @@ -# AWS Serverless Patterns Collection +# Amazon API Gateway with AWS Lambda Authorizer and DynamoDB for API Key Authentication -This repo contains serverless patterns showing how to integrate services using infrastructure-as-code (IaC). You can use these patterns to help develop your own projects quickly. +This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer validates the tenant ID from the request header, retrieves the corresponding API key from DynamoDB, and returns a policy document enabling API Gateway access. -- Learn more about these patterns at https://serverlessland.com/patterns. -- To learn more about submitting a pattern, read the [publishing guidelines page](https://github.com/aws-samples/serverless-patterns/blob/main/PUBLISHING.md). +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk](https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk) Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +* [Node.js and npm](https://nodejs.org/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed + ## Deployment Instructions -1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login. +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd apigw-dynamodb-apikey-cdk + ``` +1. Install dependencies: + ``` + npm install + ``` +1. Deploy the stack: + ``` + cdk deploy + ``` + +Note the outputs from the CDK deployment process. The output will include the API Gateway URL and the DynamoDB table name you'll need for testing. + +## How it works + +![Architecture Diagram](./apigw-dynamodb-apikey-cdk.drawio) + +1. Client makes a request to the API with a tenant ID in the `x-tenant-id` header +2. API Gateway forwards the authorization request to the Lambda Authorizer +3. The Lambda Authorizer looks up the tenant ID in the DynamoDB table + - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` + - If the tenant does not exist, the request is denied +4. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer + +The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. + +## Testing -1. [Install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) and [install the AWS Serverless Application Model CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) on your local machine. +1. Get the DynamoDB table name and API Gateway URL from the deployment output: + ```bash + # The outputs will be similar to + ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ + ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY + ``` -1. Create a new directory and navigate to that directory in a terminal. +1. Insert a tenant mapping into the DynamoDB table: + ```bash + aws dynamodb put-item \ + --table-name TABLE_NAME \ + --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' + ``` -1. Clone this repo +1. Make a request to the protected endpoint with a valid tenant ID: + ```bash + curl -H "x-tenant-id: sample-tenant" https://REPLACE_WITH_API_URL/protected + ``` + If successful, you should receive a response like: + ```json + { "message": "Access granted" } + ``` -``` -git clone https://github.com/aws-samples/serverless-patterns -``` +1. Try with an invalid tenant ID: + ```bash + curl -H "x-tenant-id: invalid-tenant" https://REPLACE_WITH_API_URL/protected + ``` + You should receive an unauthorized error. -Each subdirectory contains additional installation and usage instructions. +1. Try without a tenant ID: + ```bash + curl https://REPLACE_WITH_API_URL/protected + ``` + You should also receive an unauthorized error. -## Ownership +## Cleanup -This project is owned, managed, and maintained by the **AWS Sererless Developer Advocacy team**. +1. Delete the stack: + ```bash + cdk destroy + ``` ---- Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/apigw-dynamodb-apikey-cdk.drawio b/apigw-dynamodb-apikey-cdk.drawio new file mode 100644 index 0000000000..6a17b10e95 --- /dev/null +++ b/apigw-dynamodb-apikey-cdk.drawio @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cdk.json b/cdk.json new file mode 100644 index 0000000000..77e1f9143a --- /dev/null +++ b/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true + } +} diff --git a/deploy_dynamodb.sh b/deploy_dynamodb.sh new file mode 100644 index 0000000000..7758737dc7 --- /dev/null +++ b/deploy_dynamodb.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +APP="npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts" +STACK_NAME="ApigwDynamodbApikeyCdkStack" + +npm install +cdk deploy "$STACK_NAME" --app "$APP" "$@" diff --git a/example-pattern-dynamodb.json b/example-pattern-dynamodb.json new file mode 100644 index 0000000000..f86a74b81b --- /dev/null +++ b/example-pattern-dynamodb.json @@ -0,0 +1,61 @@ +{ + "title": "Amazon API Gateway, AWS Lambda Authorizer & DynamoDB for API Key Authentication", + "description": "Implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB for tenant-to-API-key mapping.", + + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", + "A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer derives the tenant ID from the authorization token and queries DynamoDB to retrieve the corresponding API key.", + "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", + "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-dynamodb-apikey-cdk", + "templateURL": "serverless-patterns/apigw-dynamodb-apikey-cdk", + "projectFolder": "apigw-dynamodb-apikey-cdk", + "templateFile": "lib/apigw-dynamodb-apikey-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Authorizers for Amazon API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" + }, + { + "text": "Amazon DynamoDB Developer Guide", + "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + }, + { + "text": "API Gateway Usage Plans", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" + } + ] + }, + "deploy": { + "text": ["npm install", "cdk deploy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\""] + }, + "testing": { + "text": [ + "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", + "Make a request to the protected endpoint using the tenant ID: curl -H \"x-tenant-id: sample-tenant\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\"" + ] + } + +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..b76f45b798 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1163 @@ +{ + "name": "apigw-dynamodb-apikey-cdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "apigw-dynamodb-apikey-cdk", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + }, + "bin": { + "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", + "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1003.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz", + "integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.189.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.1.tgz", + "integrity": "sha512-9JU0yUr2iRTJ1oCPrHyx7hOtBDWyUfyOcdb6arlumJnMcQr2cyAMASY8HuAXHc8Y10ipVp8dRTW+J4/132IIYA==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.229", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^41.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.1", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..7e9def1262 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "apigw-dynamodb-apikey-cdk", + "version": "0.1.0", + "bin": { + "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..b87bdcc448 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 939787398602a921ab50d6e2b3663bafd1895fdd Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Mon, 30 Mar 2026 15:26:38 -0400 Subject: [PATCH 02/10] Revert "added new pattern for using Lambda Authorizer to map API Keys to tenant" This reverts commit 004c876205b753206724aec3f8c6f8485f96e705. --- README.md | 93 +-- apigw-dynamodb-apikey-cdk.drawio | 56 -- cdk.json | 88 --- deploy_dynamodb.sh | 8 - example-pattern-dynamodb.json | 61 -- package-lock.json | 1163 ------------------------------ package.json | 23 - tsconfig.json | 31 - 8 files changed, 14 insertions(+), 1509 deletions(-) delete mode 100644 apigw-dynamodb-apikey-cdk.drawio delete mode 100644 cdk.json delete mode 100644 deploy_dynamodb.sh delete mode 100644 example-pattern-dynamodb.json delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 tsconfig.json diff --git a/README.md b/README.md index 9a49e4d9bc..40d509c880 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,31 @@ -# Amazon API Gateway with AWS Lambda Authorizer and DynamoDB for API Key Authentication +# AWS Serverless Patterns Collection -This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer validates the tenant ID from the request header, retrieves the corresponding API key from DynamoDB, and returns a policy document enabling API Gateway access. +This repo contains serverless patterns showing how to integrate services using infrastructure-as-code (IaC). You can use these patterns to help develop your own projects quickly. -Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk](https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk) +- Learn more about these patterns at https://serverlessland.com/patterns. +- To learn more about submitting a pattern, read the [publishing guidelines page](https://github.com/aws-samples/serverless-patterns/blob/main/PUBLISHING.md). Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. -## Requirements - -* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. -* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured -* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed -* [Node.js and npm](https://nodejs.org/) installed -* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed - ## Deployment Instructions -1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: - ``` - git clone https://github.com/aws-samples/serverless-patterns - ``` -1. Change directory to the pattern directory: - ``` - cd apigw-dynamodb-apikey-cdk - ``` -1. Install dependencies: - ``` - npm install - ``` -1. Deploy the stack: - ``` - cdk deploy - ``` - -Note the outputs from the CDK deployment process. The output will include the API Gateway URL and the DynamoDB table name you'll need for testing. - -## How it works - -![Architecture Diagram](./apigw-dynamodb-apikey-cdk.drawio) - -1. Client makes a request to the API with a tenant ID in the `x-tenant-id` header -2. API Gateway forwards the authorization request to the Lambda Authorizer -3. The Lambda Authorizer looks up the tenant ID in the DynamoDB table - - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` - - If the tenant does not exist, the request is denied -4. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer - -The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. - -## Testing +1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login. -1. Get the DynamoDB table name and API Gateway URL from the deployment output: - ```bash - # The outputs will be similar to - ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ - ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY - ``` +1. [Install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) and [install the AWS Serverless Application Model CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) on your local machine. -1. Insert a tenant mapping into the DynamoDB table: - ```bash - aws dynamodb put-item \ - --table-name TABLE_NAME \ - --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' - ``` +1. Create a new directory and navigate to that directory in a terminal. -1. Make a request to the protected endpoint with a valid tenant ID: - ```bash - curl -H "x-tenant-id: sample-tenant" https://REPLACE_WITH_API_URL/protected - ``` - If successful, you should receive a response like: - ```json - { "message": "Access granted" } - ``` +1. Clone this repo -1. Try with an invalid tenant ID: - ```bash - curl -H "x-tenant-id: invalid-tenant" https://REPLACE_WITH_API_URL/protected - ``` - You should receive an unauthorized error. +``` +git clone https://github.com/aws-samples/serverless-patterns +``` -1. Try without a tenant ID: - ```bash - curl https://REPLACE_WITH_API_URL/protected - ``` - You should also receive an unauthorized error. +Each subdirectory contains additional installation and usage instructions. -## Cleanup +## Ownership -1. Delete the stack: - ```bash - cdk destroy - ``` +This project is owned, managed, and maintained by the **AWS Sererless Developer Advocacy team**. ---- Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/apigw-dynamodb-apikey-cdk.drawio b/apigw-dynamodb-apikey-cdk.drawio deleted file mode 100644 index 6a17b10e95..0000000000 --- a/apigw-dynamodb-apikey-cdk.drawio +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cdk.json b/cdk.json deleted file mode 100644 index 77e1f9143a..0000000000 --- a/cdk.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts", - "watch": { - "include": [ - "**" - ], - "exclude": [ - "README.md", - "cdk*.json", - "**/*.d.ts", - "**/*.js", - "tsconfig.json", - "package*.json", - "yarn.lock", - "node_modules", - "test" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": [ - "aws", - "aws-cn" - ], - "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, - "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, - "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, - "@aws-cdk/aws-iam:minimizePolicies": true, - "@aws-cdk/core:validateSnapshotRemovalPolicy": true, - "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, - "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, - "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, - "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, - "@aws-cdk/core:enablePartitionLiterals": true, - "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, - "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, - "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, - "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, - "@aws-cdk/aws-route53-patters:useCertificate": true, - "@aws-cdk/customresources:installLatestAwsSdkDefault": false, - "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, - "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, - "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, - "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, - "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, - "@aws-cdk/aws-redshift:columnId": true, - "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, - "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, - "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, - "@aws-cdk/aws-kms:aliasNameRef": true, - "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, - "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, - "@aws-cdk/aws-efs:denyAnonymousAccess": true, - "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, - "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, - "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, - "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, - "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, - "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, - "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, - "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, - "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, - "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, - "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, - "@aws-cdk/aws-eks:nodegroupNameAttribute": true, - "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, - "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, - "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, - "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, - "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, - "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, - "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, - "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, - "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, - "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, - "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, - "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, - "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, - "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, - "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, - "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, - "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, - "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, - "@aws-cdk/core:enableAdditionalMetadataCollection": true, - "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true - } -} diff --git a/deploy_dynamodb.sh b/deploy_dynamodb.sh deleted file mode 100644 index 7758737dc7..0000000000 --- a/deploy_dynamodb.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -euo pipefail - -APP="npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts" -STACK_NAME="ApigwDynamodbApikeyCdkStack" - -npm install -cdk deploy "$STACK_NAME" --app "$APP" "$@" diff --git a/example-pattern-dynamodb.json b/example-pattern-dynamodb.json deleted file mode 100644 index f86a74b81b..0000000000 --- a/example-pattern-dynamodb.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "title": "Amazon API Gateway, AWS Lambda Authorizer & DynamoDB for API Key Authentication", - "description": "Implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB for tenant-to-API-key mapping.", - - "language": "TypeScript", - "level": "200", - "framework": "AWS CDK", - "introBox": { - "headline": "How it works", - "text": [ - "This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", - "A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer derives the tenant ID from the authorization token and queries DynamoDB to retrieve the corresponding API key.", - "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", - "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." - ] - }, - "gitHub": { - "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-dynamodb-apikey-cdk", - "templateURL": "serverless-patterns/apigw-dynamodb-apikey-cdk", - "projectFolder": "apigw-dynamodb-apikey-cdk", - "templateFile": "lib/apigw-dynamodb-apikey-stack.ts" - } - }, - "resources": { - "bullets": [ - { - "text": "Lambda Authorizers for Amazon API Gateway", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" - }, - { - "text": "Amazon DynamoDB Developer Guide", - "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" - }, - { - "text": "Amazon API Gateway - REST APIs", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" - }, - { - "text": "API Gateway Usage Plans", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" - } - ] - }, - "deploy": { - "text": ["npm install", "cdk deploy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\""] - }, - "testing": { - "text": [ - "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", - "Make a request to the protected endpoint using the tenant ID: curl -H \"x-tenant-id: sample-tenant\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", - "If successful, you should receive a response: { \"message\": \"Access granted\" }" - ] - }, - "cleanup": { - "text": [ - "Delete the CDK stack: cdk destroy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\"" - ] - } - -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b76f45b798..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,1163 +0,0 @@ -{ - "name": "apigw-dynamodb-apikey-cdk", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "apigw-dynamodb-apikey-cdk", - "version": "0.1.0", - "dependencies": { - "aws-cdk-lib": "2.189.1", - "constructs": "^10.0.0" - }, - "bin": { - "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1003.0", - "esbuild": "^0.25.1", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - } - }, - "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.273", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", - "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", - "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "41.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", - "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", - "bundleDependencies": [ - "jsonschema", - "semver" - ], - "license": "Apache-2.0", - "dependencies": { - "jsonschema": "~1.4.1", - "semver": "^7.7.1" - }, - "engines": { - "node": ">= 14.15.0" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { - "version": "1.4.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.7.1", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.7.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", - "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/aws-cdk": { - "version": "2.1003.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz", - "integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "cdk": "bin/cdk" - }, - "engines": { - "node": ">= 14.15.0" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/aws-cdk-lib": { - "version": "2.189.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.1.tgz", - "integrity": "sha512-9JU0yUr2iRTJ1oCPrHyx7hOtBDWyUfyOcdb6arlumJnMcQr2cyAMASY8HuAXHc8Y10ipVp8dRTW+J4/132IIYA==", - "bundleDependencies": [ - "@balena/dockerignore", - "case", - "fs-extra", - "ignore", - "jsonschema", - "minimatch", - "punycode", - "semver", - "table", - "yaml", - "mime-types" - ], - "license": "Apache-2.0", - "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.229", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^41.0.0", - "@balena/dockerignore": "^1.0.2", - "case": "1.6.3", - "fs-extra": "^11.3.0", - "ignore": "^5.3.2", - "jsonschema": "^1.5.0", - "mime-types": "^2.1.35", - "minimatch": "^3.1.2", - "punycode": "^2.3.1", - "semver": "^7.7.1", - "table": "^6.9.0", - "yaml": "1.10.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "constructs": "^10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.17.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/aws-cdk-lib/node_modules/case": { - "version": "1.6.3", - "inBundle": true, - "license": "(MIT OR GPL-3.0-or-later)", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/aws-cdk-lib/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/mime-db": { - "version": "1.52.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/mime-types": { - "version": "2.1.35", - "inBundle": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.7.1", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.9.0", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/constructs": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", - "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", - "license": "Apache-2.0" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7e9def1262..0000000000 --- a/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "apigw-dynamodb-apikey-cdk", - "version": "0.1.0", - "bin": { - "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" - }, - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "cdk": "cdk" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1003.0", - "esbuild": "^0.25.1", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - }, - "dependencies": { - "aws-cdk-lib": "2.189.1", - "constructs": "^10.0.0" - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index b87bdcc448..0000000000 --- a/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": [ - "es2020", - "dom" - ], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "typeRoots": [ - "./node_modules/@types" - ] - }, - "exclude": [ - "node_modules", - "cdk.out" - ] -} From 0c7a68f68bbfe6905c9c88771d259ef0a01e0bb9 Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Mon, 30 Mar 2026 15:33:51 -0400 Subject: [PATCH 03/10] added new pattern for tenant to apikey --- apigw-dynamodb-tenantid-cdk/README.md | 98 +++++++++++++++++ .../apigw-dynamodb-apikey-cdk.drawio | 56 ++++++++++ .../arbitrary keys/README.md | 59 ++++++++++ .../apigw-arbitrary-keys.drawio | 101 +++++++++++++++++ .../bin/apigw-arbitrary-keys.ts | 11 ++ .../arbitrary keys/cdk.json | 21 ++++ .../lib/apigw-arbitrary-keys-stack.ts | 103 ++++++++++++++++++ .../lib/lambda/arbitrary-key-authorizer.js | 55 ++++++++++ .../arbitrary keys/package.json | 23 ++++ .../arbitrary keys/tsconfig.json | 23 ++++ .../bin/apigw-dynamodb-apikey-cdk.ts | 11 ++ apigw-dynamodb-tenantid-cdk/cdk.json | 88 +++++++++++++++ .../deploy_dynamodb.sh | 8 ++ .../example-pattern-dynamodb.json | 61 +++++++++++ .../lib/apigw-dynamodb-apikey-stack.ts | 89 +++++++++++++++ .../lib/lambda/dynamodb-authorizer.js | 48 ++++++++ apigw-dynamodb-tenantid-cdk/package.json | 23 ++++ apigw-dynamodb-tenantid-cdk/tsconfig.json | 31 ++++++ 18 files changed, 909 insertions(+) create mode 100644 apigw-dynamodb-tenantid-cdk/README.md create mode 100644 apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json create mode 100644 apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts create mode 100644 apigw-dynamodb-tenantid-cdk/cdk.json create mode 100644 apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh create mode 100644 apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json create mode 100644 apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts create mode 100644 apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js create mode 100644 apigw-dynamodb-tenantid-cdk/package.json create mode 100644 apigw-dynamodb-tenantid-cdk/tsconfig.json diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md new file mode 100644 index 0000000000..9a49e4d9bc --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/README.md @@ -0,0 +1,98 @@ +# Amazon API Gateway with AWS Lambda Authorizer and DynamoDB for API Key Authentication + +This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer validates the tenant ID from the request header, retrieves the corresponding API key from DynamoDB, and returns a policy document enabling API Gateway access. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk](https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +* [Node.js and npm](https://nodejs.org/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd apigw-dynamodb-apikey-cdk + ``` +1. Install dependencies: + ``` + npm install + ``` +1. Deploy the stack: + ``` + cdk deploy + ``` + +Note the outputs from the CDK deployment process. The output will include the API Gateway URL and the DynamoDB table name you'll need for testing. + +## How it works + +![Architecture Diagram](./apigw-dynamodb-apikey-cdk.drawio) + +1. Client makes a request to the API with a tenant ID in the `x-tenant-id` header +2. API Gateway forwards the authorization request to the Lambda Authorizer +3. The Lambda Authorizer looks up the tenant ID in the DynamoDB table + - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` + - If the tenant does not exist, the request is denied +4. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer + +The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. + +## Testing + +1. Get the DynamoDB table name and API Gateway URL from the deployment output: + ```bash + # The outputs will be similar to + ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ + ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY + ``` + +1. Insert a tenant mapping into the DynamoDB table: + ```bash + aws dynamodb put-item \ + --table-name TABLE_NAME \ + --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' + ``` + +1. Make a request to the protected endpoint with a valid tenant ID: + ```bash + curl -H "x-tenant-id: sample-tenant" https://REPLACE_WITH_API_URL/protected + ``` + If successful, you should receive a response like: + ```json + { "message": "Access granted" } + ``` + +1. Try with an invalid tenant ID: + ```bash + curl -H "x-tenant-id: invalid-tenant" https://REPLACE_WITH_API_URL/protected + ``` + You should receive an unauthorized error. + +1. Try without a tenant ID: + ```bash + curl https://REPLACE_WITH_API_URL/protected + ``` + You should also receive an unauthorized error. + +## Cleanup + +1. Delete the stack: + ```bash + cdk destroy + ``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio new file mode 100644 index 0000000000..6a17b10e95 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md b/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md new file mode 100644 index 0000000000..ecbb63688c --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md @@ -0,0 +1,59 @@ +# API Gateway with Arbitrary Usage Identifier Keys (AUIK) + +This pattern demonstrates API Gateway with a Lambda authorizer that returns arbitrary usage identifier keys per the [AUIK specification](https://w.amazon.com/bin/view/AWS/Mobile/API_Gateway/Onboarding/Arbitrary_Usage_Identifier_Keys). + +## How it works + +1. Client sends a request with `x-tenant-id` header +2. API Gateway forwards to the Lambda authorizer +3. The authorizer: + - Extracts the **stage** from the method ARN + - Generates a random 128-character arbitrary API key + - Calls `GetUsagePlans` to find a usage plan associated with the API + stage + - Returns `usageIdentifierKey` (always) and `usagePlanId` (if a plan exists for the stage) +4. API Gateway uses the returned key for throttling/quota enforcement against the usage plan + +Per the AUIK docs: if no `usagePlanId` is returned, API Gateway treats `usageIdentifierKey` as a configured API Key. + +## Authorizer Response Format + +```json +{ + "principalId": "tenant-id", + "policyDocument": { ... }, + "usageIdentifierKey": "<128-char-random-key>", + "usagePlanId": "" +} +``` + +## Prerequisites + +- AWS account allowlisted +- Node.js, npm, AWS CDK installed + +## Deploy + +```bash +cd arbitrary-keys +npm install +cdk deploy +``` + +## Test + +```bash +# Hit prod stage (has usage plan) +curl -H "x-tenant-id: my-tenant" https://.execute-api..amazonaws.com/prod/protected + +# Hit dev stage (has usage plan) +curl -H "x-tenant-id: my-tenant" https://.execute-api..amazonaws.com/dev/protected + +# Without tenant ID (should fail) +curl https://.execute-api..amazonaws.com/prod/protected +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio b/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio new file mode 100644 index 0000000000..078ba7ebbc --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts b/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts new file mode 100644 index 0000000000..50801425e7 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { ApigwArbitraryKeysStack } from "../lib/apigw-arbitrary-keys-stack"; + +const app = new cdk.App(); +new ApigwArbitraryKeysStack(app, "ApigwArbitraryKeysCdkStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json new file mode 100644 index 0000000000..28fd66ad2b --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json @@ -0,0 +1,21 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/apigw-arbitrary-keys.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", "cdk*.json", "**/*.d.ts", "**/*.js", + "tsconfig.json", "package*.json", "yarn.lock", "node_modules", "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true + } +} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts new file mode 100644 index 0000000000..fbf9a89ea6 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts @@ -0,0 +1,103 @@ +import * as cdk from "aws-cdk-lib"; +import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Runtime } from "aws-cdk-lib/aws-lambda"; +import * as path from "path"; +import { Construct } from "constructs"; + +export class ApigwArbitraryKeysStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { + retention: cdk.aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const authorizerFn = new lambda.NodejsFunction(this, "ArbitraryKeyAuthorizer", { + runtime: Runtime.NODEJS_24_X, + entry: path.join(__dirname, "lambda/arbitrary-key-authorizer.js"), + timeout: cdk.Duration.seconds(10), + bundling: { externalModules: ["@aws-sdk/*"] }, + logGroup: authorizerLogGroup, + }); + + // Allow lambda to read usage plans + authorizerFn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["apigateway:GET"], + resources: [`arn:aws:apigateway:${this.region}::/usageplans`], + }), + ); + + // API Gateway with key source from AUTHORIZER + const api = new apigateway.RestApi(this, "ApiGateway", { + restApiName: "Arbitrary Usage Identifier Key Service", + apiKeySourceType: apigateway.ApiKeySourceType.AUTHORIZER, + deploy: false, + }); + + const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { + handler: authorizerFn, + identitySource: "method.request.header.x-tenant-id", + }); + + const protectedResource = api.root.addResource("protected"); + protectedResource.addMethod( + "GET", + new apigateway.MockIntegration({ + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "application/json": '{ "message": "Access granted" }', + }, + }, + ], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { "application/json": '{ "statusCode": 200 }' }, + }), + { + authorizer: lambdaAuthorizer, + apiKeyRequired: true, + methodResponses: [{ statusCode: "200" }], + }, + ); + + // Prod stage + const prodDeployment = new apigateway.Deployment(this, "ProdDeployment", { api }); + const prodStage = new apigateway.Stage(this, "ProdStage", { + deployment: prodDeployment, + stageName: "prod", + }); + api.deploymentStage = prodStage; + + // Dev stage + const devDeployment = new apigateway.Deployment(this, "DevDeployment", { api }); + const devStage = new apigateway.Stage(this, "DevStage", { + deployment: devDeployment, + stageName: "dev", + }); + + // Usage plans per stage + const prodUsagePlan = new apigateway.UsagePlan(this, "ProdUsagePlan", { + name: "ProdUsagePlan", + throttle: { rateLimit: 100, burstLimit: 50 }, + quota: { limit: 10000, period: apigateway.Period.MONTH }, + apiStages: [{ api, stage: prodStage }], + }); + + const devUsagePlan = new apigateway.UsagePlan(this, "DevUsagePlan", { + name: "DevUsagePlan", + throttle: { rateLimit: 10, burstLimit: 5 }, + quota: { limit: 1000, period: apigateway.Period.MONTH }, + apiStages: [{ api, stage: devStage }], + }); + + new cdk.CfnOutput(this, "ProdApiUrl", { value: prodStage.urlForPath("/") }); + new cdk.CfnOutput(this, "DevApiUrl", { value: devStage.urlForPath("/") }); + new cdk.CfnOutput(this, "ProdUsagePlanId", { value: prodUsagePlan.usagePlanId }); + new cdk.CfnOutput(this, "DevUsagePlanId", { value: devUsagePlan.usagePlanId }); + } +} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js new file mode 100644 index 0000000000..7dcb722a02 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js @@ -0,0 +1,55 @@ +import { APIGatewayClient, GetUsagePlansCommand } from "@aws-sdk/client-api-gateway"; +import { randomBytes } from "crypto"; + +const apigwClient = new APIGatewayClient(); + +exports.handler = async (event) => { + const tenantId = event.authorizationToken; + if (!tenantId) { + throw new Error("Unauthorized"); + } + + // arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{method}/{resource} + const arnParts = event.methodArn.split(":"); + const apiGatewayArnPart = arnParts[5].split("/"); + const apiId = apiGatewayArnPart[0]; + const stage = apiGatewayArnPart[1]; + + // Generate arbitrary key (128 chars max) + const usageIdentifierKey = randomBytes(64).toString("hex").substring(0, 128); + + // Find usage plan associated with this API's stage + let usagePlanId; + try { + const resp = await apigwClient.send(new GetUsagePlansCommand({})); + const plan = resp.items?.find((p) => + p.apiStages?.some((s) => s.apiId === apiId && s.stage === stage) + ); + if (plan) { + usagePlanId = plan.id; + } + } catch (err) { + console.error("Error fetching usage plans:", err.message); + } + + const authResponse = { + principalId: tenantId, + policyDocument: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Resource: event.methodArn, + Action: "execute-api:Invoke", + }, + ], + }, + usageIdentifierKey, + }; + + if (usagePlanId) { + authResponse.usagePlanId = usagePlanId; + } + + return authResponse; +}; diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json new file mode 100644 index 0000000000..318dbb16a0 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json @@ -0,0 +1,23 @@ +{ + "name": "apigw-arbitrary-keys-cdk", + "version": "0.1.0", + "bin": { + "apigw-arbitrary-keys-cdk": "bin/apigw-arbitrary-keys.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json new file mode 100644 index 0000000000..464ed774ba --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts b/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts new file mode 100644 index 0000000000..09da105bcd --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { ApigwDynamodbApikeyStack } from "../lib/apigw-dynamodb-apikey-stack"; + +const app = new cdk.App(); +new ApigwDynamodbApikeyStack(app, "ApigwDynamodbApikeyCdkStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/apigw-dynamodb-tenantid-cdk/cdk.json b/apigw-dynamodb-tenantid-cdk/cdk.json new file mode 100644 index 0000000000..0d3b8fe63f --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true + } +} diff --git a/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh b/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh new file mode 100644 index 0000000000..7758737dc7 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +APP="npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts" +STACK_NAME="ApigwDynamodbApikeyCdkStack" + +npm install +cdk deploy "$STACK_NAME" --app "$APP" "$@" diff --git a/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json b/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json new file mode 100644 index 0000000000..f86a74b81b --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json @@ -0,0 +1,61 @@ +{ + "title": "Amazon API Gateway, AWS Lambda Authorizer & DynamoDB for API Key Authentication", + "description": "Implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB for tenant-to-API-key mapping.", + + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", + "A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer derives the tenant ID from the authorization token and queries DynamoDB to retrieve the corresponding API key.", + "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", + "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-dynamodb-apikey-cdk", + "templateURL": "serverless-patterns/apigw-dynamodb-apikey-cdk", + "projectFolder": "apigw-dynamodb-apikey-cdk", + "templateFile": "lib/apigw-dynamodb-apikey-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Authorizers for Amazon API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" + }, + { + "text": "Amazon DynamoDB Developer Guide", + "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + }, + { + "text": "API Gateway Usage Plans", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" + } + ] + }, + "deploy": { + "text": ["npm install", "cdk deploy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\""] + }, + "testing": { + "text": [ + "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", + "Make a request to the protected endpoint using the tenant ID: curl -H \"x-tenant-id: sample-tenant\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\"" + ] + } + +} diff --git a/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts b/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts new file mode 100644 index 0000000000..e3a0570b64 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts @@ -0,0 +1,89 @@ +import * as cdk from "aws-cdk-lib"; +import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; +import { Runtime } from "aws-cdk-lib/aws-lambda"; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import * as path from "path"; +import { Construct } from "constructs"; + +export class ApigwDynamodbApikeyStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // DynamoDB table mapping tenantId -> apiKey + const table = new dynamodb.Table(this, "TenantApiKeyTable", { + partitionKey: { name: "tenantId", type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Lambda authorizer log group + const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { + retention: cdk.aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Lambda authorizer + const authorizerFn = new lambda.NodejsFunction(this, "DynamoDbApiKeyAuthorizer", { + runtime: Runtime.NODEJS_22_X, + entry: path.join(__dirname, "lambda/dynamodb-authorizer.js"), + timeout: cdk.Duration.seconds(10), + environment: { + TABLE_NAME: table.tableName, + }, + bundling: { + externalModules: ["@aws-sdk/*"], + }, + logGroup: authorizerLogGroup, + }); + + table.grantReadData(authorizerFn); + + // API Gateway + const api = new apigateway.RestApi(this, "ApiGateway", { + restApiName: "DynamoDB API Key Protected Service", + description: "API protected with DynamoDB-based API key authorization", + }); + + // Token authorizer + const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { + handler: authorizerFn, + identitySource: "method.request.header.x-tenant-id", + }); + + // Protected endpoint with mock integration + const protectedResource = api.root.addResource("protected"); + + protectedResource.addMethod( + "GET", + new apigateway.MockIntegration({ + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "application/json": '{ "message": "Access granted" }', + }, + }, + ], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { + "application/json": '{ "statusCode": 200 }', + }, + }), + { + authorizer: lambdaAuthorizer, + methodResponses: [{ statusCode: "200" }], + }, + ); + + new cdk.CfnOutput(this, "ApiUrl", { + value: api.url, + description: "URL of the API Gateway", + }); + + new cdk.CfnOutput(this, "TableName", { + value: table.tableName, + description: "DynamoDB table name for tenant-apikey mappings", + }); + } +} diff --git a/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js b/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js new file mode 100644 index 0000000000..71cf182a0d --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js @@ -0,0 +1,48 @@ +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient(); + +const TABLE_NAME = process.env.TABLE_NAME; + +exports.handler = async (event) => { + const tenantId = event.authorizationToken; + + if (!tenantId) { + throw new Error("Unauthorized: No tenant ID provided"); + } + + try { + const result = await client.send( + new GetItemCommand({ + TableName: TABLE_NAME, + Key: { tenantId: { S: tenantId } }, + }), + ); + + if (!result.Item || !result.Item.apiKey) { + throw new Error("Unauthorized: Tenant not found"); + } + + const apiKey = result.Item.apiKey.S; + + const authResponse = { + principalId: tenantId, + policyDocument: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Resource: event.methodArn, + Action: "execute-api:Invoke", + }, + ], + }, + context: { tenantId }, + usageIdentifierKey: apiKey, + }; + + return authResponse; + } catch (error) { + console.error("Authorization error:", error.message); + throw new Error("Unauthorized"); + } +}; diff --git a/apigw-dynamodb-tenantid-cdk/package.json b/apigw-dynamodb-tenantid-cdk/package.json new file mode 100644 index 0000000000..7e9def1262 --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/package.json @@ -0,0 +1,23 @@ +{ + "name": "apigw-dynamodb-apikey-cdk", + "version": "0.1.0", + "bin": { + "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/apigw-dynamodb-tenantid-cdk/tsconfig.json b/apigw-dynamodb-tenantid-cdk/tsconfig.json new file mode 100644 index 0000000000..aaa7dc510f --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 5e69d50579fb1eec5e54db9dc78023c6a3296c6d Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Wed, 22 Apr 2026 14:51:31 -0400 Subject: [PATCH 04/10] changes to add cognito --- apigw-dynamodb-tenantid-cdk/README.md | 55 +++++++++----- .../apigw-dynamodb-apikey-cdk.drawio | 26 ++++--- .../arbitrary keys/README.md | 61 +++++++++++----- .../apigw-arbitrary-keys.drawio | 34 +++++---- .../arbitrary keys/get-token.js | 71 +++++++++++++++++++ .../lib/apigw-arbitrary-keys-stack.ts | 26 ++++++- .../lib/lambda/arbitrary-key-authorizer.js | 19 ++++- .../arbitrary keys/package.json | 1 + .../example-pattern-dynamodb.json | 22 +++--- apigw-dynamodb-tenantid-cdk/get-token.js | 71 +++++++++++++++++++ .../lib/apigw-dynamodb-apikey-stack.ts | 29 +++++++- .../lib/lambda/dynamodb-authorizer.js | 20 ++++-- apigw-dynamodb-tenantid-cdk/package.json | 1 + 13 files changed, 360 insertions(+), 76 deletions(-) create mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js create mode 100644 apigw-dynamodb-tenantid-cdk/get-token.js diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md index 9a49e4d9bc..a12ef3bfe3 100644 --- a/apigw-dynamodb-tenantid-cdk/README.md +++ b/apigw-dynamodb-tenantid-cdk/README.md @@ -1,6 +1,6 @@ -# Amazon API Gateway with AWS Lambda Authorizer and DynamoDB for API Key Authentication +# Amazon API Gateway with Cognito, Lambda Authorizer, and DynamoDB for Tenant API Key Authentication -This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer validates the tenant ID from the request header, retrieves the corresponding API key from DynamoDB, and returns a policy document enabling API Gateway access. +This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk](https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk) @@ -33,28 +33,49 @@ Important: this application uses various AWS services and there are costs associ cdk deploy ``` -Note the outputs from the CDK deployment process. The output will include the API Gateway URL and the DynamoDB table name you'll need for testing. +Note the outputs from the CDK deployment process. The output will include the API Gateway URL, DynamoDB table name, Cognito User Pool ID, and User Pool Client ID. ## How it works ![Architecture Diagram](./apigw-dynamodb-apikey-cdk.drawio) -1. Client makes a request to the API with a tenant ID in the `x-tenant-id` header -2. API Gateway forwards the authorization request to the Lambda Authorizer -3. The Lambda Authorizer looks up the tenant ID in the DynamoDB table +1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim +2. Client makes a request to the API with the JWT in the `Authorization` header +3. API Gateway forwards the token to the Lambda Authorizer +4. The Lambda Authorizer decodes the JWT, extracts the `custom:tenantId` claim, and looks up the tenant in the DynamoDB table - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` - - If the tenant does not exist, the request is denied -4. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer + - If the tenant does not exist or the token is invalid, the request is denied +5. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. ## Testing -1. Get the DynamoDB table name and API Gateway URL from the deployment output: +1. Get the outputs from the deployment: ```bash # The outputs will be similar to ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY + ApigwDynamodbApikeyCdkStack.UserPoolId = us-east-1_XXXXXXXXX + ApigwDynamodbApikeyCdkStack.UserPoolClientId = XXXXXXXXXXXXXXXXXXXXXXXXXX + ``` + +1. Create a Cognito user with a tenantId: + ```bash + aws cognito-idp admin-create-user \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant \ + --temporary-password "TempPass1!" + ``` + +1. Set a permanent password for the user: + ```bash + aws cognito-idp admin-set-user-password \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --password "MySecurePass1!" \ + --permanent ``` 1. Insert a tenant mapping into the DynamoDB table: @@ -64,26 +85,22 @@ The DynamoDB table uses `tenantId` as the partition key and stores the correspon --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' ``` -1. Make a request to the protected endpoint with a valid tenant ID: +1. Get a token and call the API using the helper script: ```bash - curl -H "x-tenant-id: sample-tenant" https://REPLACE_WITH_API_URL/protected + node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID \ + --username user@example.com --password "MySecurePass1!" \ + --api-url https://REPLACE_WITH_API_URL/protected ``` If successful, you should receive a response like: ```json { "message": "Access granted" } ``` -1. Try with an invalid tenant ID: - ```bash - curl -H "x-tenant-id: invalid-tenant" https://REPLACE_WITH_API_URL/protected - ``` - You should receive an unauthorized error. - -1. Try without a tenant ID: +1. Try with an invalid or missing token: ```bash curl https://REPLACE_WITH_API_URL/protected ``` - You should also receive an unauthorized error. + You should receive an unauthorized error. ## Cleanup diff --git a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio index 6a17b10e95..7ade1dbbb7 100644 --- a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio +++ b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio @@ -10,43 +10,53 @@ + + + + + - + - + - + - + + + + + + - + - + - + - + diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md b/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md index ecbb63688c..e681766571 100644 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md @@ -1,17 +1,21 @@ -# API Gateway with Arbitrary Usage Identifier Keys (AUIK) +# API Gateway with Cognito, Arbitrary Usage Identifier Keys (AUIK), and Lambda Authorizer -This pattern demonstrates API Gateway with a Lambda authorizer that returns arbitrary usage identifier keys per the [AUIK specification](https://w.amazon.com/bin/view/AWS/Mobile/API_Gateway/Onboarding/Arbitrary_Usage_Identifier_Keys). +This pattern demonstrates API Gateway with a Cognito-authenticated Lambda authorizer that returns arbitrary usage identifier keys per the AUIK specification. ## How it works -1. Client sends a request with `x-tenant-id` header -2. API Gateway forwards to the Lambda authorizer -3. The authorizer: +![Architecture Diagram](./apigw-arbitrary-keys.drawio) + +1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim +2. Client sends a request with the JWT in the `Authorization` header +3. API Gateway forwards the token to the Lambda authorizer +4. The authorizer: + - Decodes the JWT and extracts the `custom:tenantId` claim - Extracts the **stage** from the method ARN - Generates a random 128-character arbitrary API key - Calls `GetUsagePlans` to find a usage plan associated with the API + stage - Returns `usageIdentifierKey` (always) and `usagePlanId` (if a plan exists for the stage) -4. API Gateway uses the returned key for throttling/quota enforcement against the usage plan +5. API Gateway uses the returned key for throttling/quota enforcement against the usage plan Per the AUIK docs: if no `usagePlanId` is returned, API Gateway treats `usageIdentifierKey` as a configured API Key. @@ -28,29 +32,50 @@ Per the AUIK docs: if no `usagePlanId` is returned, API Gateway treats `usageIde ## Prerequisites -- AWS account allowlisted +- AWS account allowlisted - Node.js, npm, AWS CDK installed ## Deploy ```bash -cd arbitrary-keys +cd "arbitrary keys" npm install cdk deploy ``` -## Test - -```bash -# Hit prod stage (has usage plan) -curl -H "x-tenant-id: my-tenant" https://.execute-api..amazonaws.com/prod/protected +Note the outputs: Prod/Dev API URLs, Usage Plan IDs, Cognito User Pool ID, and User Pool Client ID. -# Hit dev stage (has usage plan) -curl -H "x-tenant-id: my-tenant" https://.execute-api..amazonaws.com/dev/protected +## Test -# Without tenant ID (should fail) -curl https://.execute-api..amazonaws.com/prod/protected -``` +1. Create a Cognito user with a tenantId: + ```bash + aws cognito-idp admin-create-user \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=my-tenant \ + --temporary-password "TempPass1!" + ``` + +1. Set a permanent password: + ```bash + aws cognito-idp admin-set-user-password \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --password "MySecurePass1!" \ + --permanent + ``` + +1. Get a token and call the API using the helper script: + ```bash + node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID \ + --username user@example.com --password "MySecurePass1!" \ + --api-url https://.execute-api..amazonaws.com/prod/protected + ``` + +1. Without a token (should fail): + ```bash + curl https://.execute-api..amazonaws.com/prod/protected + ``` ## Cleanup diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio b/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio index 078ba7ebbc..14526604e6 100644 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio @@ -10,14 +10,19 @@ + + + + + - + - - + + @@ -27,12 +32,12 @@ - + - + @@ -45,33 +50,38 @@ + + + + + - + - + - + - + - + - + @@ -86,7 +96,7 @@ - + diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js b/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js new file mode 100644 index 0000000000..9a5f2b039a --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +// Usage: +// node get-token.js --user-pool-id --client-id --username --password --api-url +// +// Authenticates with Cognito, retrieves an ID token, and calls the API Gateway endpoint. + +const { + CognitoIdentityProviderClient, + InitiateAuthCommand, +} = require("@aws-sdk/client-cognito-identity-provider"); +const https = require("https"); +const http = require("http"); + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = {}; + for (let i = 0; i < args.length; i += 2) { + parsed[args[i].replace(/^--/, "")] = args[i + 1]; + } + const required = ["user-pool-id", "client-id", "username", "password", "api-url"]; + for (const key of required) { + if (!parsed[key]) { + console.error(`Missing required argument: --${key}`); + process.exit(1); + } + } + return parsed; +} + +async function getToken(clientId, username, password) { + const client = new CognitoIdentityProviderClient(); + const resp = await client.send( + new InitiateAuthCommand({ + AuthFlow: "USER_PASSWORD_AUTH", + ClientId: clientId, + AuthParameters: { USERNAME: username, PASSWORD: password }, + }) + ); + return resp.AuthenticationResult.IdToken; +} + +function callApi(url, token) { + return new Promise((resolve, reject) => { + const mod = url.startsWith("https") ? https : http; + const req = mod.get(url, { headers: { Authorization: `Bearer ${token}` } }, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => { + console.log(`Status: ${res.statusCode}`); + try { console.log(JSON.stringify(JSON.parse(body), null, 2)); } + catch { console.log(body); } + resolve(); + }); + }); + req.on("error", reject); + }); +} + +(async () => { + const args = parseArgs(); + try { + console.log("Authenticating with Cognito..."); + const token = await getToken(args["client-id"], args.username, args.password); + console.log("Token obtained. Calling API...\n"); + await callApi(args["api-url"], token); + } catch (err) { + console.error("Error:", err.message); + process.exit(1); + } +})(); diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts index fbf9a89ea6..ddf1dbdc58 100644 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts @@ -1,6 +1,7 @@ import * as cdk from "aws-cdk-lib"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; +import * as cognito from "aws-cdk-lib/aws-cognito"; import * as iam from "aws-cdk-lib/aws-iam"; import { Runtime } from "aws-cdk-lib/aws-lambda"; import * as path from "path"; @@ -10,6 +11,20 @@ export class ApigwArbitraryKeysStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + // Cognito User Pool with custom tenantId attribute + const userPool = new cognito.UserPool(this, "TenantUserPool", { + selfSignUpEnabled: false, + signInAliases: { email: true }, + customAttributes: { + tenantId: new cognito.StringAttribute({ mutable: false }), + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const userPoolClient = userPool.addClient("TenantUserPoolClient", { + authFlows: { userPassword: true }, + }); + const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { retention: cdk.aws_logs.RetentionDays.ONE_WEEK, removalPolicy: cdk.RemovalPolicy.DESTROY, @@ -38,9 +53,10 @@ export class ApigwArbitraryKeysStack extends cdk.Stack { deploy: false, }); + // Token authorizer using Authorization header (Cognito JWT) const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { handler: authorizerFn, - identitySource: "method.request.header.x-tenant-id", + identitySource: "method.request.header.Authorization", }); const protectedResource = api.root.addResource("protected"); @@ -99,5 +115,13 @@ export class ApigwArbitraryKeysStack extends cdk.Stack { new cdk.CfnOutput(this, "DevApiUrl", { value: devStage.urlForPath("/") }); new cdk.CfnOutput(this, "ProdUsagePlanId", { value: prodUsagePlan.usagePlanId }); new cdk.CfnOutput(this, "DevUsagePlanId", { value: devUsagePlan.usagePlanId }); + new cdk.CfnOutput(this, "UserPoolId", { + value: userPool.userPoolId, + description: "Cognito User Pool ID", + }); + new cdk.CfnOutput(this, "UserPoolClientId", { + value: userPoolClient.userPoolClientId, + description: "Cognito User Pool Client ID", + }); } } diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js index 7dcb722a02..f4cc454a8a 100644 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js @@ -4,11 +4,26 @@ import { randomBytes } from "crypto"; const apigwClient = new APIGatewayClient(); exports.handler = async (event) => { - const tenantId = event.authorizationToken; - if (!tenantId) { + const token = event.authorizationToken; + if (!token) { throw new Error("Unauthorized"); } + // Decode the JWT payload to extract tenantId + let tenantId; + try { + const payload = JSON.parse( + Buffer.from(token.replace(/^Bearer\s+/i, "").split(".")[1], "base64url").toString(), + ); + tenantId = payload["custom:tenantId"]; + } catch (err) { + throw new Error("Unauthorized: Invalid token"); + } + + if (!tenantId) { + throw new Error("Unauthorized: No tenant ID in claims"); + } + // arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{method}/{resource} const arnParts = event.methodArn.split(":"); const apiGatewayArnPart = arnParts[5].split("/"); diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json index 318dbb16a0..ea86cb9405 100644 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json +++ b/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json @@ -17,6 +17,7 @@ "typescript": "~5.6.3" }, "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.1034.0", "aws-cdk-lib": "2.189.1", "constructs": "^10.0.0" } diff --git a/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json b/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json index f86a74b81b..d22707439c 100644 --- a/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json +++ b/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json @@ -1,6 +1,6 @@ { - "title": "Amazon API Gateway, AWS Lambda Authorizer & DynamoDB for API Key Authentication", - "description": "Implement a secure API key-based authorization system using Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB for tenant-to-API-key mapping.", + "title": "Amazon API Gateway with Cognito, Lambda Authorizer & DynamoDB for Tenant API Key Authentication", + "description": "Implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito issues JWTs with a custom tenantId claim, and the Lambda authorizer maps tenants to API keys via DynamoDB.", "language": "TypeScript", "level": "200", @@ -8,8 +8,9 @@ "introBox": { "headline": "How it works", "text": [ - "This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", - "A DynamoDB table stores the mapping between tenant IDs and API keys. The Lambda authorizer derives the tenant ID from the authorization token and queries DynamoDB to retrieve the corresponding API key.", + "This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", + "Amazon Cognito authenticates users and issues JWTs (ID tokens) containing a custom tenantId claim.", + "The client sends the JWT in the Authorization header. API Gateway forwards the token to the Lambda authorizer, which decodes the JWT, extracts the custom:tenantId claim, and queries DynamoDB to retrieve the corresponding API key.", "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." ] @@ -24,6 +25,10 @@ }, "resources": { "bullets": [ + { + "text": "Amazon Cognito Developer Guide", + "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html" + }, { "text": "Lambda Authorizers for Amazon API Gateway", "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" @@ -43,19 +48,20 @@ ] }, "deploy": { - "text": ["npm install", "cdk deploy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\""] + "text": ["npm install", "cdk deploy"] }, "testing": { "text": [ + "Create a Cognito user: aws cognito-idp admin-create-user --user-pool-id USER_POOL_ID --username user@example.com --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant --temporary-password \"TempPass1!\"", + "Set a permanent password: aws cognito-idp admin-set-user-password --user-pool-id USER_POOL_ID --username user@example.com --password \"MySecurePass1!\" --permanent", "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", - "Make a request to the protected endpoint using the tenant ID: curl -H \"x-tenant-id: sample-tenant\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected", + "Get a token and call the API: node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID --username user@example.com --password \"MySecurePass1!\" --api-url https://REPLACE_WITH_API_URL/protected", "If successful, you should receive a response: { \"message\": \"Access granted\" }" ] }, "cleanup": { "text": [ - "Delete the CDK stack: cdk destroy --app \"npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts\"" + "Delete the CDK stack: cdk destroy" ] } - } diff --git a/apigw-dynamodb-tenantid-cdk/get-token.js b/apigw-dynamodb-tenantid-cdk/get-token.js new file mode 100644 index 0000000000..9a5f2b039a --- /dev/null +++ b/apigw-dynamodb-tenantid-cdk/get-token.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +// Usage: +// node get-token.js --user-pool-id --client-id --username --password --api-url +// +// Authenticates with Cognito, retrieves an ID token, and calls the API Gateway endpoint. + +const { + CognitoIdentityProviderClient, + InitiateAuthCommand, +} = require("@aws-sdk/client-cognito-identity-provider"); +const https = require("https"); +const http = require("http"); + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = {}; + for (let i = 0; i < args.length; i += 2) { + parsed[args[i].replace(/^--/, "")] = args[i + 1]; + } + const required = ["user-pool-id", "client-id", "username", "password", "api-url"]; + for (const key of required) { + if (!parsed[key]) { + console.error(`Missing required argument: --${key}`); + process.exit(1); + } + } + return parsed; +} + +async function getToken(clientId, username, password) { + const client = new CognitoIdentityProviderClient(); + const resp = await client.send( + new InitiateAuthCommand({ + AuthFlow: "USER_PASSWORD_AUTH", + ClientId: clientId, + AuthParameters: { USERNAME: username, PASSWORD: password }, + }) + ); + return resp.AuthenticationResult.IdToken; +} + +function callApi(url, token) { + return new Promise((resolve, reject) => { + const mod = url.startsWith("https") ? https : http; + const req = mod.get(url, { headers: { Authorization: `Bearer ${token}` } }, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => { + console.log(`Status: ${res.statusCode}`); + try { console.log(JSON.stringify(JSON.parse(body), null, 2)); } + catch { console.log(body); } + resolve(); + }); + }); + req.on("error", reject); + }); +} + +(async () => { + const args = parseArgs(); + try { + console.log("Authenticating with Cognito..."); + const token = await getToken(args["client-id"], args.username, args.password); + console.log("Token obtained. Calling API...\n"); + await callApi(args["api-url"], token); + } catch (err) { + console.error("Error:", err.message); + process.exit(1); + } +})(); diff --git a/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts b/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts index e3a0570b64..d3e4948758 100644 --- a/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts +++ b/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts @@ -3,6 +3,7 @@ import * as apigateway from "aws-cdk-lib/aws-apigateway"; import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; import { Runtime } from "aws-cdk-lib/aws-lambda"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import * as cognito from "aws-cdk-lib/aws-cognito"; import * as path from "path"; import { Construct } from "constructs"; @@ -17,6 +18,20 @@ export class ApigwDynamodbApikeyStack extends cdk.Stack { removalPolicy: cdk.RemovalPolicy.DESTROY, }); + // Cognito User Pool with custom tenantId attribute + const userPool = new cognito.UserPool(this, "TenantUserPool", { + selfSignUpEnabled: false, + signInAliases: { email: true }, + customAttributes: { + tenantId: new cognito.StringAttribute({ mutable: false }), + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const userPoolClient = userPool.addClient("TenantUserPoolClient", { + authFlows: { userPassword: true }, + }); + // Lambda authorizer log group const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { retention: cdk.aws_logs.RetentionDays.ONE_WEEK, @@ -45,10 +60,10 @@ export class ApigwDynamodbApikeyStack extends cdk.Stack { description: "API protected with DynamoDB-based API key authorization", }); - // Token authorizer + // Token authorizer using Authorization header (Cognito JWT) const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { handler: authorizerFn, - identitySource: "method.request.header.x-tenant-id", + identitySource: "method.request.header.Authorization", }); // Protected endpoint with mock integration @@ -85,5 +100,15 @@ export class ApigwDynamodbApikeyStack extends cdk.Stack { value: table.tableName, description: "DynamoDB table name for tenant-apikey mappings", }); + + new cdk.CfnOutput(this, "UserPoolId", { + value: userPool.userPoolId, + description: "Cognito User Pool ID", + }); + + new cdk.CfnOutput(this, "UserPoolClientId", { + value: userPoolClient.userPoolClientId, + description: "Cognito User Pool Client ID", + }); } } diff --git a/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js b/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js index 71cf182a0d..995f93f9fa 100644 --- a/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js +++ b/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js @@ -4,13 +4,23 @@ const client = new DynamoDBClient(); const TABLE_NAME = process.env.TABLE_NAME; exports.handler = async (event) => { - const tenantId = event.authorizationToken; + const token = event.authorizationToken; - if (!tenantId) { - throw new Error("Unauthorized: No tenant ID provided"); + if (!token) { + throw new Error("Unauthorized: No token provided"); } try { + // Decode the JWT payload (Cognito token is already validated by API Gateway if needed) + const payload = JSON.parse( + Buffer.from(token.replace(/^Bearer\s+/i, "").split(".")[1], "base64url").toString(), + ); + const tenantId = payload["custom:tenantId"]; + + if (!tenantId) { + throw new Error("Unauthorized: No tenant ID in claims"); + } + const result = await client.send( new GetItemCommand({ TableName: TABLE_NAME, @@ -24,7 +34,7 @@ exports.handler = async (event) => { const apiKey = result.Item.apiKey.S; - const authResponse = { + return { principalId: tenantId, policyDocument: { Version: "2012-10-17", @@ -39,8 +49,6 @@ exports.handler = async (event) => { context: { tenantId }, usageIdentifierKey: apiKey, }; - - return authResponse; } catch (error) { console.error("Authorization error:", error.message); throw new Error("Unauthorized"); diff --git a/apigw-dynamodb-tenantid-cdk/package.json b/apigw-dynamodb-tenantid-cdk/package.json index 7e9def1262..c74b3300e3 100644 --- a/apigw-dynamodb-tenantid-cdk/package.json +++ b/apigw-dynamodb-tenantid-cdk/package.json @@ -17,6 +17,7 @@ "typescript": "~5.6.3" }, "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.1034.0", "aws-cdk-lib": "2.189.1", "constructs": "^10.0.0" } From 1739bc0dc31724be76cda65fcc6c003c425e97c3 Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Thu, 28 May 2026 13:56:55 -0400 Subject: [PATCH 05/10] Added jpg file and updated readme --- apigw-dynamodb-tenantid-cdk/README.md | 15 ++++++++++++--- .../apigw-dynamodb-apikey-cdk.jpg | Bin 0 -> 38960 bytes 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md index a12ef3bfe3..9b73e5a890 100644 --- a/apigw-dynamodb-tenantid-cdk/README.md +++ b/apigw-dynamodb-tenantid-cdk/README.md @@ -1,8 +1,17 @@ # Amazon API Gateway with Cognito, Lambda Authorizer, and DynamoDB for Tenant API Key Authentication +API Gateway's usage plans and API keys are fundamentally disconnected from authorization tokens. +Usage plans enforce rate limits via API keys, but auth tokens (JWTs from Cognito, Auth0, etc.) carry identity and permissions — these are two separate systems with no native link. This means customers cannot simply issue an auth token that inherently comes with rate-limiting attached. At scale (millions of auth tokens across thousands of tenants), managing this disconnect manually becomes untenable. + This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. -Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk](https://serverlessland.com/patterns/apigw-dynamodb-apikey-cdk) +What this pattern solves: + - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via usageIdentifierKey, a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. + - Scales to millions of tokens per tenant — Any number of JWTs can map to the same tenant's API key. You don't need a 1:1 relationship between auth tokens and API keys. A tenant can have millions of active tokens, but they all resolve to one API key and one rate-limit policy — making management tractable at scale. + - Eliminates per-application auth logic — Backend services no longer independently validate tenants or enforce limits. The gateway handles both centrally, preventing inconsistency and reducing overhead. + - Prevents noisy neighbors transparently — Tenants only interact with their auth credentials. The API key mapping and usage plan enforcement happen internally, so rate-limiting is invisible to consumers but enforced consistently. + - Makes auth and usage a single operational concern — Onboarding a new tenant means: create identity (Cognito/Auth0), create API key with a usage plan, store the mapping in DynamoDB. One workflow governs both auth and throttling, rather than managing them as separate systems that drift apart over time. + Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. @@ -37,13 +46,13 @@ Note the outputs from the CDK deployment process. The output will include the AP ## How it works -![Architecture Diagram](./apigw-dynamodb-apikey-cdk.drawio) +![Architecture Diagram](./apigw-dynamodb-apikey-cdk.jpg) 1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim 2. Client makes a request to the API with the JWT in the `Authorization` header 3. API Gateway forwards the token to the Lambda Authorizer 4. The Lambda Authorizer decodes the JWT, extracts the `custom:tenantId` claim, and looks up the tenant in the DynamoDB table - - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` + - If the tenant exists, the associated API key is retrieved and returned in the authorization context via [`usageIdentifierKey`] (https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html) - If the tenant does not exist or the token is invalid, the request is denied 5. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer diff --git a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8db079e1567eb0c4d860f4ec534bd0bfd202f7b GIT binary patch literal 38960 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%0AdxSqK2Vpe64rbAtH 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 literal 0 HcmV?d00001 From c6e8226a34b0b39828e526f703427d88d9b060c4 Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Thu, 28 May 2026 14:14:26 -0400 Subject: [PATCH 06/10] Updated readme --- apigw-dynamodb-tenantid-cdk/README.md | 4 +- .../apigw-dynamodb-apikey-cdk.drawio | 66 --------- .../arbitrary keys/README.md | 84 ------------ .../apigw-arbitrary-keys.drawio | 111 --------------- .../bin/apigw-arbitrary-keys.ts | 11 -- .../arbitrary keys/cdk.json | 21 --- .../arbitrary keys/get-token.js | 71 ---------- .../lib/apigw-arbitrary-keys-stack.ts | 127 ------------------ .../lib/lambda/arbitrary-key-authorizer.js | 70 ---------- .../arbitrary keys/package.json | 24 ---- .../arbitrary keys/tsconfig.json | 23 ---- .../example-pattern-dynamodb.json | 67 --------- 12 files changed, 2 insertions(+), 677 deletions(-) delete mode 100644 apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json delete mode 100644 apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json delete mode 100644 apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md index 9b73e5a890..75d8556763 100644 --- a/apigw-dynamodb-tenantid-cdk/README.md +++ b/apigw-dynamodb-tenantid-cdk/README.md @@ -6,7 +6,7 @@ Usage plans enforce rate limits via API keys, but auth tokens (JWTs from Cognito This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. What this pattern solves: - - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via usageIdentifierKey, a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. + - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via [usageIdentifierKey] (https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html), a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. - Scales to millions of tokens per tenant — Any number of JWTs can map to the same tenant's API key. You don't need a 1:1 relationship between auth tokens and API keys. A tenant can have millions of active tokens, but they all resolve to one API key and one rate-limit policy — making management tractable at scale. - Eliminates per-application auth logic — Backend services no longer independently validate tenants or enforce limits. The gateway handles both centrally, preventing inconsistency and reducing overhead. - Prevents noisy neighbors transparently — Tenants only interact with their auth credentials. The API key mapping and usage plan enforcement happen internally, so rate-limiting is invisible to consumers but enforced consistently. @@ -52,7 +52,7 @@ Note the outputs from the CDK deployment process. The output will include the AP 2. Client makes a request to the API with the JWT in the `Authorization` header 3. API Gateway forwards the token to the Lambda Authorizer 4. The Lambda Authorizer decodes the JWT, extracts the `custom:tenantId` claim, and looks up the tenant in the DynamoDB table - - If the tenant exists, the associated API key is retrieved and returned in the authorization context via [`usageIdentifierKey`] (https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html) + - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` - If the tenant does not exist or the token is invalid, the request is denied 5. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer diff --git a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio deleted file mode 100644 index 7ade1dbbb7..0000000000 --- a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.drawio +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md b/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md deleted file mode 100644 index e681766571..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# API Gateway with Cognito, Arbitrary Usage Identifier Keys (AUIK), and Lambda Authorizer - -This pattern demonstrates API Gateway with a Cognito-authenticated Lambda authorizer that returns arbitrary usage identifier keys per the AUIK specification. - -## How it works - -![Architecture Diagram](./apigw-arbitrary-keys.drawio) - -1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim -2. Client sends a request with the JWT in the `Authorization` header -3. API Gateway forwards the token to the Lambda authorizer -4. The authorizer: - - Decodes the JWT and extracts the `custom:tenantId` claim - - Extracts the **stage** from the method ARN - - Generates a random 128-character arbitrary API key - - Calls `GetUsagePlans` to find a usage plan associated with the API + stage - - Returns `usageIdentifierKey` (always) and `usagePlanId` (if a plan exists for the stage) -5. API Gateway uses the returned key for throttling/quota enforcement against the usage plan - -Per the AUIK docs: if no `usagePlanId` is returned, API Gateway treats `usageIdentifierKey` as a configured API Key. - -## Authorizer Response Format - -```json -{ - "principalId": "tenant-id", - "policyDocument": { ... }, - "usageIdentifierKey": "<128-char-random-key>", - "usagePlanId": "" -} -``` - -## Prerequisites - -- AWS account allowlisted -- Node.js, npm, AWS CDK installed - -## Deploy - -```bash -cd "arbitrary keys" -npm install -cdk deploy -``` - -Note the outputs: Prod/Dev API URLs, Usage Plan IDs, Cognito User Pool ID, and User Pool Client ID. - -## Test - -1. Create a Cognito user with a tenantId: - ```bash - aws cognito-idp admin-create-user \ - --user-pool-id USER_POOL_ID \ - --username user@example.com \ - --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=my-tenant \ - --temporary-password "TempPass1!" - ``` - -1. Set a permanent password: - ```bash - aws cognito-idp admin-set-user-password \ - --user-pool-id USER_POOL_ID \ - --username user@example.com \ - --password "MySecurePass1!" \ - --permanent - ``` - -1. Get a token and call the API using the helper script: - ```bash - node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID \ - --username user@example.com --password "MySecurePass1!" \ - --api-url https://.execute-api..amazonaws.com/prod/protected - ``` - -1. Without a token (should fail): - ```bash - curl https://.execute-api..amazonaws.com/prod/protected - ``` - -## Cleanup - -```bash -cdk destroy -``` diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio b/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio deleted file mode 100644 index 14526604e6..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/apigw-arbitrary-keys.drawio +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts b/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts deleted file mode 100644 index 50801425e7..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/bin/apigw-arbitrary-keys.ts +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node -import * as cdk from "aws-cdk-lib"; -import { ApigwArbitraryKeysStack } from "../lib/apigw-arbitrary-keys-stack"; - -const app = new cdk.App(); -new ApigwArbitraryKeysStack(app, "ApigwArbitraryKeysCdkStack", { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION, - }, -}); diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json deleted file mode 100644 index 28fd66ad2b..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/cdk.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts bin/apigw-arbitrary-keys.ts", - "watch": { - "include": ["**"], - "exclude": [ - "README.md", "cdk*.json", "**/*.d.ts", "**/*.js", - "tsconfig.json", "package*.json", "yarn.lock", "node_modules", "test" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], - "@aws-cdk/aws-iam:minimizePolicies": true, - "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, - "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, - "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, - "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, - "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true - } -} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js b/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js deleted file mode 100644 index 9a5f2b039a..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/get-token.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -// Usage: -// node get-token.js --user-pool-id --client-id --username --password --api-url -// -// Authenticates with Cognito, retrieves an ID token, and calls the API Gateway endpoint. - -const { - CognitoIdentityProviderClient, - InitiateAuthCommand, -} = require("@aws-sdk/client-cognito-identity-provider"); -const https = require("https"); -const http = require("http"); - -function parseArgs() { - const args = process.argv.slice(2); - const parsed = {}; - for (let i = 0; i < args.length; i += 2) { - parsed[args[i].replace(/^--/, "")] = args[i + 1]; - } - const required = ["user-pool-id", "client-id", "username", "password", "api-url"]; - for (const key of required) { - if (!parsed[key]) { - console.error(`Missing required argument: --${key}`); - process.exit(1); - } - } - return parsed; -} - -async function getToken(clientId, username, password) { - const client = new CognitoIdentityProviderClient(); - const resp = await client.send( - new InitiateAuthCommand({ - AuthFlow: "USER_PASSWORD_AUTH", - ClientId: clientId, - AuthParameters: { USERNAME: username, PASSWORD: password }, - }) - ); - return resp.AuthenticationResult.IdToken; -} - -function callApi(url, token) { - return new Promise((resolve, reject) => { - const mod = url.startsWith("https") ? https : http; - const req = mod.get(url, { headers: { Authorization: `Bearer ${token}` } }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => { - console.log(`Status: ${res.statusCode}`); - try { console.log(JSON.stringify(JSON.parse(body), null, 2)); } - catch { console.log(body); } - resolve(); - }); - }); - req.on("error", reject); - }); -} - -(async () => { - const args = parseArgs(); - try { - console.log("Authenticating with Cognito..."); - const token = await getToken(args["client-id"], args.username, args.password); - console.log("Token obtained. Calling API...\n"); - await callApi(args["api-url"], token); - } catch (err) { - console.error("Error:", err.message); - process.exit(1); - } -})(); diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts deleted file mode 100644 index ddf1dbdc58..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/apigw-arbitrary-keys-stack.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as apigateway from "aws-cdk-lib/aws-apigateway"; -import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; -import * as cognito from "aws-cdk-lib/aws-cognito"; -import * as iam from "aws-cdk-lib/aws-iam"; -import { Runtime } from "aws-cdk-lib/aws-lambda"; -import * as path from "path"; -import { Construct } from "constructs"; - -export class ApigwArbitraryKeysStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - - // Cognito User Pool with custom tenantId attribute - const userPool = new cognito.UserPool(this, "TenantUserPool", { - selfSignUpEnabled: false, - signInAliases: { email: true }, - customAttributes: { - tenantId: new cognito.StringAttribute({ mutable: false }), - }, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const userPoolClient = userPool.addClient("TenantUserPoolClient", { - authFlows: { userPassword: true }, - }); - - const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { - retention: cdk.aws_logs.RetentionDays.ONE_WEEK, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const authorizerFn = new lambda.NodejsFunction(this, "ArbitraryKeyAuthorizer", { - runtime: Runtime.NODEJS_24_X, - entry: path.join(__dirname, "lambda/arbitrary-key-authorizer.js"), - timeout: cdk.Duration.seconds(10), - bundling: { externalModules: ["@aws-sdk/*"] }, - logGroup: authorizerLogGroup, - }); - - // Allow lambda to read usage plans - authorizerFn.addToRolePolicy( - new iam.PolicyStatement({ - actions: ["apigateway:GET"], - resources: [`arn:aws:apigateway:${this.region}::/usageplans`], - }), - ); - - // API Gateway with key source from AUTHORIZER - const api = new apigateway.RestApi(this, "ApiGateway", { - restApiName: "Arbitrary Usage Identifier Key Service", - apiKeySourceType: apigateway.ApiKeySourceType.AUTHORIZER, - deploy: false, - }); - - // Token authorizer using Authorization header (Cognito JWT) - const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { - handler: authorizerFn, - identitySource: "method.request.header.Authorization", - }); - - const protectedResource = api.root.addResource("protected"); - protectedResource.addMethod( - "GET", - new apigateway.MockIntegration({ - integrationResponses: [ - { - statusCode: "200", - responseTemplates: { - "application/json": '{ "message": "Access granted" }', - }, - }, - ], - passthroughBehavior: apigateway.PassthroughBehavior.NEVER, - requestTemplates: { "application/json": '{ "statusCode": 200 }' }, - }), - { - authorizer: lambdaAuthorizer, - apiKeyRequired: true, - methodResponses: [{ statusCode: "200" }], - }, - ); - - // Prod stage - const prodDeployment = new apigateway.Deployment(this, "ProdDeployment", { api }); - const prodStage = new apigateway.Stage(this, "ProdStage", { - deployment: prodDeployment, - stageName: "prod", - }); - api.deploymentStage = prodStage; - - // Dev stage - const devDeployment = new apigateway.Deployment(this, "DevDeployment", { api }); - const devStage = new apigateway.Stage(this, "DevStage", { - deployment: devDeployment, - stageName: "dev", - }); - - // Usage plans per stage - const prodUsagePlan = new apigateway.UsagePlan(this, "ProdUsagePlan", { - name: "ProdUsagePlan", - throttle: { rateLimit: 100, burstLimit: 50 }, - quota: { limit: 10000, period: apigateway.Period.MONTH }, - apiStages: [{ api, stage: prodStage }], - }); - - const devUsagePlan = new apigateway.UsagePlan(this, "DevUsagePlan", { - name: "DevUsagePlan", - throttle: { rateLimit: 10, burstLimit: 5 }, - quota: { limit: 1000, period: apigateway.Period.MONTH }, - apiStages: [{ api, stage: devStage }], - }); - - new cdk.CfnOutput(this, "ProdApiUrl", { value: prodStage.urlForPath("/") }); - new cdk.CfnOutput(this, "DevApiUrl", { value: devStage.urlForPath("/") }); - new cdk.CfnOutput(this, "ProdUsagePlanId", { value: prodUsagePlan.usagePlanId }); - new cdk.CfnOutput(this, "DevUsagePlanId", { value: devUsagePlan.usagePlanId }); - new cdk.CfnOutput(this, "UserPoolId", { - value: userPool.userPoolId, - description: "Cognito User Pool ID", - }); - new cdk.CfnOutput(this, "UserPoolClientId", { - value: userPoolClient.userPoolClientId, - description: "Cognito User Pool Client ID", - }); - } -} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js b/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js deleted file mode 100644 index f4cc454a8a..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/lib/lambda/arbitrary-key-authorizer.js +++ /dev/null @@ -1,70 +0,0 @@ -import { APIGatewayClient, GetUsagePlansCommand } from "@aws-sdk/client-api-gateway"; -import { randomBytes } from "crypto"; - -const apigwClient = new APIGatewayClient(); - -exports.handler = async (event) => { - const token = event.authorizationToken; - if (!token) { - throw new Error("Unauthorized"); - } - - // Decode the JWT payload to extract tenantId - let tenantId; - try { - const payload = JSON.parse( - Buffer.from(token.replace(/^Bearer\s+/i, "").split(".")[1], "base64url").toString(), - ); - tenantId = payload["custom:tenantId"]; - } catch (err) { - throw new Error("Unauthorized: Invalid token"); - } - - if (!tenantId) { - throw new Error("Unauthorized: No tenant ID in claims"); - } - - // arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{method}/{resource} - const arnParts = event.methodArn.split(":"); - const apiGatewayArnPart = arnParts[5].split("/"); - const apiId = apiGatewayArnPart[0]; - const stage = apiGatewayArnPart[1]; - - // Generate arbitrary key (128 chars max) - const usageIdentifierKey = randomBytes(64).toString("hex").substring(0, 128); - - // Find usage plan associated with this API's stage - let usagePlanId; - try { - const resp = await apigwClient.send(new GetUsagePlansCommand({})); - const plan = resp.items?.find((p) => - p.apiStages?.some((s) => s.apiId === apiId && s.stage === stage) - ); - if (plan) { - usagePlanId = plan.id; - } - } catch (err) { - console.error("Error fetching usage plans:", err.message); - } - - const authResponse = { - principalId: tenantId, - policyDocument: { - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Resource: event.methodArn, - Action: "execute-api:Invoke", - }, - ], - }, - usageIdentifierKey, - }; - - if (usagePlanId) { - authResponse.usagePlanId = usagePlanId; - } - - return authResponse; -}; diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json deleted file mode 100644 index ea86cb9405..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "apigw-arbitrary-keys-cdk", - "version": "0.1.0", - "bin": { - "apigw-arbitrary-keys-cdk": "bin/apigw-arbitrary-keys.js" - }, - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "cdk": "cdk" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1003.0", - "esbuild": "^0.25.1", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - }, - "dependencies": { - "@aws-sdk/client-cognito-identity-provider": "^3.1034.0", - "aws-cdk-lib": "2.189.1", - "constructs": "^10.0.0" - } -} diff --git a/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json b/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json deleted file mode 100644 index 464ed774ba..0000000000 --- a/apigw-dynamodb-tenantid-cdk/arbitrary keys/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["es2020", "dom"], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "typeRoots": ["./node_modules/@types"] - }, - "exclude": ["node_modules", "cdk.out"] -} diff --git a/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json b/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json deleted file mode 100644 index d22707439c..0000000000 --- a/apigw-dynamodb-tenantid-cdk/example-pattern-dynamodb.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "title": "Amazon API Gateway with Cognito, Lambda Authorizer & DynamoDB for Tenant API Key Authentication", - "description": "Implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito issues JWTs with a custom tenantId claim, and the Lambda authorizer maps tenants to API keys via DynamoDB.", - - "language": "TypeScript", - "level": "200", - "framework": "AWS CDK", - "introBox": { - "headline": "How it works", - "text": [ - "This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", - "Amazon Cognito authenticates users and issues JWTs (ID tokens) containing a custom tenantId claim.", - "The client sends the JWT in the Authorization header. API Gateway forwards the token to the Lambda authorizer, which decodes the JWT, extracts the custom:tenantId claim, and queries DynamoDB to retrieve the corresponding API key.", - "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", - "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." - ] - }, - "gitHub": { - "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-dynamodb-apikey-cdk", - "templateURL": "serverless-patterns/apigw-dynamodb-apikey-cdk", - "projectFolder": "apigw-dynamodb-apikey-cdk", - "templateFile": "lib/apigw-dynamodb-apikey-stack.ts" - } - }, - "resources": { - "bullets": [ - { - "text": "Amazon Cognito Developer Guide", - "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html" - }, - { - "text": "Lambda Authorizers for Amazon API Gateway", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" - }, - { - "text": "Amazon DynamoDB Developer Guide", - "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" - }, - { - "text": "Amazon API Gateway - REST APIs", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" - }, - { - "text": "API Gateway Usage Plans", - "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" - } - ] - }, - "deploy": { - "text": ["npm install", "cdk deploy"] - }, - "testing": { - "text": [ - "Create a Cognito user: aws cognito-idp admin-create-user --user-pool-id USER_POOL_ID --username user@example.com --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant --temporary-password \"TempPass1!\"", - "Set a permanent password: aws cognito-idp admin-set-user-password --user-pool-id USER_POOL_ID --username user@example.com --password \"MySecurePass1!\" --permanent", - "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", - "Get a token and call the API: node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID --username user@example.com --password \"MySecurePass1!\" --api-url https://REPLACE_WITH_API_URL/protected", - "If successful, you should receive a response: { \"message\": \"Access granted\" }" - ] - }, - "cleanup": { - "text": [ - "Delete the CDK stack: cdk destroy" - ] - } -} From e78d8959ad4d6526b4991dfc8b820386fae8d96c Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Thu, 28 May 2026 14:18:57 -0400 Subject: [PATCH 07/10] changed link --- apigw-dynamodb-tenantid-cdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md index 75d8556763..687e1be4d5 100644 --- a/apigw-dynamodb-tenantid-cdk/README.md +++ b/apigw-dynamodb-tenantid-cdk/README.md @@ -6,7 +6,7 @@ Usage plans enforce rate limits via API keys, but auth tokens (JWTs from Cognito This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. What this pattern solves: - - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via [usageIdentifierKey] (https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html), a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. + - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via [usageIdentifierKey](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html), a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. - Scales to millions of tokens per tenant — Any number of JWTs can map to the same tenant's API key. You don't need a 1:1 relationship between auth tokens and API keys. A tenant can have millions of active tokens, but they all resolve to one API key and one rate-limit policy — making management tractable at scale. - Eliminates per-application auth logic — Backend services no longer independently validate tenants or enforce limits. The gateway handles both centrally, preventing inconsistency and reducing overhead. - Prevents noisy neighbors transparently — Tenants only interact with their auth credentials. The API key mapping and usage plan enforcement happen internally, so rate-limiting is invisible to consumers but enforced consistently. From 6bf4bc5777a1fa0c0ad8bb0b68e5bab53cfd78e4 Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Mon, 1 Jun 2026 21:54:56 -0400 Subject: [PATCH 08/10] apigw tenantid pattern --- apigw-APIKey-tenantid-cdk/README.md | 124 ++++++++++++++++++ .../apigw-dynamodb-apikey-cdk.jpg | Bin 0 -> 38960 bytes apigw-APIKey-tenantid-cdk/cdk.json | 88 +++++++++++++ apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh | 8 ++ ...attern-tenantbasedAPIkeyauthorization.json | 67 ++++++++++ apigw-APIKey-tenantid-cdk/get-token.js | 71 ++++++++++ apigw-APIKey-tenantid-cdk/package.json | 24 ++++ .../src/bin/apigw-dynamodb-apikey-cdk.ts | 11 ++ .../src/lib/apigw-dynamodb-apikey-stack.ts | 114 ++++++++++++++++ .../src/lib/lambda/dynamodb-authorizer.js | 56 ++++++++ apigw-APIKey-tenantid-cdk/tsconfig.json | 31 +++++ 11 files changed, 594 insertions(+) create mode 100644 apigw-APIKey-tenantid-cdk/README.md create mode 100644 apigw-APIKey-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg create mode 100644 apigw-APIKey-tenantid-cdk/cdk.json create mode 100644 apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh create mode 100644 apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json create mode 100644 apigw-APIKey-tenantid-cdk/get-token.js create mode 100644 apigw-APIKey-tenantid-cdk/package.json create mode 100644 apigw-APIKey-tenantid-cdk/src/bin/apigw-dynamodb-apikey-cdk.ts create mode 100644 apigw-APIKey-tenantid-cdk/src/lib/apigw-dynamodb-apikey-stack.ts create mode 100644 apigw-APIKey-tenantid-cdk/src/lib/lambda/dynamodb-authorizer.js create mode 100644 apigw-APIKey-tenantid-cdk/tsconfig.json diff --git a/apigw-APIKey-tenantid-cdk/README.md b/apigw-APIKey-tenantid-cdk/README.md new file mode 100644 index 0000000000..119d82bc73 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/README.md @@ -0,0 +1,124 @@ +# Amazon API Gateway with Cognito, Lambda Authorizer, and DynamoDB for Tenant API Key Authentication + +API Gateway's usage plans and API keys are fundamentally disconnected from authorization tokens. +Usage plans enforce rate limits via API keys, but auth tokens (JWTs from Cognito, Auth0, etc.) carry identity and permissions — these are two separate systems with no native link. This means customers cannot simply issue an auth token that inherently comes with rate-limiting attached. At scale (millions of auth tokens across thousands of tenants), managing this disconnect manually becomes untenable. + +This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. + +What this pattern solves: + - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via [usageIdentifierKey](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html), a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. + - Scales to millions of tokens per tenant — Any number of JWTs can map to the same tenant's API key. You don't need a 1:1 relationship between auth tokens and API keys. A tenant can have millions of active tokens, but they all resolve to one API key and one rate-limit policy — making management tractable at scale. + - Eliminates per-application auth logic — Backend services no longer independently validate tenants or enforce limits. The gateway handles both centrally, preventing inconsistency and reducing overhead. + - Prevents noisy neighbors transparently — Tenants only interact with their auth credentials. The API key mapping and usage plan enforcement happen internally, so rate-limiting is invisible to consumers but enforced consistently. + - Makes auth and usage a single operational concern — Onboarding a new tenant means: create identity (Cognito/Auth0), create API key with a usage plan, store the mapping in DynamoDB. One workflow governs both auth and throttling, rather than managing them as separate systems that drift apart over time. + + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +* [Node.js and npm](https://nodejs.org/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd apigw-APIKey-tenantid-cdk + ``` +1. Install dependencies: + ``` + npm install + ``` +1. Deploy the stack: + ``` + cdk deploy + ``` + +Note the outputs from the CDK deployment process. The output will include the API Gateway URL, DynamoDB table name, Cognito User Pool ID, and User Pool Client ID. + +## How it works + +![Architecture Diagram](./apigw-dynamodb-apikey-cdk.jpg) + +1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim +2. Client makes a request to the API with the JWT in the `Authorization` header +3. API Gateway forwards the token to the Lambda Authorizer +4. The Lambda Authorizer decodes the JWT, extracts the `custom:tenantId` claim, and looks up the tenant in the DynamoDB table + - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` + - If the tenant does not exist or the token is invalid, the request is denied +5. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer + +The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. + +## Testing + +1. Get the outputs from the deployment: + ```bash + # The outputs will be similar to + ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ + ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY + ApigwDynamodbApikeyCdkStack.UserPoolId = us-east-1_XXXXXXXXX + ApigwDynamodbApikeyCdkStack.UserPoolClientId = XXXXXXXXXXXXXXXXXXXXXXXXXX + ``` + +1. Create a Cognito user with a tenantId: + ```bash + aws cognito-idp admin-create-user \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant \ + --temporary-password "TempPass1!" + ``` + +1. Set a permanent password for the user: + ```bash + aws cognito-idp admin-set-user-password \ + --user-pool-id USER_POOL_ID \ + --username user@example.com \ + --password "MySecurePass1!" \ + --permanent + ``` + +1. Insert a tenant mapping into the DynamoDB table: + ```bash + aws dynamodb put-item \ + --table-name TABLE_NAME \ + --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' + ``` + +1. Get a token and call the API using the helper script: + ```bash + node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID \ + --username user@example.com --password "MySecurePass1!" \ + --api-url https://REPLACE_WITH_API_URL/protected + ``` + If successful, you should receive a response like: + ```json + { "message": "Access granted" } + ``` + +1. Try with an invalid or missing token: + ```bash + curl https://REPLACE_WITH_API_URL/protected + ``` + You should receive an unauthorized error. + +## Cleanup + +1. Delete the stack: + ```bash + cdk destroy + ``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-APIKey-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg b/apigw-APIKey-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8db079e1567eb0c4d860f4ec534bd0bfd202f7b GIT binary patch literal 38960 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%0AdxSqK2Vpe64rbAtH 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 literal 0 HcmV?d00001 diff --git a/apigw-APIKey-tenantid-cdk/cdk.json b/apigw-APIKey-tenantid-cdk/cdk.json new file mode 100644 index 0000000000..36520c8e39 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx ts-node --prefer-ts-exts src/bin/apigw-dynamodb-apikey-cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true + } +} diff --git a/apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh b/apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh new file mode 100644 index 0000000000..83554368c6 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +APP="npx ts-node --prefer-ts-exts src/bin/apigw-dynamodb-apikey-cdk.ts" +STACK_NAME="ApigwDynamodbApikeyCdkStack" + +npm install +cdk deploy "$STACK_NAME" --app "$APP" "$@" diff --git a/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json b/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json new file mode 100644 index 0000000000..a6073ca116 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json @@ -0,0 +1,67 @@ +{ + "title": "Amazon API Gateway with Cognito, Lambda Authorizer & DynamoDB for Tenant API Key Authentication", + "description": "Implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito issues JWTs with a custom tenantId claim, and the Lambda authorizer maps tenants to API keys via DynamoDB.", + + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, Lambda Authorizer, and Amazon DynamoDB.", + "Amazon Cognito authenticates users and issues JWTs (ID tokens) containing a custom tenantId claim.", + "The client sends the JWT in the Authorization header. API Gateway forwards the token to the Lambda authorizer, which decodes the JWT, extracts the custom:tenantId claim, and queries DynamoDB to retrieve the corresponding API key.", + "The authorizer returns a policy document with the usageIdentifierKey set to the API key, enabling API Gateway usage plan integration.", + "The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-APIKey-tenantid-cdk", + "templateURL": "serverless-patterns/apigw-APIKey-tenantid-cdk", + "projectFolder": "apigw-APIKey-tenantid-cdk", + "templateFile": "src/lib/apigw-dynamodb-apikey-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Cognito Developer Guide", + "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html" + }, + { + "text": "Lambda Authorizers for Amazon API Gateway", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" + }, + { + "text": "Amazon DynamoDB Developer Guide", + "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + }, + { + "text": "API Gateway Usage Plans", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" + } + ] + }, + "deploy": { + "text": ["npm install", "cdk deploy"] + }, + "testing": { + "text": [ + "Create a Cognito user: aws cognito-idp admin-create-user --user-pool-id USER_POOL_ID --username user@example.com --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant --temporary-password \"TempPass1!\"", + "Set a permanent password: aws cognito-idp admin-set-user-password --user-pool-id USER_POOL_ID --username user@example.com --password \"MySecurePass1!\" --permanent", + "Insert a tenant mapping into the DynamoDB table: aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'", + "Get a token and call the API: node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID --username user@example.com --password \"MySecurePass1!\" --api-url https://REPLACE_WITH_API_URL/protected", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy" + ] + } +} diff --git a/apigw-APIKey-tenantid-cdk/get-token.js b/apigw-APIKey-tenantid-cdk/get-token.js new file mode 100644 index 0000000000..9a5f2b039a --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/get-token.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +// Usage: +// node get-token.js --user-pool-id --client-id --username --password --api-url +// +// Authenticates with Cognito, retrieves an ID token, and calls the API Gateway endpoint. + +const { + CognitoIdentityProviderClient, + InitiateAuthCommand, +} = require("@aws-sdk/client-cognito-identity-provider"); +const https = require("https"); +const http = require("http"); + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = {}; + for (let i = 0; i < args.length; i += 2) { + parsed[args[i].replace(/^--/, "")] = args[i + 1]; + } + const required = ["user-pool-id", "client-id", "username", "password", "api-url"]; + for (const key of required) { + if (!parsed[key]) { + console.error(`Missing required argument: --${key}`); + process.exit(1); + } + } + return parsed; +} + +async function getToken(clientId, username, password) { + const client = new CognitoIdentityProviderClient(); + const resp = await client.send( + new InitiateAuthCommand({ + AuthFlow: "USER_PASSWORD_AUTH", + ClientId: clientId, + AuthParameters: { USERNAME: username, PASSWORD: password }, + }) + ); + return resp.AuthenticationResult.IdToken; +} + +function callApi(url, token) { + return new Promise((resolve, reject) => { + const mod = url.startsWith("https") ? https : http; + const req = mod.get(url, { headers: { Authorization: `Bearer ${token}` } }, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => { + console.log(`Status: ${res.statusCode}`); + try { console.log(JSON.stringify(JSON.parse(body), null, 2)); } + catch { console.log(body); } + resolve(); + }); + }); + req.on("error", reject); + }); +} + +(async () => { + const args = parseArgs(); + try { + console.log("Authenticating with Cognito..."); + const token = await getToken(args["client-id"], args.username, args.password); + console.log("Token obtained. Calling API...\n"); + await callApi(args["api-url"], token); + } catch (err) { + console.error("Error:", err.message); + process.exit(1); + } +})(); diff --git a/apigw-APIKey-tenantid-cdk/package.json b/apigw-APIKey-tenantid-cdk/package.json new file mode 100644 index 0000000000..c74b3300e3 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/package.json @@ -0,0 +1,24 @@ +{ + "name": "apigw-dynamodb-apikey-cdk", + "version": "0.1.0", + "bin": { + "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "@aws-sdk/client-cognito-identity-provider": "^3.1034.0", + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/apigw-APIKey-tenantid-cdk/src/bin/apigw-dynamodb-apikey-cdk.ts b/apigw-APIKey-tenantid-cdk/src/bin/apigw-dynamodb-apikey-cdk.ts new file mode 100644 index 0000000000..09da105bcd --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/src/bin/apigw-dynamodb-apikey-cdk.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { ApigwDynamodbApikeyStack } from "../lib/apigw-dynamodb-apikey-stack"; + +const app = new cdk.App(); +new ApigwDynamodbApikeyStack(app, "ApigwDynamodbApikeyCdkStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/apigw-APIKey-tenantid-cdk/src/lib/apigw-dynamodb-apikey-stack.ts b/apigw-APIKey-tenantid-cdk/src/lib/apigw-dynamodb-apikey-stack.ts new file mode 100644 index 0000000000..d3e4948758 --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/src/lib/apigw-dynamodb-apikey-stack.ts @@ -0,0 +1,114 @@ +import * as cdk from "aws-cdk-lib"; +import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; +import { Runtime } from "aws-cdk-lib/aws-lambda"; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import * as cognito from "aws-cdk-lib/aws-cognito"; +import * as path from "path"; +import { Construct } from "constructs"; + +export class ApigwDynamodbApikeyStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // DynamoDB table mapping tenantId -> apiKey + const table = new dynamodb.Table(this, "TenantApiKeyTable", { + partitionKey: { name: "tenantId", type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Cognito User Pool with custom tenantId attribute + const userPool = new cognito.UserPool(this, "TenantUserPool", { + selfSignUpEnabled: false, + signInAliases: { email: true }, + customAttributes: { + tenantId: new cognito.StringAttribute({ mutable: false }), + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const userPoolClient = userPool.addClient("TenantUserPoolClient", { + authFlows: { userPassword: true }, + }); + + // Lambda authorizer log group + const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { + retention: cdk.aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Lambda authorizer + const authorizerFn = new lambda.NodejsFunction(this, "DynamoDbApiKeyAuthorizer", { + runtime: Runtime.NODEJS_22_X, + entry: path.join(__dirname, "lambda/dynamodb-authorizer.js"), + timeout: cdk.Duration.seconds(10), + environment: { + TABLE_NAME: table.tableName, + }, + bundling: { + externalModules: ["@aws-sdk/*"], + }, + logGroup: authorizerLogGroup, + }); + + table.grantReadData(authorizerFn); + + // API Gateway + const api = new apigateway.RestApi(this, "ApiGateway", { + restApiName: "DynamoDB API Key Protected Service", + description: "API protected with DynamoDB-based API key authorization", + }); + + // Token authorizer using Authorization header (Cognito JWT) + const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { + handler: authorizerFn, + identitySource: "method.request.header.Authorization", + }); + + // Protected endpoint with mock integration + const protectedResource = api.root.addResource("protected"); + + protectedResource.addMethod( + "GET", + new apigateway.MockIntegration({ + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "application/json": '{ "message": "Access granted" }', + }, + }, + ], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { + "application/json": '{ "statusCode": 200 }', + }, + }), + { + authorizer: lambdaAuthorizer, + methodResponses: [{ statusCode: "200" }], + }, + ); + + new cdk.CfnOutput(this, "ApiUrl", { + value: api.url, + description: "URL of the API Gateway", + }); + + new cdk.CfnOutput(this, "TableName", { + value: table.tableName, + description: "DynamoDB table name for tenant-apikey mappings", + }); + + new cdk.CfnOutput(this, "UserPoolId", { + value: userPool.userPoolId, + description: "Cognito User Pool ID", + }); + + new cdk.CfnOutput(this, "UserPoolClientId", { + value: userPoolClient.userPoolClientId, + description: "Cognito User Pool Client ID", + }); + } +} diff --git a/apigw-APIKey-tenantid-cdk/src/lib/lambda/dynamodb-authorizer.js b/apigw-APIKey-tenantid-cdk/src/lib/lambda/dynamodb-authorizer.js new file mode 100644 index 0000000000..995f93f9fa --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/src/lib/lambda/dynamodb-authorizer.js @@ -0,0 +1,56 @@ +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient(); + +const TABLE_NAME = process.env.TABLE_NAME; + +exports.handler = async (event) => { + const token = event.authorizationToken; + + if (!token) { + throw new Error("Unauthorized: No token provided"); + } + + try { + // Decode the JWT payload (Cognito token is already validated by API Gateway if needed) + const payload = JSON.parse( + Buffer.from(token.replace(/^Bearer\s+/i, "").split(".")[1], "base64url").toString(), + ); + const tenantId = payload["custom:tenantId"]; + + if (!tenantId) { + throw new Error("Unauthorized: No tenant ID in claims"); + } + + const result = await client.send( + new GetItemCommand({ + TableName: TABLE_NAME, + Key: { tenantId: { S: tenantId } }, + }), + ); + + if (!result.Item || !result.Item.apiKey) { + throw new Error("Unauthorized: Tenant not found"); + } + + const apiKey = result.Item.apiKey.S; + + return { + principalId: tenantId, + policyDocument: { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Resource: event.methodArn, + Action: "execute-api:Invoke", + }, + ], + }, + context: { tenantId }, + usageIdentifierKey: apiKey, + }; + } catch (error) { + console.error("Authorization error:", error.message); + throw new Error("Unauthorized"); + } +}; diff --git a/apigw-APIKey-tenantid-cdk/tsconfig.json b/apigw-APIKey-tenantid-cdk/tsconfig.json new file mode 100644 index 0000000000..aaa7dc510f --- /dev/null +++ b/apigw-APIKey-tenantid-cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} From 431e3eec2ac9ac596c6e4605371b3894c71addde Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Mon, 1 Jun 2026 22:49:49 -0400 Subject: [PATCH 09/10] removed old directory --- apigw-dynamodb-tenantid-cdk/README.md | 124 ------------------ .../apigw-dynamodb-apikey-cdk.jpg | Bin 38960 -> 0 bytes .../bin/apigw-dynamodb-apikey-cdk.ts | 11 -- apigw-dynamodb-tenantid-cdk/cdk.json | 88 ------------- .../deploy_dynamodb.sh | 8 -- apigw-dynamodb-tenantid-cdk/get-token.js | 71 ---------- .../lib/apigw-dynamodb-apikey-stack.ts | 114 ---------------- .../lib/lambda/dynamodb-authorizer.js | 56 -------- apigw-dynamodb-tenantid-cdk/package.json | 24 ---- apigw-dynamodb-tenantid-cdk/tsconfig.json | 31 ----- 10 files changed, 527 deletions(-) delete mode 100644 apigw-dynamodb-tenantid-cdk/README.md delete mode 100644 apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg delete mode 100644 apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts delete mode 100644 apigw-dynamodb-tenantid-cdk/cdk.json delete mode 100644 apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh delete mode 100644 apigw-dynamodb-tenantid-cdk/get-token.js delete mode 100644 apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts delete mode 100644 apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js delete mode 100644 apigw-dynamodb-tenantid-cdk/package.json delete mode 100644 apigw-dynamodb-tenantid-cdk/tsconfig.json diff --git a/apigw-dynamodb-tenantid-cdk/README.md b/apigw-dynamodb-tenantid-cdk/README.md deleted file mode 100644 index 687e1be4d5..0000000000 --- a/apigw-dynamodb-tenantid-cdk/README.md +++ /dev/null @@ -1,124 +0,0 @@ -# Amazon API Gateway with Cognito, Lambda Authorizer, and DynamoDB for Tenant API Key Authentication - -API Gateway's usage plans and API keys are fundamentally disconnected from authorization tokens. -Usage plans enforce rate limits via API keys, but auth tokens (JWTs from Cognito, Auth0, etc.) carry identity and permissions — these are two separate systems with no native link. This means customers cannot simply issue an auth token that inherently comes with rate-limiting attached. At scale (millions of auth tokens across thousands of tenants), managing this disconnect manually becomes untenable. - -This pattern demonstrates how to implement a secure tenant-based API key authorization system using Amazon Cognito, Amazon API Gateway, AWS Lambda Authorizer, and Amazon DynamoDB. Cognito authenticates users and issues JWTs containing a custom `tenantId` claim. The Lambda authorizer extracts the tenant ID from the JWT, looks up the corresponding API key in DynamoDB, and returns a policy document enabling API Gateway access. - -What this pattern solves: - - Bridges the auth–throttling gap — The Lambda authorizer acts as the glue between identity (JWT tenantId) and rate-limiting (API Gateway API key). By looking up the tenant's API key in DynamoDB and returning it via [usageIdentifierKey](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html), a single auth token automatically activates the correct usage plan. Auth and throttling become one unified flow rather than two disconnected systems. - - Scales to millions of tokens per tenant — Any number of JWTs can map to the same tenant's API key. You don't need a 1:1 relationship between auth tokens and API keys. A tenant can have millions of active tokens, but they all resolve to one API key and one rate-limit policy — making management tractable at scale. - - Eliminates per-application auth logic — Backend services no longer independently validate tenants or enforce limits. The gateway handles both centrally, preventing inconsistency and reducing overhead. - - Prevents noisy neighbors transparently — Tenants only interact with their auth credentials. The API key mapping and usage plan enforcement happen internally, so rate-limiting is invisible to consumers but enforced consistently. - - Makes auth and usage a single operational concern — Onboarding a new tenant means: create identity (Cognito/Auth0), create API key with a usage plan, store the mapping in DynamoDB. One workflow governs both auth and throttling, rather than managing them as separate systems that drift apart over time. - - -Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. - -## Requirements - -* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. -* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured -* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed -* [Node.js and npm](https://nodejs.org/) installed -* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed - -## Deployment Instructions - -1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: - ``` - git clone https://github.com/aws-samples/serverless-patterns - ``` -1. Change directory to the pattern directory: - ``` - cd apigw-dynamodb-apikey-cdk - ``` -1. Install dependencies: - ``` - npm install - ``` -1. Deploy the stack: - ``` - cdk deploy - ``` - -Note the outputs from the CDK deployment process. The output will include the API Gateway URL, DynamoDB table name, Cognito User Pool ID, and User Pool Client ID. - -## How it works - -![Architecture Diagram](./apigw-dynamodb-apikey-cdk.jpg) - -1. Client authenticates with Amazon Cognito and receives a JWT (ID token) containing the custom `tenantId` claim -2. Client makes a request to the API with the JWT in the `Authorization` header -3. API Gateway forwards the token to the Lambda Authorizer -4. The Lambda Authorizer decodes the JWT, extracts the `custom:tenantId` claim, and looks up the tenant in the DynamoDB table - - If the tenant exists, the associated API key is retrieved and returned in the authorization context via `usageIdentifierKey` - - If the tenant does not exist or the token is invalid, the request is denied -5. The API Gateway allows or denies access to the protected endpoint based on the policy returned by the authorizer - -The DynamoDB table uses `tenantId` as the partition key and stores the corresponding `apiKey` for each tenant. - -## Testing - -1. Get the outputs from the deployment: - ```bash - # The outputs will be similar to - ApigwDynamodbApikeyCdkStack.ApiUrl = https://abc123def.execute-api.us-east-1.amazonaws.com/prod/ - ApigwDynamodbApikeyCdkStack.TableName = ApigwDynamodbApikeyCdkStack-TenantApiKeyTableXXXXXX-YYYYYY - ApigwDynamodbApikeyCdkStack.UserPoolId = us-east-1_XXXXXXXXX - ApigwDynamodbApikeyCdkStack.UserPoolClientId = XXXXXXXXXXXXXXXXXXXXXXXXXX - ``` - -1. Create a Cognito user with a tenantId: - ```bash - aws cognito-idp admin-create-user \ - --user-pool-id USER_POOL_ID \ - --username user@example.com \ - --user-attributes Name=email,Value=user@example.com Name=custom:tenantId,Value=sample-tenant \ - --temporary-password "TempPass1!" - ``` - -1. Set a permanent password for the user: - ```bash - aws cognito-idp admin-set-user-password \ - --user-pool-id USER_POOL_ID \ - --username user@example.com \ - --password "MySecurePass1!" \ - --permanent - ``` - -1. Insert a tenant mapping into the DynamoDB table: - ```bash - aws dynamodb put-item \ - --table-name TABLE_NAME \ - --item '{"tenantId": {"S": "sample-tenant"}, "apiKey": {"S": "my-api-key-123"}}' - ``` - -1. Get a token and call the API using the helper script: - ```bash - node get-token.js --user-pool-id USER_POOL_ID --client-id CLIENT_ID \ - --username user@example.com --password "MySecurePass1!" \ - --api-url https://REPLACE_WITH_API_URL/protected - ``` - If successful, you should receive a response like: - ```json - { "message": "Access granted" } - ``` - -1. Try with an invalid or missing token: - ```bash - curl https://REPLACE_WITH_API_URL/protected - ``` - You should receive an unauthorized error. - -## Cleanup - -1. Delete the stack: - ```bash - cdk destroy - ``` - ----- -Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -SPDX-License-Identifier: MIT-0 diff --git a/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg b/apigw-dynamodb-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg deleted file mode 100644 index e8db079e1567eb0c4d860f4ec534bd0bfd202f7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38960 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%0AdxSqK2Vpe64rbAtH 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 diff --git a/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts b/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts deleted file mode 100644 index 09da105bcd..0000000000 --- a/apigw-dynamodb-tenantid-cdk/bin/apigw-dynamodb-apikey-cdk.ts +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node -import * as cdk from "aws-cdk-lib"; -import { ApigwDynamodbApikeyStack } from "../lib/apigw-dynamodb-apikey-stack"; - -const app = new cdk.App(); -new ApigwDynamodbApikeyStack(app, "ApigwDynamodbApikeyCdkStack", { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION, - }, -}); diff --git a/apigw-dynamodb-tenantid-cdk/cdk.json b/apigw-dynamodb-tenantid-cdk/cdk.json deleted file mode 100644 index 0d3b8fe63f..0000000000 --- a/apigw-dynamodb-tenantid-cdk/cdk.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts", - "watch": { - "include": [ - "**" - ], - "exclude": [ - "README.md", - "cdk*.json", - "**/*.d.ts", - "**/*.js", - "tsconfig.json", - "package*.json", - "yarn.lock", - "node_modules", - "test" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": [ - "aws", - "aws-cn" - ], - "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, - "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, - "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, - "@aws-cdk/aws-iam:minimizePolicies": true, - "@aws-cdk/core:validateSnapshotRemovalPolicy": true, - "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, - "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, - "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, - "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, - "@aws-cdk/core:enablePartitionLiterals": true, - "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, - "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, - "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, - "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, - "@aws-cdk/aws-route53-patters:useCertificate": true, - "@aws-cdk/customresources:installLatestAwsSdkDefault": false, - "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, - "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, - "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, - "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, - "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, - "@aws-cdk/aws-redshift:columnId": true, - "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, - "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, - "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, - "@aws-cdk/aws-kms:aliasNameRef": true, - "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, - "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, - "@aws-cdk/aws-efs:denyAnonymousAccess": true, - "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, - "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, - "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, - "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, - "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, - "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, - "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, - "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, - "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, - "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, - "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, - "@aws-cdk/aws-eks:nodegroupNameAttribute": true, - "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, - "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, - "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, - "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, - "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, - "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, - "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, - "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, - "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, - "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, - "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, - "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, - "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, - "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, - "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, - "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, - "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, - "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, - "@aws-cdk/core:enableAdditionalMetadataCollection": true, - "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true - } -} diff --git a/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh b/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh deleted file mode 100644 index 7758737dc7..0000000000 --- a/apigw-dynamodb-tenantid-cdk/deploy_dynamodb.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -euo pipefail - -APP="npx ts-node --prefer-ts-exts bin/apigw-dynamodb-apikey-cdk.ts" -STACK_NAME="ApigwDynamodbApikeyCdkStack" - -npm install -cdk deploy "$STACK_NAME" --app "$APP" "$@" diff --git a/apigw-dynamodb-tenantid-cdk/get-token.js b/apigw-dynamodb-tenantid-cdk/get-token.js deleted file mode 100644 index 9a5f2b039a..0000000000 --- a/apigw-dynamodb-tenantid-cdk/get-token.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -// Usage: -// node get-token.js --user-pool-id --client-id --username --password --api-url -// -// Authenticates with Cognito, retrieves an ID token, and calls the API Gateway endpoint. - -const { - CognitoIdentityProviderClient, - InitiateAuthCommand, -} = require("@aws-sdk/client-cognito-identity-provider"); -const https = require("https"); -const http = require("http"); - -function parseArgs() { - const args = process.argv.slice(2); - const parsed = {}; - for (let i = 0; i < args.length; i += 2) { - parsed[args[i].replace(/^--/, "")] = args[i + 1]; - } - const required = ["user-pool-id", "client-id", "username", "password", "api-url"]; - for (const key of required) { - if (!parsed[key]) { - console.error(`Missing required argument: --${key}`); - process.exit(1); - } - } - return parsed; -} - -async function getToken(clientId, username, password) { - const client = new CognitoIdentityProviderClient(); - const resp = await client.send( - new InitiateAuthCommand({ - AuthFlow: "USER_PASSWORD_AUTH", - ClientId: clientId, - AuthParameters: { USERNAME: username, PASSWORD: password }, - }) - ); - return resp.AuthenticationResult.IdToken; -} - -function callApi(url, token) { - return new Promise((resolve, reject) => { - const mod = url.startsWith("https") ? https : http; - const req = mod.get(url, { headers: { Authorization: `Bearer ${token}` } }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => { - console.log(`Status: ${res.statusCode}`); - try { console.log(JSON.stringify(JSON.parse(body), null, 2)); } - catch { console.log(body); } - resolve(); - }); - }); - req.on("error", reject); - }); -} - -(async () => { - const args = parseArgs(); - try { - console.log("Authenticating with Cognito..."); - const token = await getToken(args["client-id"], args.username, args.password); - console.log("Token obtained. Calling API...\n"); - await callApi(args["api-url"], token); - } catch (err) { - console.error("Error:", err.message); - process.exit(1); - } -})(); diff --git a/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts b/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts deleted file mode 100644 index d3e4948758..0000000000 --- a/apigw-dynamodb-tenantid-cdk/lib/apigw-dynamodb-apikey-stack.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as apigateway from "aws-cdk-lib/aws-apigateway"; -import * as lambda from "aws-cdk-lib/aws-lambda-nodejs"; -import { Runtime } from "aws-cdk-lib/aws-lambda"; -import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; -import * as cognito from "aws-cdk-lib/aws-cognito"; -import * as path from "path"; -import { Construct } from "constructs"; - -export class ApigwDynamodbApikeyStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - - // DynamoDB table mapping tenantId -> apiKey - const table = new dynamodb.Table(this, "TenantApiKeyTable", { - partitionKey: { name: "tenantId", type: dynamodb.AttributeType.STRING }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - // Cognito User Pool with custom tenantId attribute - const userPool = new cognito.UserPool(this, "TenantUserPool", { - selfSignUpEnabled: false, - signInAliases: { email: true }, - customAttributes: { - tenantId: new cognito.StringAttribute({ mutable: false }), - }, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const userPoolClient = userPool.addClient("TenantUserPoolClient", { - authFlows: { userPassword: true }, - }); - - // Lambda authorizer log group - const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { - retention: cdk.aws_logs.RetentionDays.ONE_WEEK, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - // Lambda authorizer - const authorizerFn = new lambda.NodejsFunction(this, "DynamoDbApiKeyAuthorizer", { - runtime: Runtime.NODEJS_22_X, - entry: path.join(__dirname, "lambda/dynamodb-authorizer.js"), - timeout: cdk.Duration.seconds(10), - environment: { - TABLE_NAME: table.tableName, - }, - bundling: { - externalModules: ["@aws-sdk/*"], - }, - logGroup: authorizerLogGroup, - }); - - table.grantReadData(authorizerFn); - - // API Gateway - const api = new apigateway.RestApi(this, "ApiGateway", { - restApiName: "DynamoDB API Key Protected Service", - description: "API protected with DynamoDB-based API key authorization", - }); - - // Token authorizer using Authorization header (Cognito JWT) - const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { - handler: authorizerFn, - identitySource: "method.request.header.Authorization", - }); - - // Protected endpoint with mock integration - const protectedResource = api.root.addResource("protected"); - - protectedResource.addMethod( - "GET", - new apigateway.MockIntegration({ - integrationResponses: [ - { - statusCode: "200", - responseTemplates: { - "application/json": '{ "message": "Access granted" }', - }, - }, - ], - passthroughBehavior: apigateway.PassthroughBehavior.NEVER, - requestTemplates: { - "application/json": '{ "statusCode": 200 }', - }, - }), - { - authorizer: lambdaAuthorizer, - methodResponses: [{ statusCode: "200" }], - }, - ); - - new cdk.CfnOutput(this, "ApiUrl", { - value: api.url, - description: "URL of the API Gateway", - }); - - new cdk.CfnOutput(this, "TableName", { - value: table.tableName, - description: "DynamoDB table name for tenant-apikey mappings", - }); - - new cdk.CfnOutput(this, "UserPoolId", { - value: userPool.userPoolId, - description: "Cognito User Pool ID", - }); - - new cdk.CfnOutput(this, "UserPoolClientId", { - value: userPoolClient.userPoolClientId, - description: "Cognito User Pool Client ID", - }); - } -} diff --git a/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js b/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js deleted file mode 100644 index 995f93f9fa..0000000000 --- a/apigw-dynamodb-tenantid-cdk/lib/lambda/dynamodb-authorizer.js +++ /dev/null @@ -1,56 +0,0 @@ -import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; -const client = new DynamoDBClient(); - -const TABLE_NAME = process.env.TABLE_NAME; - -exports.handler = async (event) => { - const token = event.authorizationToken; - - if (!token) { - throw new Error("Unauthorized: No token provided"); - } - - try { - // Decode the JWT payload (Cognito token is already validated by API Gateway if needed) - const payload = JSON.parse( - Buffer.from(token.replace(/^Bearer\s+/i, "").split(".")[1], "base64url").toString(), - ); - const tenantId = payload["custom:tenantId"]; - - if (!tenantId) { - throw new Error("Unauthorized: No tenant ID in claims"); - } - - const result = await client.send( - new GetItemCommand({ - TableName: TABLE_NAME, - Key: { tenantId: { S: tenantId } }, - }), - ); - - if (!result.Item || !result.Item.apiKey) { - throw new Error("Unauthorized: Tenant not found"); - } - - const apiKey = result.Item.apiKey.S; - - return { - principalId: tenantId, - policyDocument: { - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Resource: event.methodArn, - Action: "execute-api:Invoke", - }, - ], - }, - context: { tenantId }, - usageIdentifierKey: apiKey, - }; - } catch (error) { - console.error("Authorization error:", error.message); - throw new Error("Unauthorized"); - } -}; diff --git a/apigw-dynamodb-tenantid-cdk/package.json b/apigw-dynamodb-tenantid-cdk/package.json deleted file mode 100644 index c74b3300e3..0000000000 --- a/apigw-dynamodb-tenantid-cdk/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "apigw-dynamodb-apikey-cdk", - "version": "0.1.0", - "bin": { - "apigw-dynamodb-apikey-cdk": "bin/apigw-dynamodb-apikey-cdk.js" - }, - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "cdk": "cdk" - }, - "devDependencies": { - "@types/node": "22.7.9", - "aws-cdk": "2.1003.0", - "esbuild": "^0.25.1", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - }, - "dependencies": { - "@aws-sdk/client-cognito-identity-provider": "^3.1034.0", - "aws-cdk-lib": "2.189.1", - "constructs": "^10.0.0" - } -} diff --git a/apigw-dynamodb-tenantid-cdk/tsconfig.json b/apigw-dynamodb-tenantid-cdk/tsconfig.json deleted file mode 100644 index aaa7dc510f..0000000000 --- a/apigw-dynamodb-tenantid-cdk/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": [ - "es2020", - "dom" - ], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "typeRoots": [ - "./node_modules/@types" - ] - }, - "exclude": [ - "node_modules", - "cdk.out" - ] -} From c17ba2c585470adaa51ec18472dd927f59713686 Mon Sep 17 00:00:00 2001 From: Lavanya Tangutur Date: Mon, 1 Jun 2026 23:29:03 -0400 Subject: [PATCH 10/10] added bio --- .../example-pattern-tenantbasedAPIkeyauthorization.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json b/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json index a6073ca116..80c3dcf964 100644 --- a/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json +++ b/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json @@ -63,5 +63,12 @@ "text": [ "Delete the CDK stack: cdk destroy" ] - } + }, + "authors": [ + { + "name": "Lavanya Tangutur", + "bio": "Lavanya Tangutur serves as a Senior Technical Account Manager at AWS ocused on helping customers build, deploy, and run secure, resilient, and cost-effective workloads on AWS.", + "linkedin": "www.linkedin.com/in/lavanyatangutur" + } + ] }