From 123abc0ca9f82d359ca927ff56f8d845089dadda Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Thu, 22 Jun 2023 06:09:19 -0400 Subject: [PATCH 1/6] fix: cross-environment issue --- src/NextjsLambda.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/NextjsLambda.ts b/src/NextjsLambda.ts index 8494f13e..72ac1e1a 100644 --- a/src/NextjsLambda.ts +++ b/src/NextjsLambda.ts @@ -1,6 +1,6 @@ import * as os from 'os'; import * as path from 'path'; -import { Duration, RemovalPolicy, Token } from 'aws-cdk-lib'; +import { Duration, PhysicalName, RemovalPolicy, Stack, Token } from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda'; import { Bucket } from 'aws-cdk-lib/aws-s3'; @@ -82,7 +82,10 @@ export class NextJsLambda extends Construct { handler: path.join('index.handler'), code, environment, - + // prevents "Resolution error: Cannot use resource in a cross-environment + // fashion, the resource's physical name must be explicit set or use + // PhysicalName.GENERATE_IF_NEEDED." + functionName: Stack.of(this).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined, ...functionOptions, }); this.lambdaFunction = fn; From 14abf8b229706158461c49a644f14cfd5feaf07b Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Thu, 22 Jun 2023 08:11:00 -0400 Subject: [PATCH 2/6] fix: Credential should be scoped to a valid region --- .projenrc.js | 2 +- README.md | 3 +++ .../LambdaOriginRequestIamAuth.test.ts | 22 +++++++++++++--- .../lambda@edge/LambdaOriginRequestIamAuth.ts | 26 ++++++++++++++----- package.json | 2 +- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/.projenrc.js b/.projenrc.js index 0470d681..431721a8 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -42,7 +42,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ devDeps: ['open-next', 'aws-sdk', 'constructs'] /* Build dependencies for this module. */, jestOptions: { jestConfig: { - testMatch: ['/src/**/__tests__/**/*.ts?(x)', '/(test|src)/**/*(*.)@(spec|test|assets).ts?(x)'], + testMatch: ['/src/**/__tests__/**/*.ts?(x)', '/(test|src|assets)/**/*(*.)@(spec|test).ts?(x)'], }, }, diff --git a/README.md b/README.md index ad41b1f7..b8d4b373 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,9 @@ Here is a short HowTo before you get started: 2. Link the bug in your pull request 3. Run `yarn build` after you made your changes and before you open a pull request +### Projen +Don't manually update package.json or use npm CLI. Update dependencies in .projenrc.js then run npm projen. + ## Breaking changes - v3.0.0: Using open-next for building, ARM64 architecture for image handling, new build options. diff --git a/assets/lambda@edge/LambdaOriginRequestIamAuth.test.ts b/assets/lambda@edge/LambdaOriginRequestIamAuth.test.ts index 86b9b580..1efd3b7f 100644 --- a/assets/lambda@edge/LambdaOriginRequestIamAuth.test.ts +++ b/assets/lambda@edge/LambdaOriginRequestIamAuth.test.ts @@ -1,20 +1,34 @@ import type { CloudFrontRequestEvent } from 'aws-lambda'; -import { signRequest } from './LambdaOriginRequestIamAuth'; +import { getRegionFromLambdaUrl, isLambdaUrlRequest, signRequest } from './LambdaOriginRequestIamAuth'; describe('LambdaOriginRequestIamAuth', () => { test('signRequest should add x-amz headers', async () => { // dummy AWS credentials process.env = { ...process.env, ...getFakeAwsCreds() }; - const event = getFakeCloudFrontRequest(); + const event = getFakeCloudFrontLambdaUrlRequest(); const request = event.Records[0].cf.request; await signRequest(request); const securityHeaders = ['x-amz-date', 'x-amz-security-token', 'x-amz-content-sha256', 'authorization']; const hasSignedHeaders = securityHeaders.every((h) => h in request.headers); expect(hasSignedHeaders).toBe(true); }); + + test('isLambdaFunctionUrl should correctly identity Lambda URL', () => { + const event = getFakeCloudFrontLambdaUrlRequest(); + const request = event.Records[0].cf.request; + const actual = isLambdaUrlRequest(request); + expect(actual).toBe(true); + }); + + test('getRegionFromLambdaUrl should correctly get region', () => { + const event = getFakeCloudFrontLambdaUrlRequest(); + const request = event.Records[0].cf.request; + const actual = getRegionFromLambdaUrl(request.origin?.custom?.domainName || ''); + expect(actual).toBe('us-east-1'); + }); }); -function getFakeCloudFrontRequest(): CloudFrontRequestEvent { +function getFakeCloudFrontLambdaUrlRequest(): CloudFrontRequestEvent { return { Records: [ { @@ -43,7 +57,7 @@ function getFakeCloudFrontRequest(): CloudFrontRequestEvent { referer: [ { key: 'Referer', - value: 'https://d6b8brjqfujeb.cloudfront.net/batches/2/overview', + value: 'https://d6b8brjqfujeb.cloudfront.net/some/path', }, ], 'x-forwarded-for': [ diff --git a/assets/lambda@edge/LambdaOriginRequestIamAuth.ts b/assets/lambda@edge/LambdaOriginRequestIamAuth.ts index a5fe1fe4..6bda59fe 100644 --- a/assets/lambda@edge/LambdaOriginRequestIamAuth.ts +++ b/assets/lambda@edge/LambdaOriginRequestIamAuth.ts @@ -4,6 +4,8 @@ import { SignatureV4 } from '@aws-sdk/signature-v4'; import type { CloudFrontHeaders, CloudFrontRequest, CloudFrontRequestHandler } from 'aws-lambda'; import { fixHostHeader, handleS3Request } from './common'; +const debug = false; + /** * This Lambda@Edge handler fixes s3 requests, fixes the host header, and * signs requests as they're destined for Lambda Function URL that requires @@ -11,17 +13,23 @@ import { fixHostHeader, handleS3Request } from './common'; */ export const handler: CloudFrontRequestHandler = async (event) => { const request = event.Records[0].cf.request; - // console.log('request', JSON.stringify(request, null, 2)); + if (debug) console.log('request', JSON.stringify(request, null, 2)); handleS3Request(request); fixHostHeader(request); - await signRequest(request); - // console.log(JSON.stringify(request), null, 2); + if (isLambdaUrlRequest(request)) { + await signRequest(request); + } + if (debug) console.log(JSON.stringify(request), null, 2); return request; }; let sigv4: SignatureV4; +export function isLambdaUrlRequest(request: CloudFrontRequest) { + return /[a-z0-9]+\.lambda-url\.[a-z0-9-]+\.on\.aws/.test(request.origin?.custom?.domainName || ''); +} + /** * When `NextjsDistributionProps.functionUrlAuthType` is set to * `lambda.FunctionUrlAuthType.AWS_IAM` we need to sign the `CloudFrontRequest`s @@ -33,7 +41,8 @@ let sigv4: SignatureV4; */ export async function signRequest(request: CloudFrontRequest) { if (!sigv4) { - sigv4 = getSigV4(); + const region = getRegionFromLambdaUrl(request.origin?.custom?.domainName || ''); + sigv4 = getSigV4(region); } const headerBag = cfHeadersToHeaderBag(request); let body: string | undefined; @@ -53,8 +62,7 @@ export async function signRequest(request: CloudFrontRequest) { request.headers = headerBagToCfHeaders(signed.headers); } -function getSigV4(): SignatureV4 { - const region = process.env.AWS_REGION; +function getSigV4(region: string): SignatureV4 { const accessKeyId = process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const sessionToken = process.env.AWS_SESSION_TOKEN; @@ -74,6 +82,12 @@ function getSigV4(): SignatureV4 { }); } +export function getRegionFromLambdaUrl(url: string): string { + const region = url.split('.').at(2); + if (!region) throw new Error("Region couldn't be extracted from Lambda Function URL"); + return region; +} + type HeaderBag = Record; /** * Converts CloudFront headers (can have array of header values) to simple diff --git a/package.json b/package.json index 6cca1c32..56fa7794 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "jest": { "testMatch": [ "/src/**/__tests__/**/*.ts?(x)", - "/(test|src)/**/*(*.)@(spec|test).ts?(x)" + "/(test|src|assets)/**/*(*.)@(spec|test).ts?(x)" ], "clearMocks": true, "collectCoverage": true, From 67de76a9781ca72f3c5367c78f564baee9dfa5d1 Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Thu, 22 Jun 2023 08:53:22 -0400 Subject: [PATCH 3/6] fix: jest config --- .projenrc.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.projenrc.js b/.projenrc.js index 431721a8..f6783792 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -1,4 +1,4 @@ -const { awscdk } = require('projen'); +const { awscdk, JsonPatch } = require('projen'); const project = new awscdk.AwsCdkConstructLibrary({ author: 'JetBridge', authorAddress: 'mischa@jetbridge.com', @@ -40,15 +40,19 @@ const project = new awscdk.AwsCdkConstructLibrary({ '@aws-crypto/sha256-js', ] /* Runtime dependencies of this module. */, devDeps: ['open-next', 'aws-sdk', 'constructs'] /* Build dependencies for this module. */, - jestOptions: { - jestConfig: { - testMatch: ['/src/**/__tests__/**/*.ts?(x)', '/(test|src|assets)/**/*(*.)@(spec|test).ts?(x)'], - }, - }, // do not generate sample test files sampleCode: false, }); +const packageJson = project.tryFindObjectFile('package.json'); +if (packageJson) { + packageJson.patch( + JsonPatch.replace('/jest/testMatch', [ + '/src/**/__tests__/**/*.ts?(x)', + '/(test|src|assets)/**/*(*.)@(spec|test).ts?(x)', + ]) + ); +} // project.eslint.addOverride({ // rules: {}, // }); From b25db8cbd17a963dbc32c228f8bf7353a4e48d5e Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Thu, 22 Jun 2023 09:14:05 -0400 Subject: [PATCH 4/6] fix: cross-env issue for image opt fn --- src/ImageOptimizationLambda.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts index 2f4553e0..42a64508 100644 --- a/src/ImageOptimizationLambda.ts +++ b/src/ImageOptimizationLambda.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -import { Duration } from 'aws-cdk-lib'; +import { Duration, PhysicalName, Stack } from 'aws-cdk-lib'; import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Architecture, Code, Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda'; import { IBucket } from 'aws-cdk-lib/aws-s3'; @@ -52,6 +52,10 @@ export class ImageOptimizationLambda extends Function { handler: 'index.handler', runtime: LAMBDA_RUNTIME, architecture: Architecture.ARM_64, + // prevents "Resolution error: Cannot use resource in a cross-environment + // fashion, the resource's physical name must be explicit set or use + // PhysicalName.GENERATE_IF_NEEDED." + functionName: Stack.of(scope).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined, ...lambdaOptions, // defaults memorySize: lambdaOptions?.memorySize || 1024, From ed652eb3532b2dc737575a53d89cbf57977f7f39 Mon Sep 17 00:00:00 2001 From: Ben Stickley <35735118+bestickley@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:23:56 -0400 Subject: [PATCH 5/6] Update README.md Co-authored-by: Mischa Spiegelmock --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8d4b373..32b3fba1 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Here is a short HowTo before you get started: 3. Run `yarn build` after you made your changes and before you open a pull request ### Projen -Don't manually update package.json or use npm CLI. Update dependencies in .projenrc.js then run npm projen. +Don't manually update package.json or use npm CLI. Update dependencies in .projenrc.js then run yarn projen. ## Breaking changes From 03ca3b3b23f14cbe6335de5587ce9322efc7492e Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Fri, 23 Jun 2023 14:13:13 -0400 Subject: [PATCH 6/6] docs: customization section with increased security --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 32b3fba1..965c364e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,49 @@ new Nextjs(this, 'Web', { Available on [Construct Hub](https://constructs.dev/packages/cdk-nextjs-standalone/). +## Customization + +### Increased Security +```ts +import { RemovalPolicy, Stack } from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { CfnWebAcl } from "aws-cdk-lib/aws-wafv2"; +import { SecurityPolicyProtocol, type DistributionProps } from "aws-cdk-lib/aws-cloudfront"; +import { Nextjs, type NextjsDistributionProps } from "cdk-nextjs-standalone"; +import { Bucket, BlockPublicAccess, BucketEncryption } from "aws-cdk-lib/aws-s3"; + +// Because of `WebAcl`, this stack must be deployed in us-east-1. If you want +// to deploy Nextjs in another region, add WAF in separate stack deployed in us-east-1 +export class UiStack { + constructor(scope: Construct, id: string) { + const webAcl = new CfnWebAcl(this, "WebAcl", { ... }); + new Nextjs(this, "NextSite", { + nextjsPath: "...", + defaults: { + assetDeployment: { + bucket: new Bucket(this, "NextjsAssetDeploymentBucket", { + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + encryption: BucketEncryption.S3_MANAGED, + enforceSSL: true, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + }), + }, + distribution: { + functionUrlAuthType: FunctionUrlAuthType.AWS_IAM, + cdk: { + distribution: { + webAclId: webAcl.attrArn, + minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, + } as DistributionProps, + }, + } satisfies Partial, + }, + }); + } +} +``` + ### Discord Chat We're in the #open-next channel on the [Serverless Stack Discord](https://discord.gg/sst).