diff --git a/apigw-APIKey-tenantid-cdk/README.md b/apigw-APIKey-tenantid-cdk/README.md
new file mode 100644
index 000000000..119d82bc7
--- /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
+
+
+
+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 000000000..e8db079e1
Binary files /dev/null and b/apigw-APIKey-tenantid-cdk/apigw-dynamodb-apikey-cdk.jpg differ
diff --git a/apigw-APIKey-tenantid-cdk/cdk.json b/apigw-APIKey-tenantid-cdk/cdk.json
new file mode 100644
index 000000000..36520c8e3
--- /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 000000000..83554368c
--- /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 000000000..80c3dcf96
--- /dev/null
+++ b/apigw-APIKey-tenantid-cdk/example-pattern-tenantbasedAPIkeyauthorization.json
@@ -0,0 +1,74 @@
+{
+ "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"
+ ]
+ },
+ "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"
+ }
+ ]
+}
diff --git a/apigw-APIKey-tenantid-cdk/get-token.js b/apigw-APIKey-tenantid-cdk/get-token.js
new file mode 100644
index 000000000..9a5f2b039
--- /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 000000000..c74b3300e
--- /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 000000000..09da105bc
--- /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 000000000..d3e494875
--- /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 000000000..995f93f9f
--- /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 000000000..aaa7dc510
--- /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"
+ ]
+}