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
167 changes: 167 additions & 0 deletions apigw-apikey-tenantid-cdk/README.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor naming items: the feature is "Lambda authorizer" (lowercase "authorizer") per AWS docs; first references should be "AWS Lambda", "Amazon API Gateway", "Amazon Cognito", "Amazon DynamoDB" (the README mostly does this well). The pattern folder is apigw-APIKey-tenantid-cdk with mixed-case APIKey repo convention is all-lowercase hyphenated slugs (e.g., apigw-apikey-tenantid-cdk).
Heads up that the example-pattern.json repoURL/projectFolder already use the mixed-case folder, so renaming the folder requires updating those too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Amazon API Gateway with Lambda authorizer, Amazon Cognito and Amazon 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 AWS Lambda authorizer, Amazon API Gateway, Amazon Cognito, 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.

## Prerequisites: Usage Plan and API Key

Before using this pattern, you must create API Gateway [Usage Plans](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html) and an [API Key](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-setup-api-key-with-console.html) associated with it. The usage plan must be associated with the API and stage created by this CDK stack.
The usage plan must be associated with the API and stage created by this CDK stack. The API key value stored in the DynamoDB table must match a valid API key linked to a usage plan in API Gateway — otherwise, requests will be rejected even if the Lambda authorizer returns a successful policy.

For guidance on creating and configuring usage plans and API keys, see:
- [Create and use usage plans with API keys](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html)
- [Setting up API keys using the API Gateway console](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-setup-api-key-with-console.html)

## 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
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cdk bootstrap step, important for first time users

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added cdk bootstrap

1. Bootstrap the CDK environment (if you haven't already):
```
cdk bootstrap
```
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section says the API key is "returned in the authorization context via usageIdentifierKey," but the deployed stack never creates a usage plan or API key, and the test steps put a "apiKey": "my-api-key-123" item into DynamoDB that is shorter than the 20-character minimum for a real API key and is never associated with any plan. So even after following the README, throttling is not clearly demonstrated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added instructions for usage plan and APIkey in the testing section


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

> **Note:** This sample uses `USER_PASSWORD_AUTH` for simplicity. In production, use `USER_SRP_AUTH` (Secure Remote Password) so that the password is never transmitted over the network. The plain password flow here is for demonstration purposes only.

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 usage plan:
```bash
aws apigateway create-usage-plan \
--name "TenantUsagePlan" \
--throttle burstLimit=50,rateLimit=100 \
--api-stages apiId=API_ID,stage=prod
```
Note the `id` from the output — this is your USAGE_PLAN_ID.

1. Create an API key:
```bash
aws apigateway create-api-key \
--name "SampleTenantKey" \
--enabled \
--value "tenant-usage-api-key-123"
```
Note the `id` from the output — this is your API_KEY_ID.

1. Associate the API key with the usage plan:
```bash
aws apigateway create-usage-plan-key \
--usage-plan-id USAGE_PLAN_ID \
--key-id API_KEY_ID \
--key-type "API_KEY"
```

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": "tenant-usage-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
}
}
74 changes: 74 additions & 0 deletions apigw-apikey-tenantid-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo's schema-validation workflow expects the metadata file to be named exactly example-pattern.json at the pattern root.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed the file

"title": "Amazon API Gateway with Lambda authorizer, Amazon Cognito, & Amazon 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": "Lambda authorizers for Amazon API Gateway",
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html"
},
{
"text": "Amazon API Gateway - REST APIs",
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html"
},
{
"text": "Amazon Cognito Developer Guide",
"link": "https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html"
},
{
"text": "Amazon DynamoDB Developer Guide",
"link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.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 Amazon Web Services (AWS) focused on helping customers build, deploy, and run secure, resilient, and cost-effective workloads on AWS.",
"linkedin": "lavanyatangutur"
}
]
}
Loading