feat: add anonymous sessions support - #2797
Conversation
Introduces pre-login anonymous sessions backed by Auth0-issued access
tokens, letting applications attach identity and metadata to a visitor
before they authenticate and link that session at login.
Server API (Auth0Client):
- getAnonymousSession() — read the current anonymous session (App Router
zero-arg and Pages Router req forms); returns null when absent, disabled,
or the cookie is malformed/expired.
- createAnonymousSession() — create and persist a fresh session (zero-arg
Server Action and req/res Route Handler forms).
- anonymousSession config block ({ enabled, cookie: { name, sameSite,
secure } }) mounts three routes: GET /auth/anonymous-session,
POST /auth/anonymous-session/update, POST /auth/anonymous-session/logout,
with NEXT_PUBLIC_* route overrides.
Client API:
- useAnonymousSession() hook ({ anonymous, isLoading, error, invalidate }).
- Auth0Provider anonymousSession / anonymousSessionRoute props for SSR
cache seeding (no loading flash).
Behavior:
- Renewal state machine: valid access token returned as-is; expired access
token renewed from the session token in a request/response context;
expired session token triggers silent recovery (new session). Read-only
Server Component context defers renewal (D7).
- Metadata updates delegate the merge to the authorization server; 1KB cap
enforced on the UTF-8 byte length.
- Typed AnonymousSessionError with code→HTTP-status mapping (401
invalid_client; 403 feature_not_enabled/unauthorized_client; 500
server_error; 400 otherwise); session_expired/invalid_session_token are
silently recovered.
Security:
- Session-token fixation mitigation (SEC-1): session_token is a reserved
authorize parameter (caller values stripped), injected only from the
SDK's own encrypted cookie, and bound to the CSRF-protected transaction
state via anonymousSessionLinked.
- auth0_anon cookie is HttpOnly, Secure by default, JWE-encrypted, with
no-store cache headers; chunked-cookie fragments are cleared on logout.
Additive and fully gated: with anonymousSession unset no routes are
mounted, the methods short-circuit, and no new network calls occur.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2797 +/- ##
==========================================
- Coverage 88.36% 87.73% -0.63%
==========================================
Files 77 84 +7
Lines 10297 12166 +1869
Branches 2145 2511 +366
==========================================
+ Hits 9099 10674 +1575
- Misses 1154 1445 +291
- Partials 44 47 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| // Loading state (no data yet, no error) | ||
| return { | ||
| anonymous: data ?? null, |
📝 WalkthroughWalkthroughChangesAnonymous sessions
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Auth0Provider
participant useAnonymousSession
participant AuthClient
participant Auth0
Client->>Auth0Provider: Render with optional anonymousSession
Auth0Provider->>useAnonymousSession: Seed SWR cache
Client->>useAnonymousSession: Request anonymous session
useAnonymousSession->>AuthClient: Fetch session route
AuthClient->>Auth0: Create or renew session token
Auth0-->>AuthClient: Return token response
AuthClient-->>useAnonymousSession: Return session JSON
useAnonymousSession-->>Client: Expose session and loading/error state
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (13)
src/client/providers/auth0-provider.test.tsx-165-167 (1)
165-167: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winBoth env-var tests restore
process.envincorrectly. Eachfinallyblock assigns the saved value back toprocess.env. If the variable was unset before the test, the saved value isundefinedand Node coerces the assignment to the string"undefined". The variable then stays truthy for every later test in the same worker, and the provider resolves the affected route key to"undefined". Delete the key when the saved value isundefined.
src/client/providers/auth0-provider.test.tsx#L165-L167: replace the assignment with a conditional delete forNEXT_PUBLIC_PROFILE_ROUTE.src/client/providers/auth0-provider.test.tsx#L182-L184: replace the assignment with a conditional delete forNEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE.💚 Proposed fix for both sites
} finally { - process.env.NEXT_PUBLIC_PROFILE_ROUTE = originalEnv; + if (originalEnv === undefined) { + delete process.env.NEXT_PUBLIC_PROFILE_ROUTE; + } else { + process.env.NEXT_PUBLIC_PROFILE_ROUTE = originalEnv; + } }} finally { - process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE = originalEnv; + if (originalEnv === undefined) { + delete process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE; + } else { + process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE = originalEnv; + } }Alternatively, use
vi.stubEnvwithvi.unstubAllEnvs()in anafterEach, which handles the unset case correctly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/providers/auth0-provider.test.tsx` around lines 165 - 167, Update both environment restoration finally blocks in auth0-provider.test.tsx: for NEXT_PUBLIC_PROFILE_ROUTE at lines 165-167 and NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE at lines 182-184, delete the corresponding process.env key when its saved value is undefined; otherwise restore the saved value normally.docs/anonymous-sessions.md-25-25 (1)
25-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix the broken table-of-contents link.
Line 25 points to
#ending-an-anonymous-session, but the document defines## Logging Out. Change the link to#logging-outor rename the heading.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/anonymous-sessions.md` at line 25, Update the table-of-contents entry for “Ending an Anonymous Session” to target the document’s existing “Logging Out” heading anchor, `#logging-out`, keeping the heading unchanged.Source: Linters/SAST tools
docs/anonymous-sessions.md-164-166 (1)
164-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCheck
error.codeinstead ofinstanceof AnonymousSessionError.The imported error class is documented as an
Errorwith acode, and the other examples handle anonymous errors through the code field. Update both catch blocks to dispatch onerror.codeinstead of relying on the error-class instance check.Also applies to: 411-413
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/anonymous-sessions.md` around lines 164 - 166, Update both catch blocks in the anonymous-session examples to dispatch using the caught error’s code field instead of instanceof AnonymousSessionError. Preserve the existing success-false response and error-code handling, and remove the class-instance check from both locations.Source: Coding guidelines
src/types/anonymous-session.test.ts-162-244 (1)
162-244: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThese tests assert on local literals, not on SDK behavior, but their names claim otherwise.
Test T1.1 at lines 164-180 is named "AnonymousSession.id must be extracted from JWT sub claim (not assigned)". It builds a plain object, copies
mockJWT.subintoextractedId, and asserts thatextractedIdequals the literal it was just assigned. No SDK function runs. Test T2.1 at lines 220-243 decodes a hardcoded token inline and asserts thesub. It also never callstoPublicSession.The tests at lines 182-202 assign a field to an
AnonymousSessionliteral and then assert that the field holds the assigned value. These are compile-time type checks written as runtime assertions.The real coverage of the id-from-sub contract already exists at
src/server/auth-client.anonymous-routes.test.tslines 818-843, wherecreateAnonymousSessionruns andsession.idis compared to the decodedsub. Remove the tautological tests, or rename them so they do not imply that this file verifies the extraction logic. The current names create false confidence in the coverage of a security-relevant contract.Note also the empty catch at lines 236-238. It discards the binding
eand can hide a decode failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/types/anonymous-session.test.ts` around lines 162 - 244, Remove the tautological runtime tests in the “Type definitions” and “JWT claim extraction verification” blocks, since they do not invoke SDK behavior; retain only meaningful type coverage or rename tests to describe type/literal validation without claiming JWT extraction. Do not duplicate the contract already covered by createAnonymousSession in the existing route tests, and remove the unused catch binding in the inline decode logic if that test remains.src/server/auth-client.anonymous-routes.test.ts-365-392 (1)
365-392: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe assertion cannot demonstrate the behavior in the test name.
The test is named "Unmentioned keys preserved". The cookie payload carries
{ a: 1, b: 2, c: 3 }and the update sends{ a: 99 }. The default MSW handler at lines 46-60 returnsmetadata: body.metadata || {}, so the mocked response contains only{ a: 99 }. Keysbandcare never returned. The single assertionexpect(body.metadata).toHaveProperty("a")passes without proving preservation.Either override the handler to simulate the merge, as test T3.3 does at lines 270-304, and then assert
bandc, or rename the test to describe what it verifies.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.anonymous-routes.test.ts` around lines 365 - 392, Update the T3.5 test around handleUpdateAnonymousSession to actually verify unmentioned metadata preservation: override the MSW handler like T3.3 so the response merges the existing cookie metadata with the request, then assert that b and c remain present alongside the updated a value. Keep the “Unmentioned keys preserved” name only if these preservation assertions are added.src/server/client.ts-511-516 (1)
511-516: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe
anonymousSessionoption doc is wrong about disabled behavior in both declarations. The same sentence, "when disabled, routes are not mounted and methods return null", is duplicated on both option interfaces.getAnonymousSessionreturns null when disabled, butcreateAnonymousSessionthrowsAnonymousSessionError("unauthorized_client")(src/server/auth-client.tslines 2762-2767) because its return type is non-nullable. A consumer who follows this doc writes a null check and receives an unhandled throw.
src/server/client.ts#L511-L516: replace the "methods return null" sentence with the split behavior —getAnonymousSessionreturns null,createAnonymousSessionthrowsAnonymousSessionErrorwith codeunauthorized_client.src/server/auth-client.ts#L385-L390: apply the identical correction to theAuthClientOptions.anonymousSessiondoc block.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/client.ts` around lines 511 - 516, The anonymousSession documentation incorrectly says all methods return null when disabled. Update the documentation blocks for anonymousSession in src/server/client.ts lines 511-516 and src/server/auth-client.ts lines 385-390 identically: state that getAnonymousSession returns null, while createAnonymousSession throws AnonymousSessionError with code unauthorized_client.src/server/auth-client.ts-3113-3118 (1)
3113-3118: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winUse
parseJsonBody(req)for the anonymous request body.This handler calls
req.json()directly instead of the shared JSON parser used by nearby POST handlers, and it parses before the metadata size check. Keep parsing behindparseJsonBody(req)so the body limit and parser behavior apply consistently.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.ts` around lines 3113 - 3118, Update the anonymous request handler to obtain its body through the shared parseJsonBody(req) helper instead of calling req.json() directly. Preserve the invalid-request response for parse failures and ensure parsing occurs through the helper before applying the metadata size check, matching nearby POST handlers.src/server/auth-client.test.ts-4559-4560 (1)
4559-4560: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd coverage for
anonymousSessionLinked: truereachingonCallback.
src/server/anonymous-session.flow.test.ts:626-655records the transaction-state flag being set, but no test assertsonCallbackreceivesanonymousSessionLinked: true. Add the positive flow case insrc/server/auth-client.test.tsor an equivalent callback-flow test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.test.ts` around lines 4559 - 4560, Add a positive callback-flow test near the existing auth-client transaction-state cases, using the relevant onCallback test setup, that starts with anonymousSessionLinked set to true and asserts onCallback receives anonymousSessionLinked: true. Keep the existing false-case coverage unchanged and verify the flag propagates through the callback payload.src/server/client.ts-903-913 (1)
903-913: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix the Prettier failure on the
reqCookiestype annotation.The
Lint Codecheck fails at line 913. Prettier wants the union collapsed.🎨 Proposed formatting fix
- let reqCookies: - | RequestCookies - | import("./cookies.js").ReadonlyRequestCookies; + let reqCookies: RequestCookies | import("./cookies.js").ReadonlyRequestCookies;Run
npx prettier --write src/server/client.tsto apply the exact formatting the check expects.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/client.ts` around lines 903 - 913, Update the reqCookies type annotation in the client request-cookie initialization to use Prettier’s collapsed union formatting. Preserve the existing RequestCookies and ReadonlyRequestCookies types and runtime branching behavior.Source: Linters/SAST tools
src/server/client.test.ts-1424-1436 (1)
1424-1436: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe test does not pass the anonymous-session config it claims to test.
The title states "Auth0Client can be instantiated with anonymous session config", but the options object omits
anonymousSession. The test passes today and would keep passing if the constructor rejected that option entirely.Add the config so the assertion matches the title.
💚 Proposed fix
const testClient = new Auth0Client({ domain: "test.auth0.com", clientId: "test-id", clientSecret: "test-secret", - secret: "test-secret-32-bytes-minimum-1234567890ab" + secret: "test-secret-32-bytes-minimum-1234567890ab", + anonymousSession: { + enabled: true, + cookie: { name: "custom_anon", sameSite: "strict", secure: true } + } });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/client.test.ts` around lines 1424 - 1436, Update the Auth0Client instantiation in the “C2/C3: Auth0Client can be instantiated with anonymous session config” test to include the required anonymousSession configuration, so the test exercises acceptance of that option while preserving the existing instance and method assertions.src/server/auth-client.ts-3265-3284 (1)
3265-3284: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe catch path diverges from the success path in three ways.
Compare lines 3267-3283 with the success path at lines 3244-3264:
- The response omits
headers: { "content-type": "application/json" }. Next.js then serves the JSON string astext/plain;charset=UTF-8. A client that checks the content type before parsing fails.- The response omits
addCacheControlHeadersForSession(res). The success path marks the logout responseno-store; this path leaves it cacheable.- The caught error is discarded without a log. The success path logs the Auth0 call failure at line 3238. Here an unexpected failure in cookie decryption or cookie deletion is completely silent, which makes the path undiagnosable in production.
Make the two paths consistent.
🐛 Proposed fix
} catch (err) { + console.error("Anonymous logout handler error:", err); // Even on error, attempt to clear the cookie (and its chunks). const res = new NextResponse(JSON.stringify({ ok: true }), { - status: 200 + status: 200, + headers: { "content-type": "application/json" } }); deleteChunkedCookie( this.anonymousCookieName, req.cookies, res.cookies, false, { path: this.anonymousCookieOptions.path, domain: this.anonymousCookieOptions.domain, secure: this.anonymousCookieOptions.secure, sameSite: this.anonymousCookieOptions.sameSite, httpOnly: this.anonymousCookieOptions.httpOnly } ); + addCacheControlHeadersForSession(res); return res; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.ts` around lines 3265 - 3284, Update the catch path in the logout flow to match the success path: create the JSON response with the application/json content type, apply addCacheControlHeadersForSession to mark it no-store, and log the caught error using the same failure-logging approach as the success path before clearing cookies and returning the response.src/server/client.ts-944-964 (1)
944-964: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGuard the
(undefined, res)argument combination.The implementation validates only one direction. If a caller passes
reqwithoutres, it throws a clearTypeError. If a caller passesreswithoutreq,normalizedReqis undefined, control falls into the App Router branch, andresis silently ignored. The cookie is then written to thenext/headersstore instead of the caller's response, orcookies()throws an opaque error outside request scope.
getAccessTokenguards the mirror case at lines 1083-1087. Apply the same guard here.🐛 Proposed guard
let reqCookies: RequestCookies; let resCookies: ResponseCookies; if (normalizedReq) { if (!res) { throw new TypeError( "createAnonymousSession(req, res): The 'res' argument is missing. Both 'req' and 'res' must be provided together for Route Handler or Pages Router usage." ); } reqCookies = normalizedReq instanceof NextRequest ? normalizedReq.cookies : (this.createRequestCookies(normalizedReq) as RequestCookies); resCookies = res.cookies; } else { + if (res !== undefined) { + throw new TypeError( + "createAnonymousSession(req, res): The 'req' argument is missing. Both 'req' and 'res' must be provided together for Route Handler or Pages Router usage." + ); + } // Server Action (App Router): next/headers cookies() is writable here. const cookieStore = await cookies();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/client.ts` around lines 944 - 964, Update createAnonymousSession’s request-context validation around resolveRequestContext to reject the (undefined, res) combination before entering the App Router cookies() branch. Mirror the existing getAccessToken guard so a provided response without a request throws the same clear TypeError, while preserving the current behavior for valid request/response pairs and Server Actions.src/server/auth-client.ts-644-644 (1)
644-644: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
pathignoresNEXT_PUBLIC_BASE_PATH.
anonymousCookieOptions.pathis hard-coded to"/". Every other cookie in the SDK honors the base path.src/server/client.tsline 605-609 resolves the session cookie path asoptions.session?.cookie?.path ?? process.env.AUTH0_COOKIE_PATH ?? basePath ?? "/", and the transaction cookie does the same at line 622.Under a Next.js
basePathdeployment the session cookie is scoped to/appwhile the anonymous cookie is scoped to/. The anonymous cookie is then sent on requests to sibling applications on the same host.Resolve the path the same way the other cookies do.
♻️ Proposed fix
this.anonymousCookieOptions = { httpOnly: true, secure: anonConfig.cookie?.secure ?? true, sameSite: anonConfig.cookie?.sameSite ?? "lax", - path: "/" + path: process.env.NEXT_PUBLIC_BASE_PATH ?? "/" };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.ts` at line 644, Update anonymousCookieOptions.path in the authentication client to resolve the cookie path using the same precedence as the session and transaction cookies: configured anonymous cookie path, AUTH0_COOKIE_PATH, the Next.js basePath, then "/". Reuse the existing base-path resolution symbols and preserve the current anonymous cookie behavior otherwise.
🧹 Nitpick comments (16)
src/client/providers/auth0-provider.test.tsx (1)
38-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThese tests cannot fail; assert the seeded SWR value.
expect(container).toBeTruthy()is true for any rendered container. The "seeds SWR cache" test and all four "Route resolution" tests pass even if you delete thefallbackconstruction inauth0-provider.tsxentirely. The suite therefore does not verify FR-8.Render a consumer that reads the key and assert the seeded value appears without a fetch.
💚 Example of a failing-capable assertion
import useSWR from "swr"; function AnonProbe() { const { data } = useSWR<AnonymousSession | null>("/auth/anonymous-session"); return <span data-testid="anon-id">{data?.id ?? "none"}</span>; } it("FR-8: seeds the SWR cache for the default anonymous-session key", () => { const { getByTestId } = render( <Auth0Provider anonymousSession={mockAnonymousSession}> <AnonProbe /> </Auth0Provider> ); expect(getByTestId("anon-id").textContent).toBe(mockAnonymousSession.id); });Apply the same pattern with a custom
anonymousSessionRouteto prove the key resolution.Also applies to: 120-151
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/providers/auth0-provider.test.tsx` around lines 38 - 47, Update the FR-8 test and all route-resolution tests around Auth0Provider to render an SWR consumer for the configured anonymous-session key, then assert that it displays the seeded mock session value without relying on container truthiness. Cover both the default key and a custom anonymousSessionRoute so the tests fail when fallback construction or key resolution is removed.src/client/hooks/use-anonymous-session.test.ts (2)
109-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for
NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE.The hook resolves the route from options, then the env var, then the default. The suite covers options and the default only. The env-var branch is untested. The provider test file already covers the equivalent branch at its Lines 170-185.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/hooks/use-anonymous-session.test.ts` around lines 109 - 136, Add a test alongside the route-resolution cases in useAnonymousSession.test.ts that sets NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE, renders useAnonymousSession without a route option, and verifies useSWR receives the environment-defined route. Restore or isolate the environment variable after the test so existing default and custom-route tests remain unaffected.
23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore
global.fetchafter each test.Three tests assign
global.fetch = vi.fn()at Lines 170, 195, and 221.vi.clearAllMocks()clears call history but does not restore the original global. The last stub stays installed for the rest of the run. Usevi.stubGlobaland unstub in anafterEach.♻️ Proposed change
-import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";beforeEach(() => { vi.clearAllMocks(); }); + + afterEach(() => { + vi.unstubAllGlobals(); + });Then replace each
global.fetch = vi.fn().mockResolvedValue({...})withvi.stubGlobal("fetch", vi.fn().mockResolvedValue({...})).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/hooks/use-anonymous-session.test.ts` around lines 23 - 25, Update the use-anonymous-session test setup so mocked fetch is restored after each test instead of relying on vi.clearAllMocks(), which only resets call history. Replace the direct global.fetch assignments in the affected tests with vi.stubGlobal("fetch", ...) and add an afterEach in use-anonymous-session.test.ts to unstub the global, keeping the existing mock responses and test behavior unchanged.src/client/providers/auth0-provider.tsx (1)
57-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the anonymous-session route resolution into a shared helper.
Lines 58-62 duplicate the exact resolution order used in
src/client/hooks/use-anonymous-session.tsLines 33-37. The SWR cache key must match between the two, or the seeded fallback is never read and the loading flash returns with no error. Two independent copies of the same precedence chain can drift.Also note the coupling this creates for consumers: if an application passes
anonymousSessionRoutetoAuth0Providerbut omitsrouteinuseAnonymousSession(or the reverse), the keys differ and seeding silently fails. Document this or derive both from one source.♻️ Proposed shared helper
Add to
src/utils/pathUtils.ts(or a client-side route helper):export const resolveAnonymousSessionKey = (route?: string) => normalizeWithBasePath( route || process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE || "/auth/anonymous-session" );Then in this file:
- // Resolve anonymous session route - const anonKey = normalizeWithBasePath( - anonymousSessionRoute || - process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE || - "/auth/anonymous-session" - ); + // Resolve anonymous session route + const anonKey = resolveAnonymousSessionKey(anonymousSessionRoute);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/providers/auth0-provider.tsx` around lines 57 - 73, Extract the anonymous-session route precedence logic into a shared helper such as resolveAnonymousSessionKey, then replace the local anonKey calculation in the Auth0 provider and the equivalent resolution in useAnonymousSession with that helper. Ensure both consumers use identical keys, and document or enforce that explicitly supplied routes must match when configuring Auth0Provider and useAnonymousSession.src/client/hooks/use-anonymous-session.ts (2)
40-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInclude the HTTP status in the thrown error.
The fetcher throws one generic message for every non-OK response. Callers cannot distinguish 401, 429, and 500. Attach the status to the error so consumers can react.
♻️ Proposed change
>(route, (...args) => fetch(...args).then((res) => { if (!res.ok) { - throw new Error("Failed to load anonymous session"); + const err = new Error( + `Failed to load anonymous session (status ${res.status})` + ) as Error & { status?: number }; + err.status = res.status; + throw err; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/hooks/use-anonymous-session.ts` around lines 40 - 56, Update the fetcher inside useAnonymousSession so the non-OK branch includes the response status in the thrown error instead of always using the same message. Keep the existing useSWR and 204/200 handling unchanged, and adjust the error creation in the fetch(...).then((res) => ...) path so callers of useAnonymousSession can distinguish statuses like 401, 429, and 500 from the thrown Error.
44-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse a single-string fetcher signature for string SWR keys.
For a string key, SWR passes that string as the first fetcher argument.
(...args) => fetch(...args)works here, butfetcher: (url: string) => ...is clearer and matches the typed key.
[false_positives_maybe_suggest_optional_refactor]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client/hooks/use-anonymous-session.ts` around lines 44 - 45, Update the fetcher associated with the string SWR key in the anonymous session hook to accept a single typed url string and pass it to fetch, replacing the variadic args signature while preserving the existing response handling.src/server/auth-client.anonymous-routes.test.ts (3)
1108-1110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the static import for
decrypt.
encryptis already imported statically from./cookies.json line 18. The dynamicawait import("../server/cookies.js")resolves to the same module through a longer path, and it repeats at lines 1171-1173. Adddecryptto the static import.♻️ Proposed change
-import { encrypt } from "./cookies.js"; +import { decrypt, encrypt } from "./cookies.js";- const decrypted = await ( - await import("../server/cookies.js") - ).decrypt<AnonymousCookiePayload>(renewedCookieValue, secret); + const decrypted = await decrypt<AnonymousCookiePayload>( + renewedCookieValue, + secret + );Apply the same change at lines 1171-1173.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.anonymous-routes.test.ts` around lines 1108 - 1110, Update the static cookies import in the anonymous-route tests to include decrypt alongside encrypt, then replace both dynamic await import calls around the renewed cookie assertions with the statically imported decrypt function, preserving the existing generic payload and arguments.
22-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the duplicated test scaffolding into a shared helper.
Lines 22-36 (
createMockJWT), lines 44-78 (the MSW server setup), lines 88-110 (the clientbeforeEach), and lines 112-120 (createSessionCookie) are duplicated almost verbatim insrc/server/anonymous-session.flow.test.tslines 22-120. The only differences are the mock subject (anon@uuid-1234againstanon@uuid-9999) and the fallback session-token prefix. Move the helpers and the handler factory into a shared module undersrc/test/. Each suite then keeps only its own overrides.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.anonymous-routes.test.ts` around lines 22 - 120, Extract the duplicated createMockJWT, MSW handler/server setup, AuthClient initialization, and createSessionCookie scaffolding into a shared helper under src/test/. Update both test suites to consume the shared factory, while preserving each suite’s mock subject and fallback session-token prefix through explicit overrides; leave suite-specific configuration in the individual tests.
582-599: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe test name contradicts the test body.
The name states "Auth0 logout 5xx throws error (no 5xx swallow)". The comment on lines 594-595 states that
handleAnonymousLogoutswallows 5xx by design. The test only exercisesanonymousLogoutRequest. Rename the test to describe the network method, for example "anonymousLogoutRequest throws on Auth0 5xx". Consider adding a second case that assertshandleAnonymousLogoutstill returns 200 and clears the cookie on a 5xx, which is the behavior the comment describes but no test covers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.anonymous-routes.test.ts` around lines 582 - 599, Rename the test case around anonymousLogoutRequest to state that the network method throws on an Auth0 5xx response, removing the contradictory “no 5xx swallow” wording. Optionally add a separate test for handleAnonymousLogout that verifies a 5xx response still returns 200 and clears the cookie.src/server/anonymous-session.flow.test.ts (2)
123-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse the created cookie in Step 2 to make the lifecycle continuous.
Step 1 creates a session and asserts that
set-cookieis present. Step 2 then discards that cookie and builds a hand-craftedreadPayloadwith a differentsession_token. The test title claims a "Create → Read → Update → Logout" flow, but the create step and the read step are not linked. Extract theauth0_anonvalue fromcreateRes.cookiesand send it in the read request. The test then covers the real round trip.♻️ Proposed change to link Step 1 and Step 2
// Extract cookies from response (in real flow, client would send these back) const setCookieHeader = createRes.headers.get("set-cookie"); expect(setCookieHeader).toBeTruthy(); // Step 2: Read session (cookie already set in browser) - const now = Math.floor(Date.now() / 1000); - const readPayload: AnonymousCookiePayload = { - session_token: "session-123", - access_token: createMockJWT("anon@uuid-9999"), - expires_at: now + 3600, - session_expires_at: now + 2592000 - }; - const readEncrypted = await createSessionCookie(readPayload, secret); + const readEncrypted = createRes.cookies.get("auth0_anon")!.value;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/anonymous-session.flow.test.ts` around lines 123 - 191, Update the comprehensive lifecycle test so Step 2 reuses the auth0_anon cookie created by createAnonymousSession in createRes.cookies instead of constructing a separate readPayload and encrypted cookie. Send that extracted cookie in the read request, preserving the existing read, update, and logout assertions while linking the flow to the session created in Step 1.
926-936: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the try/catch assertion with
rejects.toMatchObject.If
createAnonymousSessionresolves on the second call, thecatchblock never runs and thee.codeassertion is skipped without failing the test. The direct form always asserts. The sibling test atsrc/server/auth-client.anonymous-routes.test.tslines 991-993 already uses this form.♻️ Proposed change
await expect( (client as any).createAnonymousSession(req.cookies, res.cookies) - ).rejects.toThrow(); - - // Verify the error is an AnonymousSessionError with code invalid_client - try { - await (client as any).createAnonymousSession(req.cookies, res.cookies); - } catch (e: any) { - expect(e.code).toBe("invalid_client"); - } + ).rejects.toMatchObject({ code: "invalid_client" });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/anonymous-session.flow.test.ts` around lines 926 - 936, Replace the try/catch assertion around createAnonymousSession with a single rejects.toMatchObject assertion that verifies code is "invalid_client"; retain the rejection expectation so a resolved promise fails the test.src/types/anonymous-session.test.ts (1)
272-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnotate the config objects with
AnonymousSessionConfig.The suite is named "Config validation", but each object is an untyped literal.
AnonymousSessionConfigis not imported in this file. A field rename or a removed property insrc/types/anonymous-session.tswould not fail these tests. Import the type and annotate each literal, so the compiler checks the shape.♻️ Proposed change
import { isRecoverableAnonymousError, type AnonymousCookiePayload, - type AnonymousSession + type AnonymousSession, + type AnonymousSessionConfig } from "./anonymous-session.js";it("T8.1: AnonymousSessionConfig has enabled flag", () => { - const config = { + const config: AnonymousSessionConfig = { enabled: false }; expect(config.enabled).toBe(false); }); it("T8.3: Cookie name override in config", () => { - const config = { + const config: AnonymousSessionConfig = { enabled: true, cookie: { name: "custom_anon" } }; expect(config.cookie?.name).toBe("custom_anon"); });Apply the same annotation to the tests at lines 288-302.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/types/anonymous-session.test.ts` around lines 272 - 303, Import AnonymousSessionConfig in the test file and annotate each config literal in the “Config validation” cases, including the enabled, cookie name, sameSite, and secure override tests, so TypeScript validates their fields against the configuration type.src/errors/index.ts (1)
78-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReconsider exporting the two mapping helpers publicly.
AnonymousSessionErrorbelongs in the public surface so consumers can branch onerror.code.getStatusForAnonymousErrorandmapAnonymousErrorCodeare SDK-internal helpers used only bysrc/server/auth-client.ts. Exporting them commits the SDK to their signatures under semver.Keep them internal unless consumers need them.
♻️ Proposed narrowing of the public surface
export { - AnonymousSessionError, - getStatusForAnonymousError, - mapAnonymousErrorCode + AnonymousSessionError } from "./anonymous-session-errors.js";
src/server/auth-client.tsalready imports the helpers directly from../errors/anonymous-session-errors.js, so no internal call site breaks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/errors/index.ts` around lines 78 - 82, Update the exports in the errors barrel to expose only AnonymousSessionError; remove getStatusForAnonymousError and mapAnonymousErrorCode from the public re-export while leaving their direct internal imports and implementations unchanged.src/server/auth-client.ts (2)
3132-3141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCall
readAnonymousCookieinstead of repeating the decrypt.Lines 3133-3136 duplicate
readAnonymousCookie(lines 2809-2822) exactly: the samegetChunkedCookiecall, the samedecrypt<AnonymousCookiePayload>call, the same secret.handleAnonymousLogoutrepeats it a third time at lines 3217-3229.Keep one decrypt path for the anonymous cookie so any future change to the read logic applies everywhere.
♻️ Proposed refactor
// Step 2: Read current session to get session_token (DESIGN §5.I4: requires active session) - const current = getChunkedCookie(this.anonymousCookieName, req.cookies); - const decrypted = current - ? await decrypt<AnonymousCookiePayload>(current, this.secret) - : null; + const payload = await this.readAnonymousCookie(req.cookies); - if (!decrypted?.payload?.session_token) { + if (!payload?.session_token) { // No active session → return 400 (cannot present session_token to Auth0) return this.anonymousErrorResponse("invalid_session_token", 400); }Update the two later references to
decrypted.payload.session_tokenat lines 3151 and 3160 topayload.session_token.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.ts` around lines 3132 - 3141, Replace the duplicated cookie retrieval and decryption in the surrounding anonymous-session flow with the existing readAnonymousCookie method, preserving its current error and null handling. Use the returned payload variable for the session_token references instead of decrypted.payload.session_token, and apply the same shared read path in handleAnonymousLogout.
451-451: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the write-only
anonymousSessionConfigfield.The constructor stores
anonConfig, but the code already uses the flattened fields and extracted values (anonymousSessionEnabled,anonymousCookieName,anonymousCookieOptions).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/auth-client.ts` at line 451, Remove the unused private anonymousSessionConfig field from the class and stop assigning the constructor’s anonConfig to it; retain the existing flattened fields anonymousSessionEnabled, anonymousCookieName, and anonymousCookieOptions.src/utils/anonymous-session-constants.ts (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
transferCookiesout of the constants module.This module holds four plain constants. Adding
transferCookiesforces anext/server.jsimport on every consumer of those constants, including client-side code that only needsDEFAULT_ANONYMOUS_SESSION_COOKIE_NAME.Place the helper in a server-only module, for example
src/server/cookies.ts, which already ownssetChunkedCookieanddeleteChunkedCookie.Also applies to: 22-26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/anonymous-session-constants.ts` at line 1, Move transferCookies out of anonymous-session-constants so src/utils/anonymous-session-constants.ts remains plain constant exports and no longer imports NextResponse from next/server.js. Put transferCookies in the server-only cookies module alongside setChunkedCookie and deleteChunkedCookie, and update any callers to import it from that server helper instead of the constants module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/anonymous-sessions.md`:
- Around line 212-216: Update the createAnonymousSession() documentation in the
Return Value section to state that it returns null when anonymous sessions are
disabled, while preserving the documented AnonymousSessionError cases for
authorization failures and authorization-server errors. Align this wording with
the auth-client.ts contract, configuration table, and “Never throws” statement.
- Around line 60-65: Update the client examples in the affected sections to use
NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and
NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, falling back to their documented
default paths when unset, instead of hardcoded routes. Apply this consistently
to every update and logout request example while preserving the existing route
behavior.
In `@src/server/anonymous-session.flow.test.ts`:
- Around line 1109-1112: Update all three startInteractiveLogin calls in
src/server/anonymous-session.flow.test.ts:1109-1112, 1127-1133, and 1184-1187 to
pass req as the second positional argument rather than inside the options
object. In the first site, assert the location header excludes
should-not-inject; in the second, assert it excludes attacker-injected; leave
the third focused on exercising the no-cookie guard path.
- Around line 626-669: Update the SEC-1 T6.1 and T6.2 tests to inspect the
decrypted transaction cookie rather than relying on the redirect location. Reuse
the file’s existing decrypt helper and transaction-cookie symbols to assert
anonymousSessionLinked is true for the injected-session case and absent or false
when no session exists; retain only assertions relevant to each test.
In `@src/server/auth-client.ts`:
- Around line 2643-2663: The read-only branches in resolveAnonymousSession must
not propagate malformed access-token errors: wrap both toPublicSession(state)
calls in src/server/auth-client.ts lines 2643-2663 so undecodable payloads
return null, while leaving the writable renewal and creation paths unchanged.
Make no code change in src/server/client.ts lines 880-919; re-verify its
never-throws documentation remains accurate.
- Around line 3120-3130: Update the metadata validation in the handler before
the JSON serialization and byte-size check to accept only non-null plain
objects, rejecting strings, arrays, numbers, and other non-object values with
the existing anonymous error response. Preserve the current size-limit
validation for valid metadata objects and ensure the value forwarded to
toCookiePayload remains compatible with AnonymousCookiePayload.metadata.
- Around line 2681-2687: Replace the inline renewedPayload construction in the
renew flow with the existing toCookiePayload helper, passing the renewal
response and prior state as required by its contract. Preserve the renew
behavior while allowing rotated session_token and server-merged metadata from
the response, matching the existing update handler usage.
- Around line 640-645: Update persistAnonymousCookie to set
anonymousCookieOptions.maxAge immediately before calling setChunkedCookie, using
Math.max(0, expiration - this.epoch()) so the cookie lifetime matches the
encrypted payload’s session_expires_at. Preserve the existing cookie options and
chunking behavior.
- Around line 780-800: Update the anonymous-route dispatch around
handleGetAnonymousSession, handleUpdateAnonymousSession, and
handleAnonymousLogout so matching paths are routed regardless of
anonymousSessionEnabled. Keep method and pathname checks intact, allowing each
handler’s existing disabled-feature guard to return 404 while preserving normal
behavior when the feature is enabled.
- Around line 903-923: Update the anonymous-session cookie lookup in the
handleLogin flow around anonymousSessionLinked so it can read cookies when req
is unavailable, using the request-less next/headers access supported by the
server-component/action path. Preserve request-based cookie reading when req
exists, and ensure the session token is injected and anonymousSessionLinked is
set for programmatic startInteractiveLogin calls as well.
- Around line 2938-2956: Update the anonymous JSON-POST authentication flows
around the request logic at both auth-client.ts sites 2938-2956 and 2995-3023:
apply the callable ClientAuth returned by getClientAuth() explicitly so client
credentials, assertions, and mTLS authentication are attached instead of
filtering only object entries. Ensure the logout flow propagates a 401
invalid_client response rather than treating it as successful.
---
Minor comments:
In `@docs/anonymous-sessions.md`:
- Line 25: Update the table-of-contents entry for “Ending an Anonymous Session”
to target the document’s existing “Logging Out” heading anchor, `#logging-out`,
keeping the heading unchanged.
- Around line 164-166: Update both catch blocks in the anonymous-session
examples to dispatch using the caught error’s code field instead of instanceof
AnonymousSessionError. Preserve the existing success-false response and
error-code handling, and remove the class-instance check from both locations.
In `@src/client/providers/auth0-provider.test.tsx`:
- Around line 165-167: Update both environment restoration finally blocks in
auth0-provider.test.tsx: for NEXT_PUBLIC_PROFILE_ROUTE at lines 165-167 and
NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE at lines 182-184, delete the corresponding
process.env key when its saved value is undefined; otherwise restore the saved
value normally.
In `@src/server/auth-client.anonymous-routes.test.ts`:
- Around line 365-392: Update the T3.5 test around handleUpdateAnonymousSession
to actually verify unmentioned metadata preservation: override the MSW handler
like T3.3 so the response merges the existing cookie metadata with the request,
then assert that b and c remain present alongside the updated a value. Keep the
“Unmentioned keys preserved” name only if these preservation assertions are
added.
In `@src/server/auth-client.test.ts`:
- Around line 4559-4560: Add a positive callback-flow test near the existing
auth-client transaction-state cases, using the relevant onCallback test setup,
that starts with anonymousSessionLinked set to true and asserts onCallback
receives anonymousSessionLinked: true. Keep the existing false-case coverage
unchanged and verify the flag propagates through the callback payload.
In `@src/server/auth-client.ts`:
- Around line 3113-3118: Update the anonymous request handler to obtain its body
through the shared parseJsonBody(req) helper instead of calling req.json()
directly. Preserve the invalid-request response for parse failures and ensure
parsing occurs through the helper before applying the metadata size check,
matching nearby POST handlers.
- Around line 3265-3284: Update the catch path in the logout flow to match the
success path: create the JSON response with the application/json content type,
apply addCacheControlHeadersForSession to mark it no-store, and log the caught
error using the same failure-logging approach as the success path before
clearing cookies and returning the response.
- Line 644: Update anonymousCookieOptions.path in the authentication client to
resolve the cookie path using the same precedence as the session and transaction
cookies: configured anonymous cookie path, AUTH0_COOKIE_PATH, the Next.js
basePath, then "/". Reuse the existing base-path resolution symbols and preserve
the current anonymous cookie behavior otherwise.
In `@src/server/client.test.ts`:
- Around line 1424-1436: Update the Auth0Client instantiation in the “C2/C3:
Auth0Client can be instantiated with anonymous session config” test to include
the required anonymousSession configuration, so the test exercises acceptance of
that option while preserving the existing instance and method assertions.
In `@src/server/client.ts`:
- Around line 511-516: The anonymousSession documentation incorrectly says all
methods return null when disabled. Update the documentation blocks for
anonymousSession in src/server/client.ts lines 511-516 and
src/server/auth-client.ts lines 385-390 identically: state that
getAnonymousSession returns null, while createAnonymousSession throws
AnonymousSessionError with code unauthorized_client.
- Around line 903-913: Update the reqCookies type annotation in the client
request-cookie initialization to use Prettier’s collapsed union formatting.
Preserve the existing RequestCookies and ReadonlyRequestCookies types and
runtime branching behavior.
- Around line 944-964: Update createAnonymousSession’s request-context
validation around resolveRequestContext to reject the (undefined, res)
combination before entering the App Router cookies() branch. Mirror the existing
getAccessToken guard so a provided response without a request throws the same
clear TypeError, while preserving the current behavior for valid
request/response pairs and Server Actions.
In `@src/types/anonymous-session.test.ts`:
- Around line 162-244: Remove the tautological runtime tests in the “Type
definitions” and “JWT claim extraction verification” blocks, since they do not
invoke SDK behavior; retain only meaningful type coverage or rename tests to
describe type/literal validation without claiming JWT extraction. Do not
duplicate the contract already covered by createAnonymousSession in the existing
route tests, and remove the unused catch binding in the inline decode logic if
that test remains.
---
Nitpick comments:
In `@src/client/hooks/use-anonymous-session.test.ts`:
- Around line 109-136: Add a test alongside the route-resolution cases in
useAnonymousSession.test.ts that sets NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE,
renders useAnonymousSession without a route option, and verifies useSWR receives
the environment-defined route. Restore or isolate the environment variable after
the test so existing default and custom-route tests remain unaffected.
- Around line 23-25: Update the use-anonymous-session test setup so mocked fetch
is restored after each test instead of relying on vi.clearAllMocks(), which only
resets call history. Replace the direct global.fetch assignments in the affected
tests with vi.stubGlobal("fetch", ...) and add an afterEach in
use-anonymous-session.test.ts to unstub the global, keeping the existing mock
responses and test behavior unchanged.
In `@src/client/hooks/use-anonymous-session.ts`:
- Around line 40-56: Update the fetcher inside useAnonymousSession so the non-OK
branch includes the response status in the thrown error instead of always using
the same message. Keep the existing useSWR and 204/200 handling unchanged, and
adjust the error creation in the fetch(...).then((res) => ...) path so callers
of useAnonymousSession can distinguish statuses like 401, 429, and 500 from the
thrown Error.
- Around line 44-45: Update the fetcher associated with the string SWR key in
the anonymous session hook to accept a single typed url string and pass it to
fetch, replacing the variadic args signature while preserving the existing
response handling.
In `@src/client/providers/auth0-provider.test.tsx`:
- Around line 38-47: Update the FR-8 test and all route-resolution tests around
Auth0Provider to render an SWR consumer for the configured anonymous-session
key, then assert that it displays the seeded mock session value without relying
on container truthiness. Cover both the default key and a custom
anonymousSessionRoute so the tests fail when fallback construction or key
resolution is removed.
In `@src/client/providers/auth0-provider.tsx`:
- Around line 57-73: Extract the anonymous-session route precedence logic into a
shared helper such as resolveAnonymousSessionKey, then replace the local anonKey
calculation in the Auth0 provider and the equivalent resolution in
useAnonymousSession with that helper. Ensure both consumers use identical keys,
and document or enforce that explicitly supplied routes must match when
configuring Auth0Provider and useAnonymousSession.
In `@src/errors/index.ts`:
- Around line 78-82: Update the exports in the errors barrel to expose only
AnonymousSessionError; remove getStatusForAnonymousError and
mapAnonymousErrorCode from the public re-export while leaving their direct
internal imports and implementations unchanged.
In `@src/server/anonymous-session.flow.test.ts`:
- Around line 123-191: Update the comprehensive lifecycle test so Step 2 reuses
the auth0_anon cookie created by createAnonymousSession in createRes.cookies
instead of constructing a separate readPayload and encrypted cookie. Send that
extracted cookie in the read request, preserving the existing read, update, and
logout assertions while linking the flow to the session created in Step 1.
- Around line 926-936: Replace the try/catch assertion around
createAnonymousSession with a single rejects.toMatchObject assertion that
verifies code is "invalid_client"; retain the rejection expectation so a
resolved promise fails the test.
In `@src/server/auth-client.anonymous-routes.test.ts`:
- Around line 1108-1110: Update the static cookies import in the anonymous-route
tests to include decrypt alongside encrypt, then replace both dynamic await
import calls around the renewed cookie assertions with the statically imported
decrypt function, preserving the existing generic payload and arguments.
- Around line 22-120: Extract the duplicated createMockJWT, MSW handler/server
setup, AuthClient initialization, and createSessionCookie scaffolding into a
shared helper under src/test/. Update both test suites to consume the shared
factory, while preserving each suite’s mock subject and fallback session-token
prefix through explicit overrides; leave suite-specific configuration in the
individual tests.
- Around line 582-599: Rename the test case around anonymousLogoutRequest to
state that the network method throws on an Auth0 5xx response, removing the
contradictory “no 5xx swallow” wording. Optionally add a separate test for
handleAnonymousLogout that verifies a 5xx response still returns 200 and clears
the cookie.
In `@src/server/auth-client.ts`:
- Around line 3132-3141: Replace the duplicated cookie retrieval and decryption
in the surrounding anonymous-session flow with the existing readAnonymousCookie
method, preserving its current error and null handling. Use the returned payload
variable for the session_token references instead of
decrypted.payload.session_token, and apply the same shared read path in
handleAnonymousLogout.
- Line 451: Remove the unused private anonymousSessionConfig field from the
class and stop assigning the constructor’s anonConfig to it; retain the existing
flattened fields anonymousSessionEnabled, anonymousCookieName, and
anonymousCookieOptions.
In `@src/types/anonymous-session.test.ts`:
- Around line 272-303: Import AnonymousSessionConfig in the test file and
annotate each config literal in the “Config validation” cases, including the
enabled, cookie name, sameSite, and secure override tests, so TypeScript
validates their fields against the configuration type.
In `@src/utils/anonymous-session-constants.ts`:
- Line 1: Move transferCookies out of anonymous-session-constants so
src/utils/anonymous-session-constants.ts remains plain constant exports and no
longer imports NextResponse from next/server.js. Put transferCookies in the
server-only cookies module alongside setChunkedCookie and deleteChunkedCookie,
and update any callers to import it from that server helper instead of the
constants module.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 09342321-2d59-4bc3-861c-cc60877bda65
📒 Files selected for processing (21)
README.mddocs/anonymous-sessions.mdsrc/client/hooks/use-anonymous-session.test.tssrc/client/hooks/use-anonymous-session.tssrc/client/index.tssrc/client/providers/auth0-provider.test.tsxsrc/client/providers/auth0-provider.tsxsrc/errors/anonymous-session-errors.tssrc/errors/index.tssrc/server/anonymous-session.flow.test.tssrc/server/auth-client.anonymous-routes.test.tssrc/server/auth-client.test.tssrc/server/auth-client.tssrc/server/client.test.tssrc/server/client.tssrc/server/transaction-store.tssrc/test/defaults.tssrc/types/anonymous-session.test.tssrc/types/anonymous-session.tssrc/types/index.tssrc/utils/anonymous-session-constants.ts
| Three environment variables allow you to customize the feature routes: | ||
|
|
||
| ```env | ||
| NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE=/auth/anonymous-session | ||
| NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE=/auth/anonymous-session/update | ||
| NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE=/auth/anonymous-session/logout |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Use the configured update and logout routes.
The documentation defines NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, but the client examples use literal default paths. Custom route values will make these requests target the wrong endpoints. Read the public environment variables with the documented defaults, or state that callers must replace these paths.
Also applies to: 310-313, 387-389, 666-672
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/anonymous-sessions.md` around lines 60 - 65, Update the client examples
in the affected sections to use NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and
NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, falling back to their documented
default paths when unset, instead of hardcoded routes. Apply this consistently
to every update and logout request example while preserving the existing route
behavior.
| **Return Value**: Returns `AnonymousSession`. Throws `AnonymousSessionError` if: | ||
|
|
||
| - The feature is disabled | ||
| - The client is not authorized for anonymous sessions | ||
| - The authorization server encounters an error |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Align the disabled-feature contract.
The supplied src/server/auth-client.ts contract states that disabled anonymous-session methods return null. This section says createAnonymousSession() throws when the feature is disabled. That also conflicts with the configuration table and the “Never throws” statement at Line 141. Document the actual behavior consistently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/anonymous-sessions.md` around lines 212 - 216, Update the
createAnonymousSession() documentation in the Return Value section to state that
it returns null when anonymous sessions are disabled, while preserving the
documented AnonymousSessionError cases for authorization failures and
authorization-server errors. Align this wording with the auth-client.ts
contract, configuration table, and “Never throws” statement.
| it("SEC-1 T6.1: Transaction state binding records anonymousSessionLinked flag", async () => { | ||
| // Layer 3 of SEC-1: transaction state binding. | ||
| // Verify that when a session is injected, the flag is set in transaction state. | ||
| // This prevents swapped-cookie attacks at callback time. | ||
|
|
||
| const now = Math.floor(Date.now() / 1000); | ||
| const anonPayload: AnonymousCookiePayload = { | ||
| session_token: "session-bound", | ||
| access_token: createMockJWT("anon@uuid-9999"), | ||
| expires_at: now + 3600, | ||
| session_expires_at: now + 2592000 | ||
| }; | ||
| const encrypted = await createSessionCookie(anonPayload, secret); | ||
|
|
||
| const req = new NextRequest(new URL("http://localhost:3000/auth/login"), { | ||
| headers: { cookie: `auth0_anon=${encrypted}` } | ||
| }); | ||
|
|
||
| const result = await (client as any).startInteractiveLogin( | ||
| { returnTo: "/" }, | ||
| req | ||
| ); | ||
|
|
||
| // After startInteractiveLogin, the transaction state should have anonymousSessionLinked=true | ||
| // This is verified at callback time to prevent cookie-swap attacks | ||
| const location = result.headers.get("location"); | ||
| expect(location).toContain("session_token=session-bound"); | ||
| }); | ||
|
|
||
| it("SEC-1 T6.2: No session at login → anonymousSessionLinked flag false", async () => { | ||
| // When no anon session exists, flag must be false so callback knows | ||
| // not to apply migration logic. | ||
|
|
||
| const req = new NextRequest(new URL("http://localhost:3000/auth/login")); | ||
|
|
||
| const result = await (client as any).startInteractiveLogin( | ||
| { returnTo: "/" }, | ||
| req | ||
| ); | ||
|
|
||
| // No session_token in URL since no cookie | ||
| const location = result.headers.get("location"); | ||
| expect(location).not.toContain("session_token="); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Assert anonymousSessionLinked in the transaction state, not the location header.
Test T6.1 is named "Transaction state binding records anonymousSessionLinked flag", and T6.2 is named "No session at login → anonymousSessionLinked flag false". Neither test reads the transaction state. T6.1 asserts only that the location header contains session_token=session-bound, which duplicates the assertions in the tests at lines 536-558 and 597-624. The Layer 3 defense therefore has no coverage.
Decrypt the transaction cookie from the response and assert the flag. The tests in this file already decrypt cookies with decrypt from ./cookies.js, so the helper is available.
💚 Proposed assertion for T6.1
- // After startInteractiveLogin, the transaction state should have anonymousSessionLinked=true
- // This is verified at callback time to prevent cookie-swap attacks
- const location = result.headers.get("location");
- expect(location).toContain("session_token=session-bound");
+ // The transaction state must record the binding flag; this is checked at
+ // callback time to prevent cookie-swap attacks.
+ const txnCookie = result.cookies
+ .getAll()
+ .find((c: any) => c.name.startsWith("__txn_"));
+ expect(txnCookie).toBeDefined();
+ const txn = await decrypt<TransactionState>(txnCookie!.value, secret);
+ expect(txn?.payload.anonymousSessionLinked).toBe(true);Import the extra symbols at the top of the file:
-import { encrypt } from "./cookies.js";
+import { decrypt, encrypt } from "./cookies.js";
+import type { TransactionState } from "./transaction-store.js";Apply the mirrored assertion in T6.2, expecting the flag to be absent or false.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/anonymous-session.flow.test.ts` around lines 626 - 669, Update the
SEC-1 T6.1 and T6.2 tests to inspect the decrypted transaction cookie rather
than relying on the redirect location. Reuse the file’s existing decrypt helper
and transaction-cookie symbols to assert anonymousSessionLinked is true for the
injected-session case and absent or false when no session exists; retain only
assertions relevant to each test.
| const result = await (disabledClient as any).startInteractiveLogin({ | ||
| req, | ||
| returnTo: "/" | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Three startInteractiveLogin calls pass req inside the options object instead of as the second argument. The signature is startInteractiveLogin(options, req?). When req is placed in the options object, the second parameter is undefined, the guard this.anonymousSessionEnabled && req is false, and the anonymous-cookie injection never runs. All three tests then pass without exercising the behavior their names describe. The calls at lines 550-553, 581-587, 616-619, and 644-647 use the correct positional form.
src/server/anonymous-session.flow.test.ts#L1109-L1112: movereqto the second argument, then assert that the location header does not containshould-not-inject, so the test proves the disabled feature suppresses injection.src/server/anonymous-session.flow.test.ts#L1127-L1133: movereqto the second argument, then assert that the location header does not containattacker-injected, so the test proves Layer 1 stripping.src/server/anonymous-session.flow.test.ts#L1184-L1187: movereqto the second argument, so the no-cookie path runs through the same guard the other tests use.
📍 Affects 1 file
src/server/anonymous-session.flow.test.ts#L1109-L1112(this comment)src/server/anonymous-session.flow.test.ts#L1127-L1133src/server/anonymous-session.flow.test.ts#L1184-L1187
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/anonymous-session.flow.test.ts` around lines 1109 - 1112, Update
all three startInteractiveLogin calls in
src/server/anonymous-session.flow.test.ts:1109-1112, 1127-1133, and 1184-1187 to
pass req as the second positional argument rather than inside the options
object. In the first site, assert the location header excludes
should-not-inject; in the second, assert it excludes attacker-injected; leave
the third focused on exercising the no-cookie guard path.
| this.anonymousCookieOptions = { | ||
| httpOnly: true, | ||
| secure: anonConfig.cookie?.secure ?? true, | ||
| sameSite: anonConfig.cookie?.sameSite ?? "lax", | ||
| path: "/" | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Confirm the anonymous cookie options never receive a maxAge.
set -euo pipefail
# Locate every read and write of the anonymous cookie options.
rg -nP -C 6 'anonymousCookieOptions' --type=ts
# Confirm setChunkedCookie forwards maxAge from its options argument.
ast-grep run --pattern $'export function setChunkedCookie($$$) { $$$ }' --lang typescript src/server/cookies.tsRepository: auth0/nextjs-auth0
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files containing anonymousCookieOptions or setChunkedCookie:\n'
rg -n 'anonymousCookieOptions|setChunkedCookie|anonymousSessionConfig|anonymousCookieName' --glob '*.{ts,tsx}' || true
printf '\nFile outline for auth-client.ts max matches:\n'
wc -l src/server/auth-client.ts
ast-grep outline src/server/auth-client.ts --view compact || true
printf '\nRelevant auth-client.ts sections:\n'
sed -n '600,655p' src/server/auth-client.ts
sed -n '2760,2800p' src/server/auth-client.ts
sed -n '3200,3290p' src/server/auth-client.ts
printf '\nCookies.ts outline and relevant section:\n'
wc -l src/server/cookies.ts
ast-grep outline src/server/cookies.ts --view compact || true
sed -n '1,260p' src/server/cookies.tsRepository: auth0/nextjs-auth0
Length of output: 217
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import pathlib, re
for path in pathlib.Path('src/server').glob('*.ts'):
text = path.read_text(errors='replace')
if 'anonymousCookieOptions' in text or 'setChunkedCookie' in text:
print(f'\n--- {path} ---')
for i, line in enumerate(text.splitlines(), 1):
if 'anonymousCookieOptions' in line or 'setChunkedCookie' in line:
start=max(1,i-8); end=min(len(text.splitlines()), i+12)
lines=text.splitlines()
print(f'@@ {start}-{end}')
for j in range(start,end+1):
print(f'{j:4}: {lines[j-1]}')
PY
printf '\nSearch for maxAge mentions in src/server:\n'
rg -n 'maxAge|max-age|max_age|cookie_options|CookieOptions' src/server --glob '*.{ts,tsx}' || trueRepository: auth0/nextjs-auth0
Length of output: 50374
Set maxAge on the anonymous cookie to match its payload lifetime.
anonymousCookieOptions has no maxAge, and setChunkedCookie forwards cookie options unchanged, so the encrypted cookie is browser-session scoped. The encrypted payload sets JWE.exp to session_expires_at, so the anonymous session is lost when the browser closes. Add maxAge: Math.max(0, expiration - this.epoch()) from persistAnonymousCookie before calling setChunkedCookie.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 640 - 645, Update
persistAnonymousCookie to set anonymousCookieOptions.maxAge immediately before
calling setChunkedCookie, using Math.max(0, expiration - this.epoch()) so the
cookie lifetime matches the encrypted payload’s session_expires_at. Preserve the
existing cookie options and chunking behavior.
| // SEC-1 three-layer fixation mitigation: inject anonymous session token if present | ||
| // Layer 1 (reserved-param stripping) has already happened via mergeAuthorizationParamsIntoSearchParams() | ||
| // Layer 2 (own-cookie sourcing) and Layer 3 (transaction state binding) below | ||
| let anonymousSessionLinked = false; | ||
| if (this.anonymousSessionEnabled && req) { | ||
| try { | ||
| const anonCookie = await this.readAnonymousCookie(req.cookies); | ||
| if (anonCookie?.session_token) { | ||
| // Session token exists in own cookie → safe to inject (Layer 2) | ||
| authorizationParams.set( | ||
| RESERVED_SESSION_TOKEN_PARAM, | ||
| anonCookie.session_token | ||
| ); | ||
| anonymousSessionLinked = true; // Flag for transaction state binding (Layer 3) | ||
| } | ||
| } catch (err) { | ||
| // Log error but don't fail login over anon cookie issue | ||
| console.error("Error reading anonymous session cookie:", err); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find every caller of AuthClient.startInteractiveLogin and check whether a request is passed.
set -euo pipefail
rg -nP -C 5 '\bstartInteractiveLogin\s*\(' --type=ts -g '!**/*.test.ts'Repository: auth0/nextjs-auth0
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files around auth-client/client =="
fd -a 'auth-client\.ts$|client\.ts$' . | sed 's#^\./##' | head -50
echo "== git status =="
git status --short | head -50
echo "== grep startInteractiveLogin across tracked files =="
git ls-files | xargs rg -n 'startInteractiveLogin\s*\(' || true
echo "== grep handleLogin and startInteractiveLogin in server/client files =="
rg -n 'function startInteractiveLogin|startInteractiveLogin\(|handleLogin\s*\(' --glob '*.ts' --glob '*.tsx' . | head -100Repository: auth0/nextjs-auth0
Length of output: 11990
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/server/auth-client.ts startInteractiveLogin/handleLogin section =="
sed -n '820,1040p' src/server/auth-client.ts | nl -ba -v820
echo "== src/server/client.ts public client startInteractiveLogin =="
sed -n '1560,1605p' src/server/client.ts | nl -ba -v1560
echo "== src/server/auth-client.ts anonymousSessionLinked references =="
rg -n 'anonymousSessionLinked|RESERVED_SESSION_TOKEN_PARAM' src/server/auth-client.ts -C 3
echo "== src/server/client.ts exports =="
sed -n '1,120p' src/server/client.ts | nl -ba -v1Repository: auth0/nextjs-auth0
Length of output: 270
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/server/auth-client.ts startInteractiveLogin/handleLogin section =="
sed -n '820,1040p' src/server/auth-client.ts
echo "== src/server/client.ts public client startInteractiveLogin =="
sed -n '1560,1605p' src/server/client.ts
echo "== src/server/auth-client.ts anonymousSessionLinked references =="
rg -n 'anonymousSessionLinked|RESERVED_SESSION_TOKEN_PARAM' src/server/auth-client.ts -C 3
echo "== src/server/client.ts relevant exports =="
sed -n '1,120p' src/server/client.tsRepository: auth0/nextjs-auth0
Length of output: 17432
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== AnonymousSessionConfig / startsInteractiveLogin docs =="
rg -n 'anonymousSession|startInteractiveLogin|onCallback' --glob '*.md' --glob '*.ts' --glob '*.tsx' | head -300
echo "== anonymous flow tests around startInteractiveLogin =="
sed -n '520,685p' src/server/anonymous-session.flow.test.tsRepository: auth0/nextjs-auth0
Length of output: 280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== AnonymousSessionConfig / startInteractiveLogin docs =="
rg -n 'anonymousSession|startInteractiveLogin|onCallback' --glob '*.md' --glob '*.ts' --glob '*.tsx' . | head -300 || true
echo "== anonymous flow tests around startInteractiveLogin =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/server/anonymous-session.flow.test.ts')
text=p.read_text()
for start in range(0, len(text), 100_000):
idx=text.find('client.startInteractiveLogin', start)
while idx != -1:
line=text[:idx].count('\n')+1
if line in (550,581,616,644):
print(f"-- context line {line} --")
lines=text.splitlines()
for i in range(max(1,line-25), min(len(lines),line+45)+1):
print(f"{i:5}: {lines[i-1]}")
idx=text.find('client.startInteractiveLogin', idx+1)
PYRepository: auth0/nextjs-auth0
Length of output: 21141
Include the request when reading anonymous-session cookies.
handleLogin() passes the NextRequest, so /auth/login links anonymous sessions. Auth0Client.startInteractiveLogin() calls authClient.startInteractiveLogin(options) from a Server Component/Action path with no req, so anonymous-session linking stays false and the migration hook will not fire. Use the available next/headers request-less cookie access here, or document that programmatic startInteractiveLogin() does not link anonymous sessions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 903 - 923, Update the
anonymous-session cookie lookup in the handleLogin flow around
anonymousSessionLinked so it can read cookies when req is unavailable, using the
request-less next/headers access supported by the server-component/action path.
Preserve request-based cookie reading when req exists, and ensure the session
token is injected and anonymousSessionLinked is set for programmatic
startInteractiveLogin calls as well.
| if (state.expires_at > now) { | ||
| // Access token still valid → return session, no renewal needed (T1.3) | ||
| return this.toPublicSession(state); | ||
| } | ||
|
|
||
| // Access token is expired; check if we can renew | ||
| if (resCookies && state.session_expires_at > now) { | ||
| // Session token valid, can write cookie → renew access token (T1.4) | ||
| return await this.renewAccessToken(state, reqCookies, resCookies); | ||
| } | ||
|
|
||
| // Session token is also expired; can we write cookies? | ||
| if (resCookies) { | ||
| // Session expired + can write → create fresh session silently (T1.5, T3.6, FR-12) | ||
| return await this.createAndPersist(reqCookies, resCookies); | ||
| } | ||
|
|
||
| // Can't write cookie (Server Component read-only context) → defer renewal (D7, T1.6) | ||
| // Return the decrypted session as-is; renewal will happen on next route handler call | ||
| return this.toPublicSession(state); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
One throwing call breaks the documented never-throws read contract. toPublicSession throws AnonymousSessionError("invalid_session_token") when the access token inside a successfully decrypted cookie cannot be decoded or its sub lacks the anon@ prefix. resolveAnonymousSession calls it on both read-path branches, so the throw propagates all the way to the public reader, which documents that it never throws for a malformed cookie and is intended for Server Components.
src/server/auth-client.ts#L2643-L2663: wrap the twotoPublicSession(state)calls on the read path so an undecodable payload returnsnull, matching howdecryptalready handles a malformed cookie. Leave the writable paths throwing.src/server/client.ts#L880-L919: no code change is needed once the root cause is fixed; re-verify that the doc claim at lines 882-883 then holds.
📍 Affects 2 files
src/server/auth-client.ts#L2643-L2663(this comment)src/server/client.ts#L880-L919
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 2643 - 2663, The read-only branches
in resolveAnonymousSession must not propagate malformed access-token errors:
wrap both toPublicSession(state) calls in src/server/auth-client.ts lines
2643-2663 so undecodable payloads return null, while leaving the writable
renewal and creation paths unchanged. Make no code change in
src/server/client.ts lines 880-919; re-verify its never-throws documentation
remains accurate.
| const renewedPayload: AnonymousCookiePayload = { | ||
| session_token: state.session_token, // Keep original session token | ||
| access_token: res.access_token, // New access token | ||
| expires_at: this.epoch() + res.expires_in, | ||
| session_expires_at: this.epoch() + res.session_expires_in, | ||
| metadata: state.metadata // Preserve metadata | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reuse toCookiePayload instead of rebuilding the payload inline.
This block duplicates toCookiePayload (lines 2874-2893) with two behavioral differences:
- Line 2682 always keeps
state.session_token.toCookiePayloadusesres.session_token ?? priorSessionToken. If Auth0 ever rotates the session token on a renew response, this path discards the new handle and keeps the stale one. Every later renewal then fails. - Line 2686 always keeps
state.metadata.toCookiePayloadprefersres.metadata, the server-merged value. Metadata updated server-side is dropped on renew.
The update handler at line 3158 already routes through toCookiePayload. Route the renew path through it too, so one function owns the response-to-payload contract.
♻️ Proposed refactor
- const renewedPayload: AnonymousCookiePayload = {
- session_token: state.session_token, // Keep original session token
- access_token: res.access_token, // New access token
- expires_at: this.epoch() + res.expires_in,
- session_expires_at: this.epoch() + res.session_expires_in,
- metadata: state.metadata // Preserve metadata
- };
+ // toCookiePayload owns the response-to-payload contract: it carries the
+ // prior session handle forward when the server omits it, and prefers
+ // server-merged metadata over the locally cached copy.
+ const renewedPayload = this.toCookiePayload(
+ res,
+ state.session_token,
+ state.metadata
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const renewedPayload: AnonymousCookiePayload = { | |
| session_token: state.session_token, // Keep original session token | |
| access_token: res.access_token, // New access token | |
| expires_at: this.epoch() + res.expires_in, | |
| session_expires_at: this.epoch() + res.session_expires_in, | |
| metadata: state.metadata // Preserve metadata | |
| }; | |
| // toCookiePayload owns the response-to-payload contract: it carries the | |
| // prior session handle forward when the server omits it, and prefers | |
| // server-merged metadata over the locally cached copy. | |
| const renewedPayload = this.toCookiePayload( | |
| res, | |
| state.session_token, | |
| state.metadata | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 2681 - 2687, Replace the inline
renewedPayload construction in the renew flow with the existing toCookiePayload
helper, passing the renewal response and prior state as required by its
contract. Preserve the renew behavior while allowing rotated session_token and
server-merged metadata from the response, matching the existing update handler
usage.
| if (clientAuth && typeof clientAuth === "object") { | ||
| for (const [key, value] of Object.entries( | ||
| clientAuth as Record<string, unknown> | ||
| )) { | ||
| if (typeof value === "string") { | ||
| headersObj[key] = value; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const res = await this.fetch(requestUrl, { | ||
| method: "POST", | ||
| headers: headersObj, | ||
| body: JSON.stringify({ | ||
| client_id: this.clientMetadata.client_id, | ||
| ...body | ||
| }), | ||
| signal: httpOpts.signal | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
oauth4webapi 3.x ClientAuth is a function ClientSecretPost returns callable
💡 Result:
In the oauth4webapi library, ClientSecretPost is a function that returns a ClientAuth object [1][2]. ClientAuth is a type alias for a function (a callable) that performs client authentication during an OAuth request [2]. Its signature is: (as: AuthorizationServer, client: Client, body: URLSearchParams, headers: Headers) => void | Promise [2] When you call ClientSecretPost(clientSecret), it returns this callable function, which, when invoked by the library during an OAuth flow (such as authorizationCodeGrantRequest), injects the client_id and client_secret into the HTTP request body as application/x-www-form-urlencoded parameters [1][2].
Citations:
- 1: https://github.com/panva/oauth4webapi/blob/main/docs/functions/ClientSecretPost.md
- 2: https://cdn.jsdelivr.net/npm/oauth4webapi@3.6.1/build/index.d.ts
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package files =="
fd -a 'package.json|pnpm-lock.yaml|package-lock.json|yarn.lock|bun.lockb|bun.lock' . | sed 's#^\./##' | head -50
echo
echo "== relevant files =="
fd -a 'auth-client.ts' . | sed 's#^\./##'
echo
echo "== dependency versions =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lock; do
if [ -f "$f" ]; then
echo "--- $f ---"
if [ "$f" = "package.json" ]; then
node -e "const p=require('./$f'); console.log(JSON.stringify({dependencies:p.dependencies, devDependencies:p.devDependencies, peerDependencies:p.peerDependencies}, null, 2))" | rg -n "oauth4webapi|nextjs-auth0|typescript|node" || true
else
rg -n "oauth4webapi|`@auth0/nextjs-auth0`" "$f" | head -100 || true
fi
fi
done
echo
echo "== auth-client outline around anon =="
file="$(fd 'auth-client.ts' . | head -1)"
if [ -n "${file:-}" ]; then
ast-grep outline "$file" --match anonymous | head -200 || true
echo "--- lines 2850-3040 ---"
sed -n '2850,3040p' "$file" | nl -ba -v2850
fiRepository: auth0/nextjs-auth0
Length of output: 1630
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lines 2850-3045 of src/server/auth-client.ts =="
sed -n '2850,3045p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+2849, $0}'
echo
echo "== getCientAuth definition/usages =="
rg -n "getClientAuth|anonymous/token|anonymous/logout|clientAuth|ClientAuth|ClientSecretPost|assertion" src/server/auth-client.ts
echo
echo "== inspect type import source if local copy exists =="
fd -a 'node_modules/oauth4webapi|oauth4webapi' . | sed 's#^\./##' | head -20 || trueRepository: auth0/nextjs-auth0
Length of output: 9663
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lines 3480-3535: getClientAuth implementation =="
sed -n '3480,3535p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+3479, $0}'
echo
echo "== nearby getServerClient usage with getClientAuth =="
sed -n '1280,1355p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+1279, $0}'
echo
echo "== auth-client imports =="
sed -n '1,120p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR, $0}'
echo
echo "== package lock for oauth4webapi type/export metadata, if present =="
sed -n '1980,2010p' pnpm-lock.yamlRepository: auth0/nextjs-auth0
Length of output: 11255
🌐 Web query:
oauth4webapi 3.8.6 source ClientAuth callable ClientSecretPost PrivateKeyJwt TLSClientAuth
💡 Result:
In the oauth4webapi library (version 3.8.6), ClientAuth is a type alias for a callable function used to implement client authentication methods when interacting with an Authorization Server [1][2][3]. The ClientAuth function signature is defined as [2][3]: (as: AuthorizationServer, client: Client, body: URLSearchParams, headers: Headers) => void | Promise This callable is invoked by authentication request functions (such as authorizationCodeGrantRequest or clientCredentialsGrantRequest) to dynamically modify the outgoing request's body or headers to include the necessary authentication credentials [1][2][3]. The library provides several built-in functions that return a ClientAuth instance [4]: ClientSecretPost(clientSecret: string): ClientAuth This method sends the client_id and client_secret as application/x-www-form-urlencoded body parameters [5][2]. PrivateKeyJwt(clientPrivateKey: CryptoKey | PrivateKey, options?: ModifyAssertionOptions): ClientAuth This method sends the client_id, client_assertion_type, and a signed client_assertion (JWT) as application/x-www-form-urlencoded body parameters, utilizing digital signatures for authentication [6][2]. TlsClientAuth: ClientAuth This method sends only the client_id as a body parameter and relies on mutual TLS (mTLS) for authentication, which must be configured externally through a custom fetch implementation (typically using an agent with a key and certificate) [7][8][3]. These implementations provide a flexible mechanism to inject authentication material into OAuth 2.0 requests while maintaining adherence to various RFC-specified authentication methods [2][3].
Citations:
- 1: https://jsr.io/@panva/oauth4webapi/doc
- 2: https://github.com/panva/oauth4webapi/blob/main/src/index.ts
- 3: https://cdn.jsdelivr.net/npm/oauth4webapi@3.6.1/build/index.d.ts
- 4: https://github.com/panva/oauth4webapi/blob/main/docs/README.md
- 5: https://github.com/panva/oauth4webapi/blob/main/docs/functions/ClientSecretPost.md
- 6: https://github.com/panva/oauth4webapi/blob/main/docs/functions/PrivateKeyJwt.md
- 7: https://github.com/panva/oauth4webapi/blob/main/docs/functions/TlsClientAuth.md
- 8: https://github.com/panva/oauth4webapi/blob/main/docs/interfaces/Client.md
Fix the anonymous Auth0 client authentication path. getClientAuth() returns oauth4webapi’s callable ClientAuth, but typeof clientAuth === "object" skips callable credentials, so /anonymous/token and /anonymous/logout do not include client_id/client_secret, client assertion, or mTLS auth. Attach credentials explicitly on these JSON-POST endpoints, then surface 401 invalid_client for logout instead of treating it as success.
📍 Affects 1 file
src/server/auth-client.ts#L2938-L2956(this comment)src/server/auth-client.ts#L2995-L3023
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 2938 - 2956, Update the anonymous
JSON-POST authentication flows around the request logic at both auth-client.ts
sites 2938-2956 and 2995-3023: apply the callable ClientAuth returned by
getClientAuth() explicitly so client credentials, assertions, and mTLS
authentication are attached instead of filtering only object entries. Ensure the
logout flow propagates a 401 invalid_client response rather than treating it as
successful.
| const { metadata } = body; | ||
|
|
||
| // Step 1: Validate metadata size | ||
| // (Before network call, per FR-15; client-side optimization) | ||
| // FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so | ||
| // multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe. | ||
| const metadataStr = JSON.stringify(metadata ?? {}); | ||
| const metadataBytes = new TextEncoder().encode(metadataStr).length; | ||
| if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) { | ||
| return this.anonymousErrorResponse("metadata_too_large", 400); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate that metadata is a plain object.
The handler checks only the serialized byte size. A client can POST {"metadata": "text"}, {"metadata": [1,2]}, or {"metadata": 42}. Each passes the 1KB check and is forwarded to Auth0 unchanged.
The value then flows through toCookiePayload into AnonymousCookiePayload.metadata and out through toPublicSession into AnonymousSession.metadata. Both are declared Record<string, unknown>. A string, array, or number breaks that contract for every consumer, including the client hook, with no type error to catch it.
Reject non-object values before the size check.
🐛 Proposed validation
const { metadata } = body;
+ // metadata must be a plain object: AnonymousCookiePayload.metadata and
+ // AnonymousSession.metadata are both declared Record<string, unknown>.
+ if (
+ metadata !== undefined &&
+ (typeof metadata !== "object" ||
+ metadata === null ||
+ Array.isArray(metadata))
+ ) {
+ return this.anonymousErrorResponse("invalid_request", 400);
+ }
+
// Step 1: Validate metadata size📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { metadata } = body; | |
| // Step 1: Validate metadata size | |
| // (Before network call, per FR-15; client-side optimization) | |
| // FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so | |
| // multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe. | |
| const metadataStr = JSON.stringify(metadata ?? {}); | |
| const metadataBytes = new TextEncoder().encode(metadataStr).length; | |
| if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) { | |
| return this.anonymousErrorResponse("metadata_too_large", 400); | |
| } | |
| const { metadata } = body; | |
| // metadata must be a plain object: AnonymousCookiePayload.metadata and | |
| // AnonymousSession.metadata are both declared Record<string, unknown>. | |
| if ( | |
| metadata !== undefined && | |
| (typeof metadata !== "object" || | |
| metadata === null || | |
| Array.isArray(metadata)) | |
| ) { | |
| return this.anonymousErrorResponse("invalid_request", 400); | |
| } | |
| // Step 1: Validate metadata size | |
| // (Before network call, per FR-15; client-side optimization) | |
| // FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so | |
| // multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe. | |
| const metadataStr = JSON.stringify(metadata ?? {}); | |
| const metadataBytes = new TextEncoder().encode(metadataStr).length; | |
| if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) { | |
| return this.anonymousErrorResponse("metadata_too_large", 400); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/auth-client.ts` around lines 3120 - 3130, Update the metadata
validation in the handler before the JSON serialization and byte-size check to
accept only non-null plain objects, rejecting strings, arrays, numbers, and
other non-object values with the existing anonymous error response. Preserve the
current size-limit validation for valid metadata objects and ensure the value
forwarded to toCookiePayload remains compatible with
AnonymousCookiePayload.metadata.
Description
Adds anonymous sessions to
@auth0/nextjs-auth0: pre-login sessions backed by Auth0-issued access tokens. Applications can attach identity and metadata to a visitor before authentication and link that anonymous session at login. The surface is entirely additive and gated behind ananonymousSession.enabledflag — existing applications are unaffected.What's included
Server API (
Auth0Client)getAnonymousSession()— read the current anonymous session. App Router zero-arg form and Pages Routerreqform. Returnsnullwhen there is no session, the feature is disabled, or the cookie is malformed/expired (never throws for those).createAnonymousSession()— create and persist a fresh session. Zero-arg Server Action form andreq/resRoute Handler form.anonymousSessionconfig block ({ enabled, cookie: { name, sameSite, secure } }) which mounts three routes:GET /auth/anonymous-session,POST /auth/anonymous-session/update,POST /auth/anonymous-session/logout, withNEXT_PUBLIC_*route overrides.Client API
useAnonymousSession()hook returning{ anonymous, isLoading, error, invalidate }.Auth0ProvideranonymousSession/anonymousSessionRouteprops for SSR cache seeding (no loading flash).Behavior
AnonymousSessionErrorwith a code→HTTP-status mapping (401invalid_client; 403feature_not_enabled/unauthorized_client; 500server_error; 400 otherwise).session_expired/invalid_session_tokenare silently recovered.Security
session_tokenis a reserved authorize parameter (caller-supplied values are stripped), the injected token is sourced only from the SDK's own encrypted cookie, and it is bound to the CSRF-protected transaction state viaanonymousSessionLinked.auth0_anoncookie isHttpOnly,Secureby default, JWE-encrypted, withno-storecache headers. Chunked-cookie fragments are cleared on logout.Testing
tscandeslintclean.Documentation
docs/anonymous-sessions.md(enabling, server/client usage, metadata, error handling, security, limitations) and a README link.Compatibility
Additive and minor-versionable. With
anonymousSessionunset, no routes are mounted, the methods short-circuit, and no new network calls occur.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests