Skip to content

Commit 9f52a6b

Browse files
author
Marco Jahn
committed
added apigw-secretsmanager-apikey-cdk
1 parent 0ca75de commit 9f52a6b

File tree

11 files changed

+555
-0
lines changed

11 files changed

+555
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# API Gateway with Lambda Authorizer and Secrets Manager for API Key Authentication
2+
3+
This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager. Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints.
4+
5+
Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk](https://serverlessland.com/patterns/apigw-secretsmanager-apikey-cdk)
6+
7+
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.
8+
9+
## Requirements
10+
11+
* [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.
12+
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
13+
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
14+
* [Node.js and npm](https://nodejs.org/) installed
15+
* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed
16+
17+
## Deployment Instructions
18+
19+
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
20+
```
21+
git clone git clone https://github.com/aws-samples/serverless-patterns
22+
```
23+
1. Change directory to the pattern directory:
24+
```
25+
cd apigw-secretsmanager-apikey-cdk
26+
```
27+
1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file:
28+
```
29+
npm install
30+
```
31+
1. Deploy the stack:
32+
```
33+
cdk deploy
34+
```
35+
36+
Note the outputs from the CDK deployment process. The output will include the API Gateway URL you'll need for testing.
37+
38+
## How it works
39+
40+
![Architecture Diagram](./apigw-secretsmanager-apikey-cdk.png)
41+
42+
1. Client makes a request to the API with an API key in the `x-api-key` header
43+
2. API Gateway forwards the authorization request to the Lambda Authorizer
44+
- The Lambda Authorizer checks if the API key exists in Secrets Manager
45+
- If the key is valid, the associated tenant information is retrieved and included in the authorization context
46+
3. The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer
47+
48+
Each API key in Secrets Manager should follow the naming convention `api-key-{keyvalue}` and contain a JSON document with at least a tenantId field.
49+
50+
## Testing
51+
52+
1. Create a new api key, you will need the api key later on
53+
```
54+
❯ ./create_api_key.sh sample-tenant
55+
API key for tenant sample-tenant created: b4037c9368990982ac5d1c670053bf76
56+
```
57+
1. Get the API Gateway URL from the deployment output:
58+
```bash
59+
# The output will be similar to
60+
ApigwSecretsmanagerApikeyCdkStack.ApiUrl = https://383rm6ue91.execute-api.us-east-1.amazonaws.com/prod/
61+
```
62+
1. Make a request to the protected endpoint with a valid API key:
63+
```
64+
curl -H "x-api-key: CREATED_API_KEY" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected
65+
```
66+
If successful, you should receive a response like:
67+
```
68+
{ "message": "Access granted" }
69+
```
70+
1. Try with an invalid API key:
71+
```
72+
curl -H "x-api-key: invalid-key" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected
73+
```
74+
You should receive an unauthorized error.
75+
1. Try without an API key:
76+
```
77+
curl https://REPLACE_WITH_URL_FROM_CDK_OUTPUT.amazonaws.com/prod/protected
78+
```
79+
You should also receive an unauthorized error.
80+
81+
82+
## Cleanup
83+
84+
1. Delete the stack
85+
```bash
86+
cdk destroy
87+
```
88+
1. Delete created SecretManager keys using
89+
```bash
90+
./remove_secrets.sh
91+
92+
# to check which keys will be removed, use the dry-run option
93+
./remove_secrets.sh --dry-run
94+
```
95+
96+
----
97+
Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
98+
99+
SPDX-License-Identifier: MIT-0
38.5 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
import * as cdk from "aws-cdk-lib";
3+
import { ApigwSecretsmanagerApikeyStack } from "../lib/apigw-secretsmanager-apikey-stack";
4+
5+
const app = new cdk.App();
6+
// amazonq-ignore-next-line
7+
new ApigwSecretsmanagerApikeyStack(app, "ApigwSecretsmanagerApikeyCdkStack", {
8+
env: {
9+
account: process.env.CDK_DEFAULT_ACCOUNT,
10+
region: process.env.CDK_DEFAULT_REGION,
11+
},
12+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/apigw-secrectsmanager-apikey-cdk.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21+
"@aws-cdk/core:checkSecretUsage": true,
22+
"@aws-cdk/core:target-partitions": [
23+
"aws",
24+
"aws-cn"
25+
],
26+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29+
"@aws-cdk/aws-iam:minimizePolicies": true,
30+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
34+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
35+
"@aws-cdk/core:enablePartitionLiterals": true,
36+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
37+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
38+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
39+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
40+
"@aws-cdk/aws-route53-patters:useCertificate": true,
41+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
42+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
43+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
44+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
45+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
46+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
47+
"@aws-cdk/aws-redshift:columnId": true,
48+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
49+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
50+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
51+
"@aws-cdk/aws-kms:aliasNameRef": true,
52+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
53+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
54+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
55+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
56+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
57+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
58+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
59+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
60+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
61+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
62+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
63+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
64+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
65+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
66+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
67+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
68+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
69+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
70+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
71+
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
72+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
73+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
74+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
75+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
76+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
77+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
78+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
79+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
80+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
81+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
82+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
83+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
84+
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
85+
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
86+
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true
87+
}
88+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
3+
# Create/update API key for a tenant
4+
# Usage: ./create-api-key tenant-1
5+
6+
tenant_id=$1
7+
if [ -z "$tenant_id" ]; then
8+
echo "Error: Tenant ID is required"
9+
exit 1
10+
fi
11+
12+
# Generate random 32-character API key
13+
api_key=$(openssl rand -hex 16)
14+
15+
# Store the secret with the API key as the identifier
16+
aws secretsmanager create-secret \
17+
--name "api-key-${api_key}" \
18+
--secret-string "{\"tenantId\":\"${tenant_id}\"}" \
19+
--no-cli-pager
20+
21+
echo "API key for tenant ${tenant_id} created: ${api_key}"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"title": "API Gateway with Lambda Authorizer and Secrets Manager for API Key Authentication",
3+
"description": "Implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.",
4+
5+
"language": "TypeScript",
6+
"level": "200",
7+
"framework": "AWS CDK",
8+
"introBox": {
9+
"headline": "How it works",
10+
"text": [
11+
"This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.",
12+
"Each user/tenant has their own unique API key stored in Secrets Manager, which is validated by a Lambda authorizer when requests are made to protected API endpoints.",
13+
"The Lambda authorizer checks if the API key exists in Secrets Manager. If the key is valid, the associated tenant information is retrieved and included in the authorization context.",
14+
"The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer."
15+
]
16+
},
17+
"gitHub": {
18+
"template": {
19+
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-secretsmanager-apikey-cdk",
20+
"templateURL": "serverless-patterns/apigw-secretsmanager-apikey-cdk",
21+
"projectFolder": "apigw-secretsmanager-apikey-cdk",
22+
"templateFile": "lib/apigw-secretsmanager-apikey-stack.ts"
23+
}
24+
},
25+
"resources": {
26+
"bullets": [
27+
{
28+
"text": "Lambda Authorizers for Amazon API Gateway",
29+
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html"
30+
},
31+
{
32+
"text": "AWS Secrets Manager User Guide",
33+
"link": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html"
34+
},
35+
{
36+
"text": "Amazon API Gateway - REST APIs",
37+
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html"
38+
}
39+
]
40+
},
41+
"deploy": {
42+
"text": ["npm install", "cdk deploy"]
43+
},
44+
"testing": {
45+
"text": [
46+
"Create an API key using the provided script: './create_api_key.sh sample-tenant'",
47+
"Make a request to the protected endpoint using the valid API key: 'curl -H \"x-api-key: CREATED_API_KEY\" https://REPLACE_WITH_CREATED_API_URL.amazonaws.com/prod/protected'",
48+
"If successful, you should receive a response: { \"message\": \"Access granted\" }"
49+
]
50+
},
51+
"cleanup": {
52+
"text": [
53+
"Delete the CDK stack: 'cdk destroy'",
54+
"Delete created SecretManager keys using the provided script: './remove_secrets.sh'"
55+
]
56+
},
57+
"authors": [
58+
{
59+
"name": "Marco Jahn",
60+
"image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png",
61+
"bio": "Senior Solutions Architect - ISV, Amazon Web Services",
62+
"linkedin": "https://www.linkedin.com/in/marcojahn/"
63+
}
64+
]
65+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as apigateway from "aws-cdk-lib/aws-apigateway";
3+
import * as lambda from "aws-cdk-lib/aws-lambda-nodejs";
4+
import { Runtime } from "aws-cdk-lib/aws-lambda";
5+
import * as iam from "aws-cdk-lib/aws-iam";
6+
import * as path from "path";
7+
import { Construct } from "constructs";
8+
9+
export class ApigwSecretsmanagerApikeyStack extends cdk.Stack {
10+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
11+
super(scope, id, props);
12+
13+
// Create IAM role for the authorizer Lambda
14+
const authorizerRole = new iam.Role(this, "AuthorizerRole", {
15+
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
16+
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")],
17+
});
18+
19+
// Add more specific permission to access only relevant Secrets Manager secrets
20+
authorizerRole.addToPolicy(
21+
new iam.PolicyStatement({
22+
actions: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
23+
resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:api-key-*`],
24+
}),
25+
);
26+
27+
// Create a dedicated logGroup for ApiKeyAuthorizer
28+
const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", {
29+
retention: cdk.aws_logs.RetentionDays.ONE_WEEK,
30+
removalPolicy: cdk.RemovalPolicy.DESTROY,
31+
});
32+
33+
// Create Lambda authorizer
34+
const authorizer = new lambda.NodejsFunction(this, "ApiKeyAuthorizer", {
35+
runtime: Runtime.NODEJS_22_X,
36+
entry: path.join(__dirname, "lambda/authorizer.js"),
37+
role: authorizerRole,
38+
timeout: cdk.Duration.seconds(10),
39+
environment: {
40+
SECRET_PREFIX: "api-key-",
41+
},
42+
bundling: {
43+
externalModules: [
44+
"@aws-sdk/*", // AWS SDK v3 modules
45+
],
46+
},
47+
logGroup: authorizerLogGroup,
48+
});
49+
50+
// Create API Gateway
51+
const api = new apigateway.RestApi(this, "ApiGateway", {
52+
restApiName: "API Key Protected Service",
53+
description: "API protected with API key authorization",
54+
});
55+
56+
// Create Lambda authorizer
57+
const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", {
58+
handler: authorizer,
59+
identitySource: "method.request.header.x-api-key",
60+
});
61+
62+
// Sample protected endpoint
63+
const protectedResource = api.root.addResource("protected");
64+
65+
// Mock integration for demo purposes
66+
const integration = new apigateway.MockIntegration({
67+
integrationResponses: [
68+
{
69+
statusCode: "200",
70+
responseTemplates: {
71+
"application/json": '{ "message": "Access granted" }',
72+
},
73+
},
74+
],
75+
passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
76+
requestTemplates: {
77+
"application/json": '{ "statusCode": 200 }',
78+
},
79+
});
80+
81+
// Add method with authorizer
82+
protectedResource.addMethod("GET", integration, {
83+
authorizer: lambdaAuthorizer,
84+
methodResponses: [{ statusCode: "200" }],
85+
});
86+
87+
// Output the API URL
88+
new cdk.CfnOutput(this, "ApiUrl", {
89+
value: api.url,
90+
description: "URL of the API Gateway",
91+
});
92+
}
93+
}

0 commit comments

Comments
 (0)