Skip to content

New serverless pattern APIGW-APIKey-tenantid-mapping#3124

Open
Lavanya0513 wants to merge 18 commits into
aws-samples:mainfrom
Lavanya0513:main
Open

New serverless pattern APIGW-APIKey-tenantid-mapping#3124
Lavanya0513 wants to merge 18 commits into
aws-samples:mainfrom
Lavanya0513:main

Conversation

@Lavanya0513

Copy link
Copy Markdown

Issue #, if available:

Description of changes:
API Gateway usage plan tenant-id mapping

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Comment on lines +57 to +90
// 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" }],

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 whole point of the pattern as per the README and title is mapping a tenant to an API Gateway usage plan via an API key so throttling is enforced. But the stack creates no apigateway.UsagePlan, no apigateway.ApiKey, no usage-plan to key association, and never sets the REST API's apiKeySource to AUTHORIZER. Returning usageIdentifierKey: apiKey from the authorizer has no effect unless
(a) apiKeySource = AUTHORIZER on the API, and
(b) the returned key value matches an API key that is attached to a usage plan applied to the stage/method.
Without those, the authorizer's API-key lookup is dead code and the pattern does not actually throttle anyone. The security/operational guarantee it advertises is absent.

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 steps to create and use usage plans while testing. The pattern is not a full solution and I have specified some pre requisites in readme

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 apiKeySource = AUTHORIZER


if (!tenantId) {
throw new Error("Unauthorized: No tenant ID in claims");
}

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 authorizer extracts the tenantId by base64url-decoding the middle segment of the JWT and reading custom:tenantId it never validates the token's signature, issuer (iss), audience (aud/client_id), expiry (exp), or token_use. The inline comment ("Cognito token is already validated by API Gateway if needed") is incorrect: a Lambda TOKEN authorizer receives the raw header value and is itself responsible for authenticating the caller (per the Lambda authorizer workflow, step 3 which says "The Lambda function authenticates the caller"). API Gateway does not pre-validate a JWT for a custom authorizer. As written, an attacker can craft an unsigned JWT (header.{"custom:tenantId":"victim-tenant"}.) with any tenant ID, if that tenant exists in the table, the request is authorized and billed/throttled against the victim tenant. This is a full authorization bypass and undermines the pattern's stated security purpose.

Recommended fix: Verify the token against the Cognito User Pool JWKS before trusting any claim. Use aws-jwt-verify (the AWS-published library) and create the verifier outside the handler so the JWKS is cached across invocations.

This would require passing USER_POOL_ID and CLIENT_ID into the function's environment in the stack, and adding aws-jwt-verify as a bundled dependency (do not add it to externalModules)

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 this check

Comment on lines +31 to +33
const userPoolClient = userPool.addClient("TenantUserPoolClient", {
authFlows: { userPassword: true },
});

@parikhudit parikhudit Jun 7, 2026

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.

authFlows: { userPassword: true } enables USER_PASSWORD_AUTH, which sends the raw password to Cognito. AWS recommends USER_SRP_AUTH (Secure Remote Password) so the password is never transmitted. For a demo USER_PASSWORD_AUTH keeps the get-token.js helper simple, but the README/pattern should at least call out that SRP is preferred for production, and ideally default to it, or you may want to use SRP if possible.
Recommended Fix: Prefer authFlows: { userSrp: true } and use an SRP-capable client in the helper, or document the trade-off explicitly in the README,

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 instructions in README

Comment on lines +22 to +29
const userPool = new cognito.UserPool(this, "TenantUserPool", {
selfSignUpEnabled: false,
signInAliases: { email: true },
customAttributes: {
tenantId: new cognito.StringAttribute({ mutable: false }),
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

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 user pool sets no explicit passwordPolicy, no MFA, and no advanced security mode. Defaults are reasonable for a demo, but as this pattern is for "secure tenant-based", it's recommended to either set a password policy (preferred) / MFA or note these as production hardening steps in the README.
Recommended Fix: Add a passwordPolicy and consider mfa: cognito.Mfa.OPTIONAL

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.

set Password policy

@@ -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

{
"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"

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.

Only LinkedIn handle should be added

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.

removed bio

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.

Bio is good to have. I meant just linkedId handle is needed, not full URL

"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.",

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.

Please add an image URL if you'd like that to appear with name & bio on serverlessland.com

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.

image is not required

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


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

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

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 script name implies it deploys DynamoDB, but it actually runs cdk deploy for the whole stack with a hardcoded --app override duplicating cdk.json. The README never references it, so it's an undocumented second deploy path that can drift from cdk.json. It also runs npm install redundantly.

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.

This script is not required. so removed it

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.

package.json > bin points at bin/apigw-dynamodb-apikey-cdk.js (no such file, sources live under src/bin and src/lib). cdk.json app uses npx ts-node ... src/bin/apigw-dynamodb-apikey-cdk.ts (correct). The example-pattern.json templateFile says src/lib/apigw-dynamodb-apikey-stack.ts. The deploy_dynamodb.sh re-specifies the app on the CLI. This scattered/contradictory wiring is confusing and the package.json bin entry is simply wrong.

Recommended Fix: Standardize on the src/-based layout, fix the package.json bin path (or remove it), and ensure cdk.json is the single source of truth for the app command. Remove deploy_dynamodb.sh so there's one documented deploy path.

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.

standardized the file paths

Comment on lines +1 to +6
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient();

const TABLE_NAME = process.env.TABLE_NAME;

exports.handler = async (event) => {

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 authorizer uses import { DynamoDBClient } ... ECMAScript Modules at the top but exports with exports.handler = ... (CommonJS). Mixing ESM import with CommonJS exports in the same .js file is invalid and may fail at runtime/bundling depending on how esbuild resolves it. NodejsFunction bundles with esbuild, but the handler contract must be consistent i.e. either full ESM (export const handler) or full CommonJS (const { DynamoDBClient } = require(...) + exports.handler).

Current Code/Configuration:

import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
// ...
exports.handler = async (event) => { ... };

Recommended Fix: Pick one module system. For ESM: export const handler = async (event) => { ... }. For CJS: const { DynamoDBClient, GetItemCommand } = require("@aws-sdk/client-dynamodb");.

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.

implemented

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants