Skip to content

feat: opt routes out of the SSR user write via routeRules#55

Draft
frederikprijck wants to merge 10 commits into
issue-52from
feat/disable-ssr-middleware
Draft

feat: opt routes out of the SSR user write via routeRules#55
frederikprijck wants to merge 10 commits into
issue-52from
feat/disable-ssr-middleware

Conversation

@frederikprijck

@frederikprijck frederikprijck commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Adds a per-route (and global) opt-out for the SSR user write, addressing point 3 of #52 ("config to disable / route-scope the global middleware").

This lets a consumer opt a route (or pattern, or everything) out of the SSR user write:

// nuxt.config.ts
routeRules: {
  '/public/**': { auth0: { ssrUser: false } }, // per route / pattern
  // '/**': { auth0: { ssrUser: false } },      // global
}

On a route with auth0: { ssrUser: false }, the middleware skips the user write even when the route is not shared-cacheable; the user is hydrated client-side from the profile endpoint instead (same path the cache guard already uses).

Why this design

  • Middleware stays global: true, so it always runs and always sees the route rules; only the write is gated. (Notably, Nuxt's native appMiddleware: { auth0: false } route rule does not work here — appMiddleware can only remove named middleware, not a global one. An e2e test was written that demonstrated this, which is why the opt-out is implemented as an explicit rule the middleware reads.)
  • No fragile codegen. NitroRouteConfig is a closed interface, so a custom route-rule key needs a type augmentation. We ship an internal .d.ts so this repo's sources/fixtures typecheck, and document a copy-paste augmentation snippet for consumers who type-check their nuxt.config — rather than generating types at build time.

Tests

  • Middleware unit tests: skips the write on ssrUser: false, writes on ssrUser: true.
  • e2e (caching.test.ts): a non-cacheable /opted-out route with auth0: { ssrUser: false } renders anonymous SSR HTML (no sub/email in the payload), proving the opt-out works independently of the cache guard.
  • Full unit suite (66) + lint + build green.

Merge order

Based on issue-52 because it builds on that branch's client-hydration plugin and profile endpoint.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f527e56f-51ae-4ba8-bcca-e77dd02b143a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/disable-ssr-middleware

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The cache guard only detects caching expressed through Nitro route rules.
When a shared cache / CDN is configured outside the app (an edge rule keyed
on path with its own TTL, no 'public'/'s-maxage' header), the guard cannot
see it and the user would be written into that route's SSR payload.

Add an explicit per-route opt-out: the SSR middleware reads an
'auth0: { ssrUser: false }' Nitro route rule (via the getRouteRules accessor
it already uses) and skips the user write. Set it on '/**' to disable the
SSR user write globally, or on specific paths for per-route control. The
middleware stays global, so it always runs and sees the route rules; only
the write is gated. The user is hydrated client-side as on cacheable routes.

- middleware: skip write when auth0.ssrUser === false (composes with the guard)
- types: internal d.ts augmenting NitroRouteConfig so repo sources/fixtures typecheck
- tests: middleware unit tests (skip on false, write on true) + e2e proving a
  non-cacheable opted-out route renders anonymous SSR HTML
- docs: README + EXAMPLES, incl. a copy-paste augmentation snippet for consumers
The auth0.ssrUser route rule already supports the inverse model: disable the
SSR user write globally with '/**': { auth0: { ssrUser: false } } and opt back
in on specific routes with { auth0: { ssrUser: true } }. This works because
Nitro merges overlapping route rules by specificity, so the more specific
rule wins — no code change needed.

Add an e2e fixture + test guarding that merge behaviour (opted-in route
server-renders the user; a non-opted route stays anonymous), and document the
pattern in README and EXAMPLES.
…CVE-2026-53721)

The middleware-level fix for the route-rule case-sensitivity bypass lives on
the issue-52 base (it re-checks the lowercased path). This commit extends the
coverage to the auth0.ssrUser opt-out that this branch adds:

- unit: an opt-out rule reachable only via the lowercased path still skips the write
- e2e: /Opted-Out renders anonymous SSR HTML (the opt-out is not bypassed by casing)
The cache guard is best-effort: it only sees caching expressed through Nitro
route rules. It cannot detect a Cache-Control header set imperatively
(setHeader in a server route/middleware) or a shared cache / CDN configured
outside the app, so those routes still write the user into cacheable SSR HTML.

Document this limitation explicitly and recommend the fail-safe posture for
apps behind a shared cache / CDN: disable the SSR user write globally with
'/**': { auth0: { ssrUser: false } } and opt in per route where the response
is never shared-cached. No route serves the user in cacheable HTML unless
explicitly allowed.
The 'caching' and 'optin' fixtures both exercised the same feature — controlling
the SSR user write — but lived apart because they needed opposite global
defaults. Merge them into one 'ssr-user' fixture: the on-by-default routes
(/private, /cacheable, /opted-out) sit alongside an '/optin/**' subtree that is
off-by-default with '/optin/dashboard' opted back in. Using a subtree instead of
a literal '/**' lets both directions coexist in one routeRules block.

The two test files become one 'ssr-user.test.ts' with two describe blocks
('on by default' and 'off by default with per-route opt-in'). No coverage lost:
all cache-guard, opt-out, CVE case-variant, and opt-in cases are retained.
ssrUser: true overrides a broader ssrUser: false but cannot force the
user into shared-cacheable HTML — the cache guard still skips the write
and the client hydrates. Note this explicitly in README and EXAMPLES so
the 'more specific rule wins' phrasing isn't read as an absolute override.
The $fetch e2e proves a shared-cacheable route's raw SSR HTML is anonymous
(the leak surface). This adds the complementary browser test: despite that
anonymous SSR HTML, the auth.client plugin fetches /auth/profile after
hydration and the account UI ends up reflecting the logged-in user — the
client-hydration loop, which only a real browser can exercise.

Seeds the encrypted stateless session cookie on the browser context before
navigating, so /auth/profile resolves getUser() offline (no Auth0 call).
Rename the fixture user (pii-victim -> user) and rewrite the test
descriptions/comments to describe the feature — controlling the SSR user
write and hydrating client-side — rather than narrating a payload leak and
its fix. The CVE-2026-53721 references for the case-sensitivity re-check are
kept, since they explain why the lowercased lookup exists. No coverage
change: 9 unit + 9 e2e still pass, lint clean.
)

Adds a module-level `auth0: { ssrUser: false }` option that disables the SSR
user write for every route with no route rule required. Unlike the automatic
cache guard (which only detects route-rule-based caching) this is the
cache-mechanism-agnostic escape hatch: it also covers caching the guard cannot
see ahead of render (imperative headers, CDN-side config).

Precedence: a per-route `auth0.ssrUser` rule overrides the module option, which
overrides the default (true). `ssrUser: true` never overrides the cache guard,
so a shared-cacheable route always stays anonymous. Enables "disable globally,
opt specific routes back in."
@frederikprijck frederikprijck force-pushed the feat/disable-ssr-middleware branch from f3ad73b to ed0acca Compare July 14, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant