Skip to content

Commit 61d9b91

Browse files
khuezyfwang
andauthored
fix: Fix next-auth middleware not working on Lambda (#38)
* fix: inject crypto and CryptoKey to global instance to get nextauth middleware to work * add body response from middleware * Sync --------- Co-authored-by: Frank <[email protected]>
1 parent 6f8b321 commit 61d9b91

File tree

6 files changed

+271
-227
lines changed

6 files changed

+271
-227
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ Vercel uses the `Headers.getAll()` function in its middleware code, but this fun
282282

283283
We decided to go with option 1 because it does not require an addition dependency and it is possible that Vercel will remove the use of the `getAll()` function in the future.
284284

285+
#### WORKAROUND: Polyfill `crypto` for the middleware function
286+
287+
[NextAuth.js](https://next-auth.js.org) uses the [`jose`](https://github.com/panva/jose) library at runtime to encrypt and decrypt JWT tokens. The library, in turn, uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). This workaround polyfills `crypto` and `CryptoKey` into the `globalThis` instance.
288+
285289
## Example
286290

287291
In the `example` folder, you can find a Next.js feature test app. It contains a variety of pages that each test a single Next.js feature.
Lines changed: 159 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,198 +1,203 @@
1-
import path from "node:path"
2-
import https from "node:https"
3-
import { Writable } from "node:stream"
4-
import { IncomingMessage, ServerResponse } from "node:http"
5-
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
1+
import path from "node:path";
2+
import https from "node:https";
3+
import { Writable } from "node:stream";
4+
import { IncomingMessage, ServerResponse } from "node:http";
5+
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
66
import type {
7-
APIGatewayProxyEventV2,
8-
APIGatewayProxyResultV2,
9-
APIGatewayProxyEventHeaders,
10-
APIGatewayProxyEventQueryStringParameters
11-
} from "aws-lambda"
7+
APIGatewayProxyEventV2,
8+
APIGatewayProxyResultV2,
9+
APIGatewayProxyEventHeaders,
10+
APIGatewayProxyEventQueryStringParameters,
11+
} from "aws-lambda";
1212
// @ts-ignore
13-
import { defaultConfig } from "next/dist/server/config-shared"
13+
import { defaultConfig } from "next/dist/server/config-shared";
1414
// @ts-ignore
15-
import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"
16-
// @ts-ignore
17-
import { imageOptimizer, ImageOptimizerCache } from "next/dist/server/image-optimizer"
18-
import { loadConfig } from "./util.js"
15+
import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
16+
import {
17+
imageOptimizer,
18+
ImageOptimizerCache,
19+
// @ts-ignore
20+
} from "next/dist/server/image-optimizer";
21+
22+
import { loadConfig } from "./util.js";
1923

2024
const bucketName = process.env.BUCKET_NAME;
2125
const nextDir = path.join(__dirname, ".next");
2226
const config = loadConfig(nextDir);
2327
const nextConfig = {
24-
...(defaultConfig),
25-
images: {
26-
...(defaultConfig.images),
27-
...config.images,
28-
},
29-
}
28+
...defaultConfig,
29+
images: {
30+
...defaultConfig.images,
31+
...config.images,
32+
},
33+
};
3034
console.log("Init config", {
31-
nextDir,
32-
bucketName,
33-
nextConfig,
35+
nextDir,
36+
bucketName,
37+
nextConfig,
3438
});
3539

3640
/////////////
3741
// Handler //
3842
/////////////
3943

40-
export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
41-
// Images are handled via header and query param information.
42-
console.log("handler event", event)
43-
const {
44-
headers: rawHeaders,
45-
queryStringParameters: queryString,
46-
} = event;
47-
48-
try {
49-
const headers = normalizeHeaderKeysToLowercase(rawHeaders)
50-
ensureBucketExists();
51-
const imageParams = validateImageParams(headers, queryString);
52-
const result = await optimizeImage(headers, imageParams);
53-
54-
return buildSuccessResponse(result);
55-
} catch (e: any) {
56-
return buildFailureResponse(e);
57-
}
44+
export async function handler(
45+
event: APIGatewayProxyEventV2
46+
): Promise<APIGatewayProxyResultV2> {
47+
// Images are handled via header and query param information.
48+
console.log("handler event", event);
49+
const { headers: rawHeaders, queryStringParameters: queryString } = event;
50+
51+
try {
52+
const headers = normalizeHeaderKeysToLowercase(rawHeaders);
53+
ensureBucketExists();
54+
const imageParams = validateImageParams(headers, queryString);
55+
const result = await optimizeImage(headers, imageParams);
56+
57+
return buildSuccessResponse(result);
58+
} catch (e: any) {
59+
return buildFailureResponse(e);
60+
}
5861
}
5962

6063
//////////////////////
6164
// Helper functions //
6265
//////////////////////
6366

6467
function normalizeHeaderKeysToLowercase(headers: APIGatewayProxyEventHeaders) {
65-
// Make header keys lowercase to ensure integrity
66-
return Object.entries(headers).reduce((acc, [key, value]) =>
67-
({ ...acc, [key.toLowerCase()]: value }),
68-
{} as APIGatewayProxyEventHeaders
69-
)
68+
// Make header keys lowercase to ensure integrity
69+
return Object.entries(headers).reduce(
70+
(acc, [key, value]) => ({ ...acc, [key.toLowerCase()]: value }),
71+
{} as APIGatewayProxyEventHeaders
72+
);
7073
}
7174

7275
function ensureBucketExists() {
73-
if (!bucketName) {
74-
throw new Error("Bucket name must be defined!")
75-
}
76+
if (!bucketName) {
77+
throw new Error("Bucket name must be defined!");
78+
}
7679
}
7780

7881
function validateImageParams(
79-
headers: APIGatewayProxyEventHeaders,
80-
queryString?: APIGatewayProxyEventQueryStringParameters
82+
headers: APIGatewayProxyEventHeaders,
83+
queryString?: APIGatewayProxyEventQueryStringParameters
8184
) {
82-
// Next.js checks if external image URL matches the
83-
// `images.remotePatterns`
84-
const imageParams = ImageOptimizerCache.validateParams(
85-
{ headers },
86-
queryString,
87-
nextConfig,
88-
false
89-
);
90-
console.log("image params", imageParams);
91-
if ("errorMessage" in imageParams) {
92-
throw new Error(imageParams.errorMessage)
93-
}
94-
return imageParams;
85+
// Next.js checks if external image URL matches the
86+
// `images.remotePatterns`
87+
const imageParams = ImageOptimizerCache.validateParams(
88+
{ headers },
89+
queryString,
90+
nextConfig,
91+
false
92+
);
93+
console.log("image params", imageParams);
94+
if ("errorMessage" in imageParams) {
95+
throw new Error(imageParams.errorMessage);
96+
}
97+
return imageParams;
9598
}
9699

97100
async function optimizeImage(
98-
headers: APIGatewayProxyEventHeaders,
99-
imageParams: any
101+
headers: APIGatewayProxyEventHeaders,
102+
imageParams: any
100103
) {
101-
const result = await imageOptimizer(
102-
{ headers },
103-
{}, // res object is not necessary as it's not actually used.
104-
imageParams,
105-
nextConfig,
106-
false, // not in dev mode
107-
downloadHandler,
108-
)
109-
console.log("optimized result", result);
110-
return result;
104+
const result = await imageOptimizer(
105+
{ headers },
106+
{}, // res object is not necessary as it's not actually used.
107+
imageParams,
108+
nextConfig,
109+
false, // not in dev mode
110+
downloadHandler
111+
);
112+
console.log("optimized result", result);
113+
return result;
111114
}
112115

113116
function buildSuccessResponse(result: any) {
114-
return {
115-
statusCode: 200,
116-
body: result.buffer.toString("base64"),
117-
isBase64Encoded: true,
118-
headers: {
119-
Vary: "Accept",
120-
"Cache-Control": `public,max-age=${result.maxAge},immutable`,
121-
"Content-Type": result.contentType,
122-
},
123-
};
117+
return {
118+
statusCode: 200,
119+
body: result.buffer.toString("base64"),
120+
isBase64Encoded: true,
121+
headers: {
122+
Vary: "Accept",
123+
"Cache-Control": `public,max-age=${result.maxAge},immutable`,
124+
"Content-Type": result.contentType,
125+
},
126+
};
124127
}
125128

126129
function buildFailureResponse(e: any) {
127-
console.error(e)
128-
return {
129-
statusCode: 500,
130-
headers: {
131-
Vary: "Accept",
132-
// For failed images, allow client to retry after 1 minute.
133-
"Cache-Control": `public,max-age=60,immutable`,
134-
"Content-Type": "application/json"
135-
},
136-
body: e?.message || e?.toString() || e
137-
}
130+
console.error(e);
131+
return {
132+
statusCode: 500,
133+
headers: {
134+
Vary: "Accept",
135+
// For failed images, allow client to retry after 1 minute.
136+
"Cache-Control": `public,max-age=60,immutable`,
137+
"Content-Type": "application/json",
138+
},
139+
body: e?.message || e?.toString() || e,
140+
};
138141
}
139142

140143
async function downloadHandler(
141-
_req: IncomingMessage,
142-
res: ServerResponse,
143-
url: NextUrlWithParsedQuery
144+
_req: IncomingMessage,
145+
res: ServerResponse,
146+
url: NextUrlWithParsedQuery
144147
) {
145-
// downloadHandler is called by Next.js. We don't call this function
146-
// directly.
147-
console.log("downloadHandler url", url);
148-
149-
// Reads the output from the Writable and writes to the response
150-
const pipeRes = (w: Writable, res: ServerResponse) => {
151-
w.pipe(res)
152-
.once("close", () => {
153-
res.statusCode = 200
154-
res.end()
155-
})
156-
.once("error", (err) => {
157-
console.error("Failed to get image", { err })
158-
res.statusCode = 400
159-
res.end()
160-
})
161-
}
162-
163-
try {
164-
// Case 1: remote image URL => download the image from the URL
165-
if (url.href.toLowerCase().match(/^https?:\/\//)) {
166-
pipeRes(https.get(url), res)
167-
}
168-
// Case 2: local image => download the image from S3
169-
else {
170-
// Download image from S3
171-
// note: S3 expects keys without leading `/`
172-
const client = new S3Client({})
173-
const response = await client.send(new GetObjectCommand({
174-
Bucket: bucketName,
175-
Key: url.href.replace(/^\//, ""),
176-
}));
177-
178-
if (!response.Body) {
179-
throw new Error("Empty response body from the S3 request.")
180-
}
181-
182-
// @ts-ignore
183-
pipeRes(response.Body, res);
184-
185-
// Respect the bucket file's content-type and cache-control
186-
// imageOptimizer will use this to set the results.maxAge
187-
if (response.ContentType) {
188-
res.setHeader("Content-Type", response.ContentType)
189-
}
190-
if (response.CacheControl) {
191-
res.setHeader("Cache-Control", response.CacheControl)
192-
}
193-
}
194-
} catch (e: any) {
195-
console.error("Failed to download image", e)
196-
throw e;
197-
}
148+
// downloadHandler is called by Next.js. We don't call this function
149+
// directly.
150+
console.log("downloadHandler url", url);
151+
152+
// Reads the output from the Writable and writes to the response
153+
const pipeRes = (w: Writable, res: ServerResponse) => {
154+
w.pipe(res)
155+
.once("close", () => {
156+
res.statusCode = 200;
157+
res.end();
158+
})
159+
.once("error", (err) => {
160+
console.error("Failed to get image", { err });
161+
res.statusCode = 400;
162+
res.end();
163+
});
164+
};
165+
166+
try {
167+
// Case 1: remote image URL => download the image from the URL
168+
if (url.href.toLowerCase().match(/^https?:\/\//)) {
169+
pipeRes(https.get(url), res);
170+
}
171+
// Case 2: local image => download the image from S3
172+
else {
173+
// Download image from S3
174+
// note: S3 expects keys without leading `/`
175+
const client = new S3Client({});
176+
const response = await client.send(
177+
new GetObjectCommand({
178+
Bucket: bucketName,
179+
Key: url.href.replace(/^\//, ""),
180+
})
181+
);
182+
183+
if (!response.Body) {
184+
throw new Error("Empty response body from the S3 request.");
185+
}
186+
187+
// @ts-ignore
188+
pipeRes(response.Body, res);
189+
190+
// Respect the bucket file's content-type and cache-control
191+
// imageOptimizer will use this to set the results.maxAge
192+
if (response.ContentType) {
193+
res.setHeader("Content-Type", response.ContentType);
194+
}
195+
if (response.CacheControl) {
196+
res.setHeader("Cache-Control", response.CacheControl);
197+
}
198+
}
199+
} catch (e: any) {
200+
console.error("Failed to download image", e);
201+
throw e;
202+
}
198203
}

0 commit comments

Comments
 (0)