Skip to content

Commit fab9069

Browse files
authored
Merge branch 'main' into fix/dpop-nonce-retry-race
2 parents 348e445 + 1188193 commit fab9069

4 files changed

Lines changed: 56 additions & 2 deletions

File tree

EXAMPLES.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,43 @@ export async function GET() {
929929
}
930930
```
931931

932+
**App Router Route Handlers — Refresh + Custom Response Headers/Cookies:**
933+
934+
If your Route Handler needs to both refresh the session **and** return a `NextResponse` you fully control (e.g., to set additional cookies with a `Domain` or `SameSite` attribute), use the explicit `getAccessToken(req, res, options)` signature. This writes the refreshed session directly onto the `NextResponse` you pass, so all `Set-Cookie` headers — session and custom — are consolidated on the one response object you return.
935+
936+
```typescript
937+
// app/api/refresh/route.ts
938+
import { NextRequest, NextResponse } from "next/server";
939+
940+
import { auth0 } from "@/lib/auth0";
941+
942+
export async function POST(req: NextRequest) {
943+
// 1. Create the response object you will return.
944+
const res = new NextResponse();
945+
946+
// 2. Pass req + res explicitly so the SDK writes the refreshed session
947+
// cookies directly onto `res` rather than into Next.js's internal
948+
// AsyncLocalStorage store. This makes the Set-Cookie headers (including
949+
// Domain, SameSite, Secure, etc. from your session.cookie config)
950+
// available on the response object you control.
951+
const { token } = await auth0.getAccessToken(req, res, { refresh: true });
952+
953+
// 3. Set any additional cookies on the same response object.
954+
res.cookies.set("my-cookie", "value", {
955+
domain: ".example.com",
956+
secure: true,
957+
sameSite: "lax"
958+
});
959+
960+
// 4. Return the single response — it now carries both the refreshed
961+
// session Set-Cookie headers and your custom cookie.
962+
return res;
963+
}
964+
```
965+
966+
> [!IMPORTANT]
967+
> Calling `getAccessToken({ refresh: true })` (without `req`/`res`) in a Route Handler writes the refreshed session through Next.js's internal cookie store, **not** onto a `NextResponse` you construct. If you then build a `new NextResponse()` and add cookies to it, that response will be missing the refreshed session cookies. Always pass `req` and `res` explicitly when you need all cookies on the same response object.
968+
932969
**Pages Router (getServerSideProps, API Routes):**
933970

934971
When calling `getAccessToken` with request and response objects (from `getServerSideProps` context or an API route), the options object is passed as the third argument.

src/server/cookies.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ describe("encrypt/decrypt", async () => {
4343
expect(decrypted).toBeNull();
4444
});
4545

46+
it("should fail to decrypt a tampered payload", async () => {
47+
const payload = { key: "value" };
48+
const maxAge = 60 * 60; // 1 hour in seconds
49+
const expiration = Math.floor(Date.now() / 1000 + maxAge);
50+
const encrypted = await encrypt(payload, secret, expiration);
51+
52+
// Tamper with the encrypted payload by shifting the first character
53+
const tampered =
54+
String.fromCharCode(encrypted.charCodeAt(0) + 1) + encrypted.slice(1);
55+
56+
const decrypted = await decrypt(tampered, secret);
57+
expect(decrypted).toBeNull();
58+
});
59+
4660
it("should fail to encrypt if a secret is not provided", async () => {
4761
const payload = { key: "value" };
4862
const maxAge = 60 * 60; // 1 hour in seconds

src/server/cookies.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ export async function decrypt<T>(
6868
// When the JWE can not be decrypted or has expired, return null to indicate an invalid cookie and treat it as non-existent.
6969
if (
7070
e.code === "ERR_JWE_DECRYPTION_FAILED" ||
71-
e.code === "ERR_JWT_EXPIRED"
71+
e.code === "ERR_JWT_EXPIRED" ||
72+
e.code === "ERR_JWE_INVALID"
7273
) {
7374
return null;
7475
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ export class StatefulSessionStore extends AbstractSessionStore {
7474
try {
7575
const sessionCookie = await cookies.decrypt<SessionCookieValue>(
7676
cookie.value,
77-
this.secret
77+
this.secret,
78+
undefined,
79+
true // throwOnJWEErrors: allow catching ERR_JWE_INVALID to handle legacy sessions
7880
);
7981

8082
if (sessionCookie === null) {

0 commit comments

Comments
 (0)