Skip to content

feat: use httpOnly cookies instead of localStorage for user sessions - #269

Open
Diyaaa-12 wants to merge 8 commits into
ali-ahnaf:developfrom
Diyaaa-12:feat/httponly-cookie-auth
Open

feat: use httpOnly cookies instead of localStorage for user sessions#269
Diyaaa-12 wants to merge 8 commits into
ali-ahnaf:developfrom
Diyaaa-12:feat/httponly-cookie-auth

Conversation

@Diyaaa-12

@Diyaaa-12 Diyaaa-12 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Closes #40

Moved session token storage from localStorage to an httpOnly cookie:

API side:

  • cookie-parser wired up in index.ts
  • sign-in / sign-up now set an httpOnly, sameSite=lax cookie (secure in production) instead of relying solely on the JSON body
  • sign-out clears the cookie
  • authenticate middleware now reads the token from the cookie as a fallback when no Bearer header is present (backward compatible)

UI side:

  • ApiClient now uses withCredentials: true; removed manual Authorization: Bearer header and all localStorage reads/writes of the auth token
  • useAuth.setSession() no longer takes a token param — only profile data, since the browser handles the cookie automatically
  • signin/page.tsx and signup/page.tsx needed no changes — they were already calling setSession without the token

Pre-existing issues found while testing (unrelated to this PR, flagging for visibility — not fixed here to keep the diff scoped):

  1. npm run dev / npm run build / npm run test for packages/api currently fail to compile because of a type error in debts.service.ts (missing dueDate field vs the DebtDto type). This actually blocks the dev server from starting at all, so I couldn't do a full manual sign-in sanity check locally. Happy to open a separate PR for this fix if that's useful — let me know, or feel free to take it yourself if you'd rather.

  2. Small typo in signup/page.tsx: ) asany; should be ) as any; (missing space).

Testing done:

  • packages/api test suite: 77/77 relevant tests pass (only debts.service.test.ts fails, due to the pre-existing compile error above — not something this PR touches)
  • packages/ui unit tests: no new failures introduced; same 8 pre-existing failures as before this change (localStorage.clear is not a function in AppBar/AuthGuard/DesktopSidebar tests)

Summary by CodeRabbit

  • New Features
    • Sign-in and sign-up now establish sessions using authentication cookies.
    • API requests automatically include credentials for cookie-based sessions.
  • Bug Fixes
    • Authentication can proceed via cookies when headers are missing, without breaking requests on invalid tokens.
    • Unauthorized users are redirected to the sign-in page.
  • Improvements
    • Session persistence now stores only the user profile in localStorage (no auth token).
    • Sign-out clears the stored profile and signs out by removing the session cookie.
    • Protected-route access is now based on the stored profile.

@github-actions

Copy link
Copy Markdown
Contributor

Greetings, @Diyaaa-12. Thy scroll hath arrived unblemished — the runes align and no conflict bars the path. The council shall now convene over its contents, weighing each incantation by candlelight. Tarry a while, brave adventurer; we shall investigate and return to thee with our verdict.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Authentication moves session tokens from UI local storage to an HttpOnly cookie. API routes set and clear the cookie, middleware reads it as a fallback, credentialed UI requests stop sending Bearer headers, and profile-only session state is retained.

Changes

Cookie Session Authentication

Layer / File(s) Summary
API cookie issuance and route integration
packages/api/src/routes/auth/cookie-options.ts, packages/api/src/routes/auth/sign-*.route.ts
Sign-in and sign-up set the 30-day auth_token cookie, while sign-out clears it and returns through the shared response helper.
Cookie-based API authentication
packages/api/src/middleware/auth.ts
Authentication checks the Bearer header, falls back to the auth cookie, and ignores missing or invalid tokens without rejecting the request.
Credentialed API requests and session invalidation
packages/ui/src/lib/api/ApiClient.ts
Axios sends credentials, removes stored-token Bearer injection, and clears the profile before redirecting on unauthorized responses.
Profile-only UI session state
packages/ui/src/hooks/useAuth.ts, packages/ui/src/app/signin/page.tsx, packages/ui/src/app/signup/page.tsx, packages/ui/src/components/AppBar.test.tsx
Session setup and logout retain only profile data, with sign-in, sign-up, and logout tests updated to the new contract.
Profile-based route guards and persistence validation
packages/ui/src/components/AuthGuard.tsx, packages/ui/src/components/AuthGuard.test.tsx, packages/ui/e2e/*.spec.ts
Route access and authentication tests now use the stored profile instead of a locally stored token.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UI
  participant API
  participant Browser
  UI->>API: Submit sign-in or sign-up
  API->>Browser: Set auth_token cookie
  UI->>API: Send credentialed API request
  Browser->>API: Attach auth_token cookie
  API->>UI: Return authenticated response
  UI->>API: Submit sign-out
  API->>Browser: Clear auth_token cookie
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Most requirements are met, but the diff does not show the required API CORS credentials configuration for cookie-based requests. Add or verify API CORS credentials support (credentials: true) so the httpOnly cookie is accepted and sent with authenticated requests.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: moving sessions from localStorage to httpOnly cookies.
Out of Scope Changes check ✅ Passed The changes stay focused on session-cookie migration, auth flow updates, and related UI tests without obvious unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/api/src/middleware/auth.ts (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

AUTH_COOKIE_NAME is duplicated across two files. Both cookie-options.ts and auth.ts independently define const AUTH_COOKIE_NAME = 'auth_token'. If one is updated and the other isn't, the cookie set on login won't match the cookie read during authentication, silently breaking the auth flow. Export from cookie-options.ts and import in auth.ts.

  • packages/api/src/routes/auth/cookie-options.ts#L3-3: change const to export const for AUTH_COOKIE_NAME.
  • packages/api/src/middleware/auth.ts#L18-18: remove the local declaration and import AUTH_COOKIE_NAME from ../routes/auth/cookie-options.
🤖 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 `@packages/api/src/middleware/auth.ts` at line 1, Use a single shared
AUTH_COOKIE_NAME definition: export AUTH_COOKIE_NAME from cookie-options.ts,
then remove the local declaration in auth.ts and import the exported symbol from
../routes/auth/cookie-options.
🧹 Nitpick comments (1)
packages/ui/src/lib/api/ApiClient.ts (1)

16-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove unnecessary dynamic import('next/navigation').

The import resolves but its exports are never used — all three branches (.then, .catch, outer try/catch) simply set window.location.href = SIGN_IN_PATH. The dynamic import adds an async hop before the redirect for no benefit.

♻️ Proposed fix
 function handleUnauthorized(): void {
   if (typeof window === 'undefined') return;
   window.localStorage.removeItem(PROFILE_STORAGE_KEY);
   const current = window.location.pathname.replace(/\/$/, '');
   if (current !== SIGN_IN_PATH) {
-    try {
-      import('next/navigation')
-        .then(() => {
-          window.location.href = SIGN_IN_PATH;
-        })
-        .catch(() => {
-          window.location.href = SIGN_IN_PATH;
-        });
-    } catch {
-      window.location.href = SIGN_IN_PATH;
-    }
+    window.location.href = SIGN_IN_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 `@packages/ui/src/lib/api/ApiClient.ts` around lines 16 - 27, Remove the
unnecessary dynamic import and its surrounding promise and try/catch handling in
the redirect logic. Update the affected method in ApiClient so it directly
assigns window.location.href to SIGN_IN_PATH, preserving the existing redirect
behavior without the asynchronous hop.
🤖 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 `@packages/api/src/middleware/auth.ts`:
- Line 18: Remove the local AUTH_COOKIE_NAME declaration from auth.ts, export
the existing AUTH_COOKIE_NAME from cookie-options.ts, and import that shared
symbol in auth.ts so login cookie creation and authentication reads use one
source of truth.

In `@packages/api/src/routes/auth/sign-out.route.ts`:
- Around line 6-9: Update the sign-out route handler to replace the direct
res.json call with the established utilService success response method,
returning that call after clearAuthCookie(res). Use the appropriate success
response without a { message } payload, preserving the existing sign-out
behavior.

---

Outside diff comments:
In `@packages/api/src/middleware/auth.ts`:
- Line 1: Use a single shared AUTH_COOKIE_NAME definition: export
AUTH_COOKIE_NAME from cookie-options.ts, then remove the local declaration in
auth.ts and import the exported symbol from ../routes/auth/cookie-options.

---

Nitpick comments:
In `@packages/ui/src/lib/api/ApiClient.ts`:
- Around line 16-27: Remove the unnecessary dynamic import and its surrounding
promise and try/catch handling in the redirect logic. Update the affected method
in ApiClient so it directly assigns window.location.href to SIGN_IN_PATH,
preserving the existing redirect behavior without the asynchronous hop.
🪄 Autofix (Beta)

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: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 5543be2b-b095-499e-a581-c3cbce54313f

📥 Commits

Reviewing files that changed from the base of the PR and between 3e49189 and aca13b2.

📒 Files selected for processing (10)
  • packages/api/src/index.ts
  • packages/api/src/middleware/auth.ts
  • packages/api/src/routes/auth/cookie-options.ts
  • packages/api/src/routes/auth/sign-in.route.ts
  • packages/api/src/routes/auth/sign-out.route.ts
  • packages/api/src/routes/auth/sign-up.route.ts
  • packages/ui/src/app/signin/page.tsx
  • packages/ui/src/app/signup/page.tsx
  • packages/ui/src/hooks/useAuth.ts
  • packages/ui/src/lib/api/ApiClient.ts

Comment thread packages/api/src/middleware/auth.ts Outdated
Comment thread packages/api/src/routes/auth/sign-out.route.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
packages/ui/src/components/AppBar.test.tsx-37-37 (1)

37-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Seed a valid profile for the logout test.

useAuth() clears persisted profiles without an id during initialization, so this fixture may already be removed before the logout click. Add an id to ensure the assertion actually verifies logout cleanup.

Proposed fix
-  localStorage.setItem('pocket_pixel_profile', JSON.stringify({ avatar: '/avatar.png' }));
+  localStorage.setItem(
+    'pocket_pixel_profile',
+    JSON.stringify({ id: 'test-user', avatar: '/avatar.png' }),
+  );
🤖 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 `@packages/ui/src/components/AppBar.test.tsx` at line 37, Update the profile
fixture in the logout test to include a valid id alongside the avatar before
storing it in localStorage. Keep the existing logout interaction and cleanup
assertions unchanged so the test verifies removal of a profile that useAuth()
preserves during initialization.
🤖 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.

Other comments:
In `@packages/ui/src/components/AppBar.test.tsx`:
- Line 37: Update the profile fixture in the logout test to include a valid id
alongside the avatar before storing it in localStorage. Keep the existing logout
interaction and cleanup assertions unchanged so the test verifies removal of a
profile that useAuth() preserves during initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 8d4d2771-c856-44dd-9ebd-6740d6383e75

📥 Commits

Reviewing files that changed from the base of the PR and between aca13b2 and eb6bfd7.

📒 Files selected for processing (1)
  • packages/ui/src/components/AppBar.test.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@packages/ui/src/components/AuthGuard.tsx`:
- Around line 18-27: Validate the persisted profile in AuthGuard before setting
hasProfile: parse the stored value, require a truthy parsed.id, and treat parse
failures or missing IDs as unauthenticated so the existing cleanup path runs. In
packages/ui/src/components/AuthGuard.test.tsx lines 41, 64, and 87, add an id to
authenticated fixtures; at lines 52-58, retain the malformed fixture and rename
the test to cover invalid persisted profiles.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 611bd65c-0efd-4c24-8567-14e330163a74

📥 Commits

Reviewing files that changed from the base of the PR and between 05ba92e and a380d41.

📒 Files selected for processing (4)
  • packages/ui/e2e/login.spec.ts
  • packages/ui/e2e/signup.spec.ts
  • packages/ui/src/components/AuthGuard.test.tsx
  • packages/ui/src/components/AuthGuard.tsx

Comment thread packages/ui/src/components/AuthGuard.tsx Outdated
…okie-auth

# Conflicts:
#	packages/api/src/middleware/auth.ts
…cookie auth

- Resolved conflict in packages/api/src/middleware/auth.ts: TokenPayload
  now imports from @expense-tracker/shared (per upstream), kept the
  httpOnly cookie fallback in authenticate()
- Fixed packages/ui/src/app/auth/google/callback/page.tsx to match the
  new single-argument setSession(profile) signature (this page appears
  orphaned since the Google sign-in button/route were removed upstream
  -- flagging for a maintainer decision on whether to delete it)
@Diyaaa-12

Copy link
Copy Markdown
Contributor Author

Hi @ali-ahnaf ! Rebased on latest develop to resolve the merge conflict in packages/api/src/middleware/auth.ts (trivial — just the TokenPayload import path moving to @expense-tracker/shared).

While fixing this I noticed packages/ui/src/app/auth/google/callback/page.tsx appears to be dead code — the old Google sign-in route (packages/api/src/routes/auth/google-sign-in.route.ts) and the GoogleSignInButton component were both removed in develop, but this callback page was left behind and nothing links to it anymore. It was calling setSession(token, profile) with the old two-argument signature, so it failed to build against this PR's cookie-based setSession(profile). I've updated it to the new signature to unblock the build, but wanted to flag it in case you'd rather just delete the page since the feature it supported seems to be gone. Happy to open that cleanup separately if useful.

@ali-ahnaf

Copy link
Copy Markdown
Owner

Hi @ali-ahnaf ! Rebased on latest develop to resolve the merge conflict in packages/api/src/middleware/auth.ts (trivial — just the TokenPayload import path moving to @expense-tracker/shared).

While fixing this I noticed packages/ui/src/app/auth/google/callback/page.tsx appears to be dead code — the old Google sign-in route (packages/api/src/routes/auth/google-sign-in.route.ts) and the GoogleSignInButton component were both removed in develop, but this callback page was left behind and nothing links to it anymore. It was calling setSession(token, profile) with the old two-argument signature, so it failed to build against this PR's cookie-based setSession(profile). I've updated it to the new signature to unblock the build, but wanted to flag it in case you'd rather just delete the page since the feature it supported seems to be gone. Happy to open that cleanup separately if useful.

Hi, yes that leftover file is something I would want gone. Appreciate it if you could clean it up, or create an issue at least.

The old Google sign-in route and button were already removed upstream
in develop; this callback page and its AuthGuard public-path entry
were leftover dead code with no remaining references. Removing both
per maintainer request on PR review.
@Diyaaa-12

Copy link
Copy Markdown
Contributor Author

Done, removed the orphaned auth/google/callback/page.tsx and the corresponding AuthGuard public-path entry. Build and tests pass clean. Ready for review whenever you get a chance!

@github-actions

Copy link
Copy Markdown
Contributor

Hark, @Diyaaa-12! Thy branch hath drifted from the main path and the runes no longer align — a merge conflict bars the way. Thou must rebase the base branch and mend the tangled incantations before this quest may be sealed. The gates of merge remain shut until the conflict is banished.

@Diyaaa-12

Copy link
Copy Markdown
Contributor Author

Hi! hope u can still review this

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.

Use httpOnly cookies instead of localStorage for user sessions

2 participants