Skip to content

Commit 15e7511

Browse files
fix: correctly handle expired JWE's in cookies (#2082)
1 parent a063a8d commit 15e7511

9 files changed

+291
-109
lines changed

src/server/auth-client.test.ts

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
520520
// assert session has been updated
521521
const updatedSessionCookie = response.cookies.get("__session");
522522
expect(updatedSessionCookie).toBeDefined();
523-
const { payload: updatedSessionCookieValue } = await decrypt(
523+
const { payload: updatedSessionCookieValue } = (await decrypt(
524524
updatedSessionCookie!.value,
525525
secret
526-
);
526+
)) as jose.JWTDecryptResult;
527527
expect(updatedSessionCookieValue).toEqual(
528528
expect.objectContaining({
529529
user: {
@@ -960,7 +960,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
960960
`__txn_${authorizationUrl.searchParams.get("state")}`
961961
);
962962
expect(transactionCookie).toBeDefined();
963-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
963+
expect(
964+
(
965+
(await decrypt(
966+
transactionCookie!.value,
967+
secret
968+
)) as jose.JWTDecryptResult
969+
).payload
970+
).toEqual(
964971
expect.objectContaining({
965972
nonce: authorizationUrl.searchParams.get("nonce"),
966973
codeVerifier: expect.any(String),
@@ -1164,7 +1171,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
11641171
);
11651172
expect(transactionCookie).toBeDefined();
11661173
expect(
1167-
(await decrypt(transactionCookie!.value, secret)).payload
1174+
(
1175+
(await decrypt(
1176+
transactionCookie!.value,
1177+
secret
1178+
)) as jose.JWTDecryptResult
1179+
).payload
11681180
).toEqual(
11691181
expect.objectContaining({
11701182
nonce: authorizationUrl.searchParams.get("nonce"),
@@ -1499,7 +1511,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
14991511
`__txn_${authorizationUrl.searchParams.get("state")}`
15001512
);
15011513
expect(transactionCookie).toBeDefined();
1502-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1514+
expect(
1515+
(
1516+
(await decrypt(
1517+
transactionCookie!.value,
1518+
secret
1519+
)) as jose.JWTDecryptResult
1520+
).payload
1521+
).toEqual(
15031522
expect.objectContaining({
15041523
nonce: authorizationUrl.searchParams.get("nonce"),
15051524
maxAge: 3600,
@@ -1546,7 +1565,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
15461565
`__txn_${authorizationUrl.searchParams.get("state")}`
15471566
);
15481567
expect(transactionCookie).toBeDefined();
1549-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1568+
expect(
1569+
(
1570+
(await decrypt(
1571+
transactionCookie!.value,
1572+
secret
1573+
)) as jose.JWTDecryptResult
1574+
).payload
1575+
).toEqual(
15501576
expect.objectContaining({
15511577
nonce: authorizationUrl.searchParams.get("nonce"),
15521578
codeVerifier: expect.any(String),
@@ -1592,7 +1618,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
15921618
`__txn_${authorizationUrl.searchParams.get("state")}`
15931619
);
15941620
expect(transactionCookie).toBeDefined();
1595-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1621+
expect(
1622+
(
1623+
(await decrypt(
1624+
transactionCookie!.value,
1625+
secret
1626+
)) as jose.JWTDecryptResult
1627+
).payload
1628+
).toEqual(
15961629
expect.objectContaining({
15971630
nonce: authorizationUrl.searchParams.get("nonce"),
15981631
codeVerifier: expect.any(String),
@@ -1726,7 +1759,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
17261759
const state = transactionCookie.name.replace("__txn_", "");
17271760
expect(transactionCookie).toBeDefined();
17281761
expect(
1729-
(await decrypt(transactionCookie!.value, secret)).payload
1762+
(
1763+
(await decrypt(
1764+
transactionCookie.value,
1765+
secret
1766+
)) as jose.JWTDecryptResult
1767+
).payload
17301768
).toEqual(
17311769
expect.objectContaining({
17321770
nonce: expect.any(String),
@@ -1880,7 +1918,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
18801918
const state = transactionCookie.name.replace("__txn_", "");
18811919
expect(transactionCookie).toBeDefined();
18821920
expect(
1883-
(await decrypt(transactionCookie!.value, secret)).payload
1921+
(
1922+
(await decrypt(
1923+
transactionCookie.value,
1924+
secret
1925+
)) as jose.JWTDecryptResult
1926+
).payload
18841927
).toEqual(
18851928
expect.objectContaining({
18861929
nonce: expect.any(String),
@@ -1962,7 +2005,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
19622005
const state = transactionCookie.name.replace("__txn_", "");
19632006
expect(transactionCookie).toBeDefined();
19642007
expect(
1965-
(await decrypt(transactionCookie!.value, secret)).payload
2008+
(await decrypt(transactionCookie!.value, secret))!.payload
19662009
).toEqual(
19672010
expect.objectContaining({
19682011
nonce: expect.any(String),
@@ -2554,7 +2597,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
25542597
// validate the session cookie
25552598
const sessionCookie = response.cookies.get("__session");
25562599
expect(sessionCookie).toBeDefined();
2557-
const { payload: session } = await decrypt(sessionCookie!.value, secret);
2600+
const { payload: session } = (await decrypt(
2601+
sessionCookie!.value,
2602+
secret
2603+
)) as jose.JWTDecryptResult;
25582604
expect(session).toEqual(
25592605
expect.objectContaining({
25602606
user: {
@@ -2735,7 +2781,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
27352781
// validate the session cookie
27362782
const sessionCookie = response.cookies.get("__session");
27372783
expect(sessionCookie).toBeDefined();
2738-
const { payload: session } = await decrypt(sessionCookie!.value, secret);
2784+
const { payload: session } = (await decrypt(
2785+
sessionCookie!.value,
2786+
secret
2787+
)) as jose.JWTDecryptResult;
27392788
expect(session).toEqual(
27402789
expect.objectContaining({
27412790
user: {
@@ -3119,10 +3168,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
31193168
// validate the session cookie
31203169
const sessionCookie = response.cookies.get("__session");
31213170
expect(sessionCookie).toBeDefined();
3122-
const { payload: session } = await decrypt(
3171+
const { payload: session } = (await decrypt(
31233172
sessionCookie!.value,
31243173
secret
3125-
);
3174+
)) as jose.JWTDecryptResult;
31263175
expect(session).toEqual(expect.objectContaining(expectedSession));
31273176
});
31283177

@@ -3662,10 +3711,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
36623711
// validate the session cookie
36633712
const sessionCookie = response.cookies.get("__session");
36643713
expect(sessionCookie).toBeDefined();
3665-
const { payload: session } = await decrypt(
3714+
const { payload: session } = (await decrypt(
36663715
sessionCookie!.value,
36673716
secret
3668-
);
3717+
)) as jose.JWTDecryptResult;
36693718
expect(session).toEqual(
36703719
expect.objectContaining({
36713720
user: {
@@ -3796,10 +3845,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
37963845
// validate the session cookie
37973846
const sessionCookie = response.cookies.get("__session");
37983847
expect(sessionCookie).toBeDefined();
3799-
const { payload: session } = await decrypt(
3848+
const { payload: session } = (await decrypt(
38003849
sessionCookie!.value,
38013850
secret
3802-
);
3851+
)) as jose.JWTDecryptResult;
38033852
expect(session).toEqual(
38043853
expect.objectContaining({
38053854
user: {
@@ -3900,10 +3949,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
39003949

39013950
// validate that the session cookie has been updated
39023951
const updatedSessionCookie = response.cookies.get("__session");
3903-
const { payload: updatedSession } = await decrypt<SessionData>(
3952+
const { payload: updatedSession } = (await decrypt<SessionData>(
39043953
updatedSessionCookie!.value,
39053954
secret
3906-
);
3955+
)) as jose.JWTDecryptResult<SessionData>;
39073956
expect(updatedSession.tokenSet.accessToken).toEqual(newAccessToken);
39083957
});
39093958

src/server/cookies.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NextResponse } from "next/server.js";
2+
import * as jose from "jose";
23
import { describe, expect, it } from "vitest";
34

45
import { generateSecret } from "../test/utils.js";
@@ -13,9 +14,9 @@ describe("encrypt/decrypt", async () => {
1314
const maxAge = 60 * 60; // 1 hour in seconds
1415
const expiration = Math.floor(Date.now() / 1000 + maxAge);
1516
const encrypted = await encrypt(payload, secret, expiration);
16-
const decrypted = await decrypt(encrypted, secret);
17+
const decrypted = await decrypt(encrypted, secret) as jose.JWTDecryptResult;
1718

18-
expect(decrypted.payload).toEqual(expect.objectContaining(payload));
19+
expect(decrypted!.payload).toEqual(expect.objectContaining(payload));
1920
});
2021

2122
it("should fail to decrypt a payload with the incorrect secret", async () => {
@@ -32,9 +33,8 @@ describe("encrypt/decrypt", async () => {
3233
const payload = { key: "value" };
3334
const expiration = Math.floor(Date.now() / 1000 - 60); // 60 seconds in the past
3435
const encrypted = await encrypt(payload, secret, expiration);
35-
await expect(() => decrypt(encrypted, secret)).rejects.toThrowError(
36-
`"exp" claim timestamp check failed`
37-
);
36+
const decrypted = await decrypt(encrypted, secret);
37+
expect(decrypted).toBeNull();
3838
});
3939

4040
it("should fail to encrypt if a secret is not provided", async () => {

src/server/cookies.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,27 @@ export async function decrypt<T>(
4444
secret: string,
4545
options?: jose.JWTDecryptOptions
4646
) {
47-
const encryptionSecret = await hkdf(
48-
DIGEST,
49-
secret,
50-
"",
51-
ENCRYPTION_INFO,
52-
BYTE_LENGTH
53-
);
47+
try {
48+
const encryptionSecret = await hkdf(
49+
DIGEST,
50+
secret,
51+
"",
52+
ENCRYPTION_INFO,
53+
BYTE_LENGTH
54+
);
5455

55-
const cookie = await jose.jwtDecrypt<T>(cookieValue, encryptionSecret, {
56-
...options,
57-
...{ clockTolerance: 15 }
58-
});
56+
const cookie = await jose.jwtDecrypt<T>(cookieValue, encryptionSecret, {
57+
...options,
58+
...{ clockTolerance: 15 }
59+
});
5960

60-
return cookie;
61+
return cookie;
62+
} catch (e: any) {
63+
if (e.code === "ERR_JWT_EXPIRED") {
64+
return null;
65+
}
66+
throw e;
67+
}
6168
}
6269

6370
/**

src/server/session/stateful-session-store.test.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as jose from "jose";
12
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23

34
import { generateSecret } from "../../test/utils.js";
@@ -335,7 +336,10 @@ describe("Stateful Session Store", async () => {
335336
await sessionStore.set(requestCookies, responseCookies, session);
336337

337338
const cookie = responseCookies.get("__session");
338-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
339+
const { payload: cookieValue } = (await decrypt(
340+
cookie!.value,
341+
secret
342+
)) as jose.JWTDecryptResult;
339343

340344
expect(cookie).toBeDefined();
341345
expect(cookieValue).toHaveProperty("id");
@@ -389,7 +393,10 @@ describe("Stateful Session Store", async () => {
389393
await sessionStore.set(requestCookies, responseCookies, session);
390394

391395
const cookie = responseCookies.get("__session");
392-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
396+
const { payload: cookieValue } = (await decrypt(
397+
cookie!.value,
398+
secret
399+
)) as jose.JWTDecryptResult;
393400

394401
expect(cookie).toBeDefined();
395402
expect(cookieValue).toHaveProperty("id");
@@ -437,7 +444,10 @@ describe("Stateful Session Store", async () => {
437444
await sessionStore.set(requestCookies, responseCookies, session);
438445

439446
const cookie = responseCookies.get("__session");
440-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
447+
const { payload: cookieValue } = (await decrypt(
448+
cookie!.value,
449+
secret
450+
)) as jose.JWTDecryptResult;
441451

442452
expect(cookie).toBeDefined();
443453
expect(cookieValue).toHaveProperty("id");
@@ -496,7 +506,10 @@ describe("Stateful Session Store", async () => {
496506
await sessionStore.set(requestCookies, responseCookies, session, true);
497507

498508
const cookie = responseCookies.get("__session");
499-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
509+
const { payload: cookieValue } = (await decrypt(
510+
cookie!.value,
511+
secret
512+
)) as jose.JWTDecryptResult;
500513

501514
expect(cookie).toBeDefined();
502515
expect(store.delete).toHaveBeenCalledWith(sessionId); // the old session should be deleted
@@ -545,7 +558,10 @@ describe("Stateful Session Store", async () => {
545558
await sessionStore.set(requestCookies, responseCookies, session);
546559

547560
const cookie = responseCookies.get("__session");
548-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
561+
const { payload: cookieValue } = (await decrypt(
562+
cookie!.value,
563+
secret
564+
)) as jose.JWTDecryptResult;
549565

550566
expect(cookie).toBeDefined();
551567
expect(cookieValue).toHaveProperty("id");
@@ -595,7 +611,10 @@ describe("Stateful Session Store", async () => {
595611
await sessionStore.set(requestCookies, responseCookies, session);
596612

597613
const cookie = responseCookies.get("__session");
598-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
614+
const { payload: cookieValue } = (await decrypt(
615+
cookie!.value,
616+
secret
617+
)) as jose.JWTDecryptResult;
599618

600619
expect(cookie).toBeDefined();
601620
expect(cookieValue).toHaveProperty("id");
@@ -689,7 +708,10 @@ describe("Stateful Session Store", async () => {
689708
await sessionStore.set(requestCookies, responseCookies, session);
690709

691710
const cookie = responseCookies.get("my-session");
692-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
711+
const { payload: cookieValue } = (await decrypt(
712+
cookie!.value,
713+
secret
714+
)) as jose.JWTDecryptResult;
693715

694716
expect(cookie).toBeDefined();
695717
expect(cookieValue).toHaveProperty("id");

0 commit comments

Comments
 (0)