Skip to content

Commit bdac51a

Browse files
fix: correctly handle expired JWE's in cookies
1 parent 0fdcd13 commit bdac51a

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
@@ -514,10 +514,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
514514
// assert session has been updated
515515
const updatedSessionCookie = response.cookies.get("__session");
516516
expect(updatedSessionCookie).toBeDefined();
517-
const { payload: updatedSessionCookieValue } = await decrypt(
517+
const { payload: updatedSessionCookieValue } = (await decrypt(
518518
updatedSessionCookie!.value,
519519
secret
520-
);
520+
)) as jose.JWTDecryptResult;
521521
expect(updatedSessionCookieValue).toEqual(
522522
expect.objectContaining({
523523
user: {
@@ -954,7 +954,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
954954
`__txn_${authorizationUrl.searchParams.get("state")}`
955955
);
956956
expect(transactionCookie).toBeDefined();
957-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
957+
expect(
958+
(
959+
(await decrypt(
960+
transactionCookie!.value,
961+
secret
962+
)) as jose.JWTDecryptResult
963+
).payload
964+
).toEqual(
958965
expect.objectContaining({
959966
nonce: authorizationUrl.searchParams.get("nonce"),
960967
codeVerifier: expect.any(String),
@@ -1158,7 +1165,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
11581165
);
11591166
expect(transactionCookie).toBeDefined();
11601167
expect(
1161-
(await decrypt(transactionCookie!.value, secret)).payload
1168+
(
1169+
(await decrypt(
1170+
transactionCookie!.value,
1171+
secret
1172+
)) as jose.JWTDecryptResult
1173+
).payload
11621174
).toEqual(
11631175
expect.objectContaining({
11641176
nonce: authorizationUrl.searchParams.get("nonce"),
@@ -1493,7 +1505,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
14931505
`__txn_${authorizationUrl.searchParams.get("state")}`
14941506
);
14951507
expect(transactionCookie).toBeDefined();
1496-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1508+
expect(
1509+
(
1510+
(await decrypt(
1511+
transactionCookie!.value,
1512+
secret
1513+
)) as jose.JWTDecryptResult
1514+
).payload
1515+
).toEqual(
14971516
expect.objectContaining({
14981517
nonce: authorizationUrl.searchParams.get("nonce"),
14991518
maxAge: 3600,
@@ -1540,7 +1559,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
15401559
`__txn_${authorizationUrl.searchParams.get("state")}`
15411560
);
15421561
expect(transactionCookie).toBeDefined();
1543-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1562+
expect(
1563+
(
1564+
(await decrypt(
1565+
transactionCookie!.value,
1566+
secret
1567+
)) as jose.JWTDecryptResult
1568+
).payload
1569+
).toEqual(
15441570
expect.objectContaining({
15451571
nonce: authorizationUrl.searchParams.get("nonce"),
15461572
codeVerifier: expect.any(String),
@@ -1586,7 +1612,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
15861612
`__txn_${authorizationUrl.searchParams.get("state")}`
15871613
);
15881614
expect(transactionCookie).toBeDefined();
1589-
expect((await decrypt(transactionCookie!.value, secret)).payload).toEqual(
1615+
expect(
1616+
(
1617+
(await decrypt(
1618+
transactionCookie!.value,
1619+
secret
1620+
)) as jose.JWTDecryptResult
1621+
).payload
1622+
).toEqual(
15901623
expect.objectContaining({
15911624
nonce: authorizationUrl.searchParams.get("nonce"),
15921625
codeVerifier: expect.any(String),
@@ -1720,7 +1753,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
17201753
const state = transactionCookie.name.replace("__txn_", "");
17211754
expect(transactionCookie).toBeDefined();
17221755
expect(
1723-
(await decrypt(transactionCookie!.value, secret)).payload
1756+
(
1757+
(await decrypt(
1758+
transactionCookie.value,
1759+
secret
1760+
)) as jose.JWTDecryptResult
1761+
).payload
17241762
).toEqual(
17251763
expect.objectContaining({
17261764
nonce: expect.any(String),
@@ -1874,7 +1912,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
18741912
const state = transactionCookie.name.replace("__txn_", "");
18751913
expect(transactionCookie).toBeDefined();
18761914
expect(
1877-
(await decrypt(transactionCookie!.value, secret)).payload
1915+
(
1916+
(await decrypt(
1917+
transactionCookie.value,
1918+
secret
1919+
)) as jose.JWTDecryptResult
1920+
).payload
18781921
).toEqual(
18791922
expect.objectContaining({
18801923
nonce: expect.any(String),
@@ -1956,7 +1999,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
19561999
const state = transactionCookie.name.replace("__txn_", "");
19572000
expect(transactionCookie).toBeDefined();
19582001
expect(
1959-
(await decrypt(transactionCookie!.value, secret)).payload
2002+
(await decrypt(transactionCookie!.value, secret))!.payload
19602003
).toEqual(
19612004
expect.objectContaining({
19622005
nonce: expect.any(String),
@@ -2512,7 +2555,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
25122555
// validate the session cookie
25132556
const sessionCookie = response.cookies.get("__session");
25142557
expect(sessionCookie).toBeDefined();
2515-
const { payload: session } = await decrypt(sessionCookie!.value, secret);
2558+
const { payload: session } = (await decrypt(
2559+
sessionCookie!.value,
2560+
secret
2561+
)) as jose.JWTDecryptResult;
25162562
expect(session).toEqual(
25172563
expect.objectContaining({
25182564
user: {
@@ -2695,7 +2741,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
26952741
// validate the session cookie
26962742
const sessionCookie = response.cookies.get("__session");
26972743
expect(sessionCookie).toBeDefined();
2698-
const { payload: session } = await decrypt(sessionCookie!.value, secret);
2744+
const { payload: session } = (await decrypt(
2745+
sessionCookie!.value,
2746+
secret
2747+
)) as jose.JWTDecryptResult;
26992748
expect(session).toEqual(
27002749
expect.objectContaining({
27012750
user: {
@@ -3081,10 +3130,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
30813130
// validate the session cookie
30823131
const sessionCookie = response.cookies.get("__session");
30833132
expect(sessionCookie).toBeDefined();
3084-
const { payload: session } = await decrypt(
3133+
const { payload: session } = (await decrypt(
30853134
sessionCookie!.value,
30863135
secret
3087-
);
3136+
)) as jose.JWTDecryptResult;
30883137
expect(session).toEqual(expect.objectContaining(expectedSession));
30893138
});
30903139

@@ -3545,10 +3594,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
35453594
// validate the session cookie
35463595
const sessionCookie = response.cookies.get("__session");
35473596
expect(sessionCookie).toBeDefined();
3548-
const { payload: session } = await decrypt(
3597+
const { payload: session } = (await decrypt(
35493598
sessionCookie!.value,
35503599
secret
3551-
);
3600+
)) as jose.JWTDecryptResult;
35523601
expect(session).toEqual(
35533602
expect.objectContaining({
35543603
user: {
@@ -3679,10 +3728,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
36793728
// validate the session cookie
36803729
const sessionCookie = response.cookies.get("__session");
36813730
expect(sessionCookie).toBeDefined();
3682-
const { payload: session } = await decrypt(
3731+
const { payload: session } = (await decrypt(
36833732
sessionCookie!.value,
36843733
secret
3685-
);
3734+
)) as jose.JWTDecryptResult;
36863735
expect(session).toEqual(
36873736
expect.objectContaining({
36883737
user: {
@@ -3783,10 +3832,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
37833832

37843833
// validate that the session cookie has been updated
37853834
const updatedSessionCookie = response.cookies.get("__session");
3786-
const { payload: updatedSession } = await decrypt<SessionData>(
3835+
const { payload: updatedSession } = (await decrypt<SessionData>(
37873836
updatedSessionCookie!.value,
37883837
secret
3789-
);
3838+
)) as jose.JWTDecryptResult<SessionData>;
37903839
expect(updatedSession.tokenSet.accessToken).toEqual(newAccessToken);
37913840
});
37923841

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";
2+
import * as jose from "jose";
23
import { describe, expect, it } from "vitest";
34

45
import { generateSecret } from "../test/utils";
@@ -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";
@@ -332,7 +333,10 @@ describe("Stateful Session Store", async () => {
332333
await sessionStore.set(requestCookies, responseCookies, session);
333334

334335
const cookie = responseCookies.get("__session");
335-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
336+
const { payload: cookieValue } = (await decrypt(
337+
cookie!.value,
338+
secret
339+
)) as jose.JWTDecryptResult;
336340

337341
expect(cookie).toBeDefined();
338342
expect(cookieValue).toHaveProperty("id");
@@ -386,7 +390,10 @@ describe("Stateful Session Store", async () => {
386390
await sessionStore.set(requestCookies, responseCookies, session);
387391

388392
const cookie = responseCookies.get("__session");
389-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
393+
const { payload: cookieValue } = (await decrypt(
394+
cookie!.value,
395+
secret
396+
)) as jose.JWTDecryptResult;
390397

391398
expect(cookie).toBeDefined();
392399
expect(cookieValue).toHaveProperty("id");
@@ -434,7 +441,10 @@ describe("Stateful Session Store", async () => {
434441
await sessionStore.set(requestCookies, responseCookies, session);
435442

436443
const cookie = responseCookies.get("__session");
437-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
444+
const { payload: cookieValue } = (await decrypt(
445+
cookie!.value,
446+
secret
447+
)) as jose.JWTDecryptResult;
438448

439449
expect(cookie).toBeDefined();
440450
expect(cookieValue).toHaveProperty("id");
@@ -493,7 +503,10 @@ describe("Stateful Session Store", async () => {
493503
await sessionStore.set(requestCookies, responseCookies, session, true);
494504

495505
const cookie = responseCookies.get("__session");
496-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
506+
const { payload: cookieValue } = (await decrypt(
507+
cookie!.value,
508+
secret
509+
)) as jose.JWTDecryptResult;
497510

498511
expect(cookie).toBeDefined();
499512
expect(store.delete).toHaveBeenCalledWith(sessionId); // the old session should be deleted
@@ -542,7 +555,10 @@ describe("Stateful Session Store", async () => {
542555
await sessionStore.set(requestCookies, responseCookies, session);
543556

544557
const cookie = responseCookies.get("__session");
545-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
558+
const { payload: cookieValue } = (await decrypt(
559+
cookie!.value,
560+
secret
561+
)) as jose.JWTDecryptResult;
546562

547563
expect(cookie).toBeDefined();
548564
expect(cookieValue).toHaveProperty("id");
@@ -592,7 +608,10 @@ describe("Stateful Session Store", async () => {
592608
await sessionStore.set(requestCookies, responseCookies, session);
593609

594610
const cookie = responseCookies.get("__session");
595-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
611+
const { payload: cookieValue } = (await decrypt(
612+
cookie!.value,
613+
secret
614+
)) as jose.JWTDecryptResult;
596615

597616
expect(cookie).toBeDefined();
598617
expect(cookieValue).toHaveProperty("id");
@@ -686,7 +705,10 @@ describe("Stateful Session Store", async () => {
686705
await sessionStore.set(requestCookies, responseCookies, session);
687706

688707
const cookie = responseCookies.get("my-session");
689-
const { payload: cookieValue } = await decrypt(cookie!.value, secret);
708+
const { payload: cookieValue } = (await decrypt(
709+
cookie!.value,
710+
secret
711+
)) as jose.JWTDecryptResult;
690712

691713
expect(cookie).toBeDefined();
692714
expect(cookieValue).toHaveProperty("id");

0 commit comments

Comments
 (0)