diff --git a/apigw-secretsmanager-apikey-cdk/README.md b/apigw-secretsmanager-apikey-cdk/README.md new file mode 100644 index 000000000..421956949 --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/README.md @@ -0,0 +1,99 @@ +# Amazon API Gateway with AWS Lambda Authorizer and AWS Secrets Manager for API Key Authentication + +This pattern demonstrates how to implement a secure API key-based authorization system using Amazon API Gateway, AWS 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. + +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) + +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-secretsmanager-apikey-cdk + ``` +1. From the command line, use AWS SAM to deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + npm install + ``` +1. Deploy the stack: + ``` + cdk deploy + ``` + +Note the outputs from the CDK deployment process. The output will include the API Gateway URL you'll need for testing. + +## How it works + +![Architecture Diagram](./apigw-secretsmanager-apikey-cdk.png) + +1. Client makes a request to the API with an API key in the `x-api-key` header +2. API Gateway forwards the authorization request to the Lambda Authorizer + - 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 +3. The API Gateway then allows or denies access to the protected endpoint based on the policy returned by the authorizer + +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. + +## Testing + +1. Create a new api key, you will need the api key later on + ``` + ❯ ./create_api_key.sh sample-tenant + API key for tenant sample-tenant created: b4037c9368990982ac5d1c670053bf76 + ``` +1. Get the API Gateway URL from the deployment output: + ```bash + # The output will be similar to + ApigwSecretsmanagerApikeyCdkStack.ApiUrl = https://383rm6ue91.execute-api.us-east-1.amazonaws.com/prod/ + ``` +1. Make a request to the protected endpoint with a valid API key: + ``` + curl -H "x-api-key: CREATED_API_KEY" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected + ``` + If successful, you should receive a response like: + ``` + { "message": "Access granted" } + ``` +1. Try with an invalid API key: + ``` + curl -H "x-api-key: invalid-key" https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected + ``` + You should receive an unauthorized error. +1. Try without an API key: + ``` + curl https://REPLACE_WITH_URL_FROM_CDK_OUTPUT/protected + ``` + You should also receive an unauthorized error. + + +## Cleanup + +1. Delete the stack + ```bash + cdk destroy + ``` +1. Delete created SecretManager keys using + ```bash + ./remove_secrets.sh + + # to check which keys will be removed, use the dry-run option + ./remove_secrets.sh --dry-run + ``` + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png b/apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png new file mode 100644 index 000000000..6d167277c Binary files /dev/null and b/apigw-secretsmanager-apikey-cdk/apigw-secretsmanager-apikey-cdk.png differ diff --git a/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json b/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json new file mode 100644 index 000000000..d81e104ef --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/apigws-secretsmanager-apikey-cdk.json @@ -0,0 +1,97 @@ +{ + "title": "API Gateway, Lambda Authorizer & Secrets Manager for API Key Authentication", + "description": "Implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "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.", + "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.", + "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-secretsmanager-apikey-cdk", + "templateURL": "serverless-patterns/apigw-secretsmanager-apikey-cdk", + "projectFolder": "apigw-secretsmanager-apikey-cdk", + "templateFile": "lib/apigw-secretsmanager-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": "AWS Secrets Manager User Guide", + "link": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + } + ] + }, + "deploy": { + "text": [ + "npm install", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Create an API key using the provided script: ./create_api_key.sh sample-tenant", + "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", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy", + "Delete created SecretManager keys using the provided script: ./remove_secrets.sh" + ] + }, + "authors": [ + { + "name": "Marco Jahn", + "image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png", + "bio": "Senior Solutions Architect - ISV, Amazon Web Services", + "linkedin": "marcojahn" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "apigw", + "label": "API Gateway REST API" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "lambda", + "label": "AWS Lambda Authorizer" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "secretsmanager", + "label": "AWS Secrets Manager" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "Authorizer" + }, + "line2": { + "from": "icon2", + "to": "icon3", + "label": "Request secret" + } + } +} diff --git a/apigw-secretsmanager-apikey-cdk/bin/apigw-secrectsmanager-apikey-cdk.ts b/apigw-secretsmanager-apikey-cdk/bin/apigw-secrectsmanager-apikey-cdk.ts new file mode 100644 index 000000000..4d1d19f9d --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/bin/apigw-secrectsmanager-apikey-cdk.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; +import { ApigwSecretsmanagerApikeyStack } from "../lib/apigw-secretsmanager-apikey-stack"; + +const app = new cdk.App(); +// amazonq-ignore-next-line +new ApigwSecretsmanagerApikeyStack(app, "ApigwSecretsmanagerApikeyCdkStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/apigw-secretsmanager-apikey-cdk/cdk.json b/apigw-secretsmanager-apikey-cdk/cdk.json new file mode 100644 index 000000000..30908c28d --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/cdk.json @@ -0,0 +1,88 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/apigw-secrectsmanager-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-secretsmanager-apikey-cdk/create_api_key.sh b/apigw-secretsmanager-apikey-cdk/create_api_key.sh new file mode 100755 index 000000000..7247dcd3f --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/create_api_key.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Create/update API key for a tenant +# Usage: ./create-api-key tenant-1 + +tenant_id=$1 +if [ -z "$tenant_id" ]; then + echo "Error: Tenant ID is required" + exit 1 +fi + +# Generate random 32-character API key +api_key=$(openssl rand -hex 16) + +# Store the secret with the API key as the identifier +aws secretsmanager create-secret \ + --name "api-key-${api_key}" \ + --secret-string "{\"tenantId\":\"${tenant_id}\"}" \ + --no-cli-pager + +echo "API key for tenant ${tenant_id} created: ${api_key}" diff --git a/apigw-secretsmanager-apikey-cdk/example-pattern.json b/apigw-secretsmanager-apikey-cdk/example-pattern.json new file mode 100644 index 000000000..2a5b08f40 --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/example-pattern.json @@ -0,0 +1,65 @@ +{ + "title": "API Gateway, Lambda Authorizer & Secrets Manager for API Key Authentication", + "description": "Implement a secure API key-based authorization system using Amazon API Gateway, Lambda Authorizer, and AWS Secrets Manager.", + + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "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.", + "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.", + "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-secretsmanager-apikey-cdk", + "templateURL": "serverless-patterns/apigw-secretsmanager-apikey-cdk", + "projectFolder": "apigw-secretsmanager-apikey-cdk", + "templateFile": "lib/apigw-secretsmanager-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": "AWS Secrets Manager User Guide", + "link": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" + }, + { + "text": "Amazon API Gateway - REST APIs", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + } + ] + }, + "deploy": { + "text": ["npm install", "cdk deploy"] + }, + "testing": { + "text": [ + "Create an API key using the provided script: ./create_api_key.sh sample-tenant", + "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", + "If successful, you should receive a response: { \"message\": \"Access granted\" }" + ] + }, + "cleanup": { + "text": [ + "Delete the CDK stack: cdk destroy", + "Delete created SecretManager keys using the provided script: ./remove_secrets.sh" + ] + }, + "authors": [ + { + "name": "Marco Jahn", + "image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png", + "bio": "Senior Solutions Architect - ISV, Amazon Web Services", + "linkedin": "marcojahn" + } + ] +} diff --git a/apigw-secretsmanager-apikey-cdk/lib/apigw-secretsmanager-apikey-stack.ts b/apigw-secretsmanager-apikey-cdk/lib/apigw-secretsmanager-apikey-stack.ts new file mode 100644 index 000000000..e59b0547b --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/lib/apigw-secretsmanager-apikey-stack.ts @@ -0,0 +1,93 @@ +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 iam from "aws-cdk-lib/aws-iam"; +import * as path from "path"; +import { Construct } from "constructs"; + +export class ApigwSecretsmanagerApikeyStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create IAM role for the authorizer Lambda + const authorizerRole = new iam.Role(this, "AuthorizerRole", { + assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")], + }); + + // Add more specific permission to access only relevant Secrets Manager secrets + authorizerRole.addToPolicy( + new iam.PolicyStatement({ + actions: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], + resources: [`arn:aws:secretsmanager:${this.region}:${this.account}:secret:api-key-*`], + }), + ); + + // Create a dedicated logGroup for ApiKeyAuthorizer + const authorizerLogGroup = new cdk.aws_logs.LogGroup(this, "AuthorizerLogGroup", { + retention: cdk.aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Create Lambda authorizer + const authorizer = new lambda.NodejsFunction(this, "ApiKeyAuthorizer", { + runtime: Runtime.NODEJS_22_X, + entry: path.join(__dirname, "lambda/authorizer.js"), + role: authorizerRole, + timeout: cdk.Duration.seconds(10), + environment: { + SECRET_PREFIX: "api-key-", + }, + bundling: { + externalModules: [ + "@aws-sdk/*", // AWS SDK v3 modules + ], + }, + logGroup: authorizerLogGroup, + }); + + // Create API Gateway + const api = new apigateway.RestApi(this, "ApiGateway", { + restApiName: "API Key Protected Service", + description: "API protected with API key authorization", + }); + + // Create Lambda authorizer + const lambdaAuthorizer = new apigateway.TokenAuthorizer(this, "TokenAuthorizer", { + handler: authorizer, + identitySource: "method.request.header.x-api-key", + }); + + // Sample protected endpoint + const protectedResource = api.root.addResource("protected"); + + // Mock integration for demo purposes + const integration = new apigateway.MockIntegration({ + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "application/json": '{ "message": "Access granted" }', + }, + }, + ], + passthroughBehavior: apigateway.PassthroughBehavior.NEVER, + requestTemplates: { + "application/json": '{ "statusCode": 200 }', + }, + }); + + // Add method with authorizer + protectedResource.addMethod("GET", integration, { + authorizer: lambdaAuthorizer, + methodResponses: [{ statusCode: "200" }], + }); + + // Output the API URL + new cdk.CfnOutput(this, "ApiUrl", { + value: api.url, + description: "URL of the API Gateway", + }); + } +} diff --git a/apigw-secretsmanager-apikey-cdk/lib/lambda/authorizer.js b/apigw-secretsmanager-apikey-cdk/lib/lambda/authorizer.js new file mode 100644 index 000000000..3a40d0725 --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/lib/lambda/authorizer.js @@ -0,0 +1,87 @@ +import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +const client = new SecretsManagerClient(); + +const SECRET_PREFIX = process.env.SECRET_PREFIX || "api-key-"; + +exports.handler = async (event) => { + // Get API key from request headers + const apiKey = event.authorizationToken; + + if (!apiKey) { + throw new Error("Unauthorized: No API key provided"); + } + + try { + // Validate API key by directly accessing the specific secret + const tenantData = await validateApiKey(apiKey, event); + + if (tenantData) { + return generatePolicy(tenantData.tenantId, "Allow", event.methodArn, tenantData.tenantId); + } else { + throw new Error("Unauthorized: Invalid API key"); + } + } catch (error) { + console.error("Authorization error:", error.message); + throw new Error("Unauthorized"); + } +}; + +async function validateApiKey(apiKey, event) { + try { + // Get the secret directly using the API key's secret name + const secretId = `${SECRET_PREFIX}${apiKey}`; + + const secretData = await client.send( + new GetSecretValueCommand({ + SecretId: secretId, + }), + ); + + if (secretData.SecretString) { + const secret = JSON.parse(secretData.SecretString); + return { + tenantId: secret.tenantId, + valid: true, + }; + } + } catch (error) { + // Secret not found or other error - API key is invalid + if (error.code === "ResourceNotFoundException") { + console.log("API key not found"); + } else { + console.error("Error fetching API key secret:", error); + } + return null; + } + + return null; +} + +// Helper function to generate IAM policy +function generatePolicy(principalId, effect, resource, tenantId) { + const authResponse = { + principalId: principalId, + }; + + if (effect && resource) { + const policyDocument = { + Version: "2012-10-17", + Statement: [ + { + Effect: effect, + Resource: resource, + Action: "execute-api:Invoke", + }, + ], + }; + + authResponse.policyDocument = policyDocument; + + // Add context with tenant ID for downstream Lambda functions + authResponse.context = { + tenantId: tenantId, + }; + } + + return authResponse; +} diff --git a/apigw-secretsmanager-apikey-cdk/package.json b/apigw-secretsmanager-apikey-cdk/package.json new file mode 100644 index 000000000..97b35e2da --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/package.json @@ -0,0 +1,26 @@ +{ + "name": "apigw-secrectsmanager-apikey-cdk", + "version": "0.1.0", + "bin": { + "apigw-secrectsmanager-apikey-cdk": "bin/apigw-secrectsmanager-apikey-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "esbuild": "^0.25.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.181.1", + "constructs": "^10.0.0" + } +} diff --git a/apigw-secretsmanager-apikey-cdk/remove_secrets.sh b/apigw-secretsmanager-apikey-cdk/remove_secrets.sh new file mode 100755 index 000000000..6f3c8ea3d --- /dev/null +++ b/apigw-secretsmanager-apikey-cdk/remove_secrets.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Define the prefix +PREFIX="api-key-" + +# Add a dry run flag +DRY_RUN=false + +# Parse command line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --dry-run) DRY_RUN=true ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +# List secrets with the prefix and store them in an array +secrets=($(aws secretsmanager list-secrets --query 'SecretList[?starts_with(Name, `'"$PREFIX"'`)].Name' --output text)) + +# Loop through each secret +for secret in "${secrets[@]}"; do + if $DRY_RUN; then + echo "Would delete secret: $secret" + else + aws secretsmanager delete-secret --secret-id "$secret" --recovery-window-in-days 7 --no-cli-pager + echo "Deleted secret: $secret" + fi +done + +if $DRY_RUN; then + echo "Dry run completed. No secrets were actually deleted." +fi diff --git a/apigw-secretsmanager-apikey-cdk/tsconfig.json b/apigw-secretsmanager-apikey-cdk/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/apigw-secretsmanager-apikey-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" + ] +}