Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions apigw-APIKey-tenantid-cdk/README.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions apigw-APIKey-tenantid-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions apigw-APIKey-tenantid-cdk/deploy_dynamodb.sh
Original file line number Diff line number Diff line change
@@ -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" "$@"
Original file line number Diff line number Diff line change
@@ -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: <code>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!\"</code>",
"Set a permanent password: <code>aws cognito-idp admin-set-user-password --user-pool-id USER_POOL_ID --username user@example.com --password \"MySecurePass1!\" --permanent</code>",
"Insert a tenant mapping into the DynamoDB table: <code>aws dynamodb put-item --table-name TABLE_NAME --item '{\"tenantId\": {\"S\": \"sample-tenant\"}, \"apiKey\": {\"S\": \"my-api-key-123\"}}'</code>",
"Get a token and call the API: <code>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</code>",
"If successful, you should receive a response: <code>{ \"message\": \"Access granted\" }</code>"
]
},
"cleanup": {
"text": [
"Delete the CDK stack: <code>cdk destroy</code>"
]
},
"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"
}
]
}
71 changes: 71 additions & 0 deletions apigw-APIKey-tenantid-cdk/get-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node

// Usage:
// node get-token.js --user-pool-id <id> --client-id <id> --username <email> --password <pass> --api-url <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);
}
})();
24 changes: 24 additions & 0 deletions apigw-APIKey-tenantid-cdk/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading