Skip to content

fix(auth): validate JWT server-side via getUser() in hooks.server.ts [P1.01]#117

Closed
cesarnml wants to merge 4 commits into
mainfrom
agents/p1-01-getsession-getuser
Closed

fix(auth): validate JWT server-side via getUser() in hooks.server.ts [P1.01]#117
cesarnml wants to merge 4 commits into
mainfrom
agents/p1-01-getsession-getuser

Conversation

@cesarnml
Copy link
Copy Markdown
Owner

@cesarnml cesarnml commented May 2, 2026

Summary

  • delivery ticket: `P1.01 getSession → getUser`
  • ticket file: docs/02-delivery/phase-01/ticket-01-get-user.md
  • stacked base branch: `main`
  • self-audit: outcome `clean` completed at 2026-05-02 05:31 UTC
  • codexPreflight: outcome `clean` completed at 2026-05-02 05:33 UTC

Review Triage

Cursor Bugbot + CodeRabbit (converging finding) — Patched ✅

Finding: `getSession()` is called 4× per request (handle → getProfile → layout.server.ts → getProjects), each invoking `getUser()` → Supabase Auth round-trip.
Fix: Memoize `getUser()` result in the `handle` closure via `cachedSession`. Subsequent calls within the same request return immediately.

Greptile — No actionable findings

No inline threads opened. Summary comment is noise.

Qodo — Same fanout finding + CI lockfile

CI lockfile issue resolved by rebasing onto main (lockfile updated in `d92746a`).

CI Baseline

`svelte-check` reports 66 pre-existing type errors across 41 files (unrelated to this PR). Test suite: 60/60 passing.

Vercel Build Failure

Pre-existing: `IP_INFO_TOKEN` missing from Vercel project env vars. Not introduced by this PR.


Note

Medium Risk
Changes request-time authentication validation by adding a getUser() round-trip and memoizing the resulting session, which could affect auth behavior/performance and error handling for logged-in users.

Overview
Auth/session validation is tightened in hooks.server.ts. event.locals.getSession() now first calls supabase.auth.getUser() to verify the JWT with Supabase; on error or missing user it returns null, otherwise it fetches and returns the full Session via getSession().

Performance/behavior change: the computed session is memoized per request (cachedSession) so repeated getSession()/getProfile()/getProjects() calls don’t trigger multiple Supabase auth round-trips. Documentation is updated to capture the security rationale and the compatibility-driven approach.

Reviewed by Cursor Bugbot for commit 0a1118f. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
coding-stats Ready Ready Preview, Comment May 2, 2026 7:11am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 679133d9-780b-441b-af5b-a81429420038

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

Walkthrough

The pull request updates server-side session validation to add an auth.getUser() precondition check before retrieving the session. In src/hooks.server.ts, the event.locals.getSession function now calls getUser() first, returning null immediately if there is an authentication error or no user exists, before proceeding to call getSession(). The corresponding documentation in docs/02-delivery/phase-01/ticket-01-get-user.md records the rationale for this approach: it maintains backward compatibility by keeping the Locals API name unchanged while modifying internals to prioritize direct user validation, and documents alternative implementation strategies.


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 and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add server-side JWT validation in getSession hook

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add server-side JWT validation via getUser() call
• Verify token against Supabase auth server before accepting
• Maintain backward compatibility with existing getSession() API
• Document security rationale and implementation decisions
Diagram
flowchart LR
  A["Client Request"] -->|JWT Token| B["getSession Hook"]
  B -->|Call getUser| C["Validate vs Auth Server"]
  C -->|Error or No User| D["Return null"]
  C -->|Valid User| E["Call getSession"]
  E -->|Return Session| F["Client Response"]
Loading

Grey Divider

File Changes

1. src/hooks.server.ts 🐞 Bug fix +7/-0

Add server-side JWT validation in getSession

• Add getUser() call to validate JWT server-side before accepting
• Check for errors or missing user and return null if validation fails
• Maintain existing getSession() call to preserve Session object with tokens
• Preserve backward compatibility with all existing callers

src/hooks.server.ts


2. docs/02-delivery/phase-01/ticket-01-get-user.md 📝 Documentation +6/-5

Document JWT validation security fix rationale

• Document security gap: getSession() trusts client JWT without verification
• Explain solution: getUser() validates token against Supabase auth server
• Justify implementation path: kept public API name for backward compatibility
• Record alternative approaches and deferred decisions for future reference

docs/02-delivery/phase-01/ticket-01-get-user.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 2, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. Uncached getUser call fanout 🐞 Bug ➹ Performance
Description
event.locals.getSession() now performs a network getUser() call every time it’s invoked, but the
app currently invokes getSession() multiple times per request (including concurrently via
Promise.all), multiplying auth round-trips and cookie writes. This adds avoidable latency and
increases the chance of inconsistent per-request auth state if multiple calls race.
Code

src/hooks.server.ts[R40-49]

+    const {
+      data: { user },
+      error,
+    } = await event.locals.supabase.auth.getUser()
+
+    if (error || !user) return null
+
    const {
      data: { session },
    } = await event.locals.supabase.auth.getSession()
Evidence
The new getSession() implementation calls supabase.auth.getUser() before getSession(). That
function is called repeatedly: hooks.server.ts calls getProfile() in the global handle (which
calls getSession()), and +layout.server.ts calls getSession(), getProfile(), and
getProjects() concurrently; both getProfile() and getProjects() call getSession() internally
when not provided a session, resulting in multiple getUser() validations per request.

src/hooks.server.ts[39-52]
src/hooks.server.ts[54-78]
src/hooks.server.ts[80-81]
src/routes/+layout.server.ts[3-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`event.locals.getSession()` performs a network `supabase.auth.getUser()` call on every invocation. Current request flows invoke `getSession()` multiple times (and concurrently), causing redundant auth validations and cookie updates.

### Issue Context
This became materially more expensive after adding `getUser()` (remote validation). The goal is to preserve the security improvement while ensuring each request only validates once.

### Fix Focus Areas
- src/hooks.server.ts[39-52]
- src/hooks.server.ts[54-78]
- src/routes/+layout.server.ts[3-16]

### Implementation sketch
- Add per-request memoization in `hooks.server.ts`, e.g. keep a `let sessionPromise: Promise<Session | null> | undefined` inside `handle` and have `getSession()` return the same promise for the duration of the request.
- Optionally short-circuit before calling `getUser()` when there is clearly no auth cookie (reduces network calls for anonymous requests).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Auth errors masked as null 🐞 Bug ☼ Reliability
Description
getSession() returns null for any getUser() error, conflating “no authenticated user” with
transient auth/network failures. Downstream logic then treats the user as logged out (including
redirects), which can hide real outages and produce misleading behavior.
Code

src/hooks.server.ts[R40-46]

+    const {
+      data: { user },
+      error,
+    } = await event.locals.supabase.auth.getUser()
+
+    if (error || !user) return null
+
Evidence
The code explicitly returns null when getUser() returns an error. getProfile() returns
null when the session is null, and the hook redirects to /login for /account when profile
is falsy, meaning any auth validation error will be interpreted as an unauthenticated user and can
trigger redirects.

src/hooks.server.ts[39-46]
src/hooks.server.ts[54-66]
src/hooks.server.ts[91-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`event.locals.getSession()` currently returns `null` both when there is no user and when `getUser()` fails with an error, which can cause downstream logic to redirect to login during transient auth outages.

### Issue Context
Failing closed is fine for security, but treating all errors as “logged out” removes observability and can mislead users. Prefer distinguishing unauthenticated (expected) vs validation failure (unexpected) and handling them differently.

### Fix Focus Areas
- src/hooks.server.ts[39-46]
- src/hooks.server.ts[91-93]

### Implementation sketch
- If `error` indicates a normal unauthenticated state, return `null`.
- Otherwise (unexpected errors), log with enough context (status/message) and either throw a 5xx (so the client sees an outage) or return a distinct error signal that callers can handle (depending on your preferred UX).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5fbc0bb. Configure here.

Comment thread src/hooks.server.ts
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 2, 2026

Not up to standards ⛔

🔴 Issues 4 minor

Alerts:
⚠ 4 issues (≤ 0 issues of at least minor severity)

Results:
4 new issues

Category Results
CodeStyle 4 minor

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/hooks.server.ts (1)

39-52: ⚡ Quick win

Memoize getUser() to avoid redundant auth server round-trips per request.

getUser() contacts the Supabase Auth server on every call. Without memoization, a single SvelteKit request triggers multiple getUser() network calls: once via getProfile() at line 80 (which calls getSession() with no pre-fetched session arg), and again from any route load/action handler that calls getSession() itself (e.g., account/+page.server.ts). Since handle creates a new closure scope per request, a simple let cache is safe:

⚡ Proposed memoization
  event.locals.getSession = async () => {
+   let cachedSession: Session | null | undefined
+   return async () => {
+     if (cachedSession !== undefined) return cachedSession
+
      const {
        data: { user },
        error,
      } = await event.locals.supabase.auth.getUser()

-     if (error || !user) return null
+     if (error || !user) {
+       cachedSession = null
+       return null
+     }

      const {
        data: { session },
      } = await event.locals.supabase.auth.getSession()

+     cachedSession = session
      return session
+   }
  }

Alternatively, hoist the cache to the handle closure (same per-request lifetime, no function nesting):

+  let sessionCache: Session | null | undefined
   event.locals.getSession = async () => {
+    if (sessionCache !== undefined) return sessionCache
     const {
       data: { user },
       error,
     } = await event.locals.supabase.auth.getUser()
-    if (error || !user) return null
+    if (error || !user) { sessionCache = null; return null }
     const {
       data: { session },
     } = await event.locals.supabase.auth.getSession()
+    sessionCache = session
     return session
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks.server.ts` around lines 39 - 52, The getSession implementation
calls event.locals.supabase.auth.getUser() on every invocation causing duplicate
network round-trips; introduce a per-request cache (e.g., let cachedUser /
cachedSession inside the handle closure) and update event.locals.getSession to
first return the cached value if present, otherwise call supabase.auth.getUser()
once, store the result in the cache, then (if needed) call getSession() or
return the cached session; reference event.locals.getSession, getUser(),
getSession(), and getProfile() so callers reuse the cached user/session for the
lifetime of the request.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/hooks.server.ts`:
- Around line 39-52: The getSession implementation calls
event.locals.supabase.auth.getUser() on every invocation causing duplicate
network round-trips; introduce a per-request cache (e.g., let cachedUser /
cachedSession inside the handle closure) and update event.locals.getSession to
first return the cached value if present, otherwise call supabase.auth.getUser()
once, store the result in the cache, then (if needed) call getSession() or
return the cached session; reference event.locals.getSession, getUser(),
getSession(), and getProfile() so callers reuse the cached user/session for the
lifetime of the request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c01f304-d640-4ff6-973a-afd76c093e8d

📥 Commits

Reviewing files that changed from the base of the PR and between fe54b54 and 5fbc0bb.

📒 Files selected for processing (2)
  • docs/02-delivery/phase-01/ticket-01-get-user.md
  • src/hooks.server.ts

cesarnml and others added 3 commits May 2, 2026 13:46
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Caches the getSession() result in a closure variable so repeated calls
within the same request (getProfile, getProjects, layout load) reuse
the validated JWT rather than making 4+ round-trips to Supabase Auth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The wait-for-vercel-preview step exhausts retries because preview
deployments are password-protected. Switched trigger to workflow_dispatch
so the job no longer blocks PR CI. Fix path documented in roadmap:
pass VERCEL_AUTOMATION_BYPASS_SECRET or make previews public.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cesarnml
Copy link
Copy Markdown
Owner Author

cesarnml commented May 2, 2026

Squash-merged to main via closeout-stack (P1.01).

@cesarnml cesarnml closed this May 2, 2026
@cesarnml cesarnml deleted the agents/p1-01-getsession-getuser branch May 2, 2026 10:14
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