Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Bug] Fix admin refresh & persist csrf along session#1307

Merged
Fermionic-Lyu merged 4 commits into
mainfrom
fix-auth-flow
May 19, 2026
Merged

[Bug] Fix admin refresh & persist csrf along session#1307
Fermionic-Lyu merged 4 commits into
mainfrom
fix-auth-flow

Conversation

@Fermionic-Lyu
Copy link
Copy Markdown
Member

@Fermionic-Lyu Fermionic-Lyu commented May 19, 2026

Summary

  1. Persist CSRF token along the same login session. This is the main bug that constantly fails the refresh request

  2. Separate admin cookies with end user cookies. This is the main bug that logs out the developer

How did you test this change?

Testing in progress


Summary by cubic

Fixes flaky admin refresh by persisting CSRF across a session and separating admin vs user refresh flows to prevent cross-logouts. Adds admin-only routes, cookies, and dashboard client updates; enforces session types and stabilizes CSRF across rotation.

  • Bug Fixes

    • CSRF derives from refresh payload claims (csrfNonce, sessionType) and is verified against them; stable across rotation; legacy tokens without claims are rejected.
    • Refresh enforces sessionType (user/admin); web refresh validates X-CSRF-Token; non-web refresh returns refresh tokens in the body; cookies are cleared only on 401.
    • Separate cookies: insforge_admin_refresh_token scoped to /api/auth/admin and insforge_refresh_token for app sessions.
  • New Features

    • Admin routes: POST /api/auth/admin/sessions, /sessions/exchange, /refresh, /logout; login/refresh return a csrfToken.
    • TokenManager: added generateRefreshTokenWithCsrf; refresh payloads carry csrfNonce and sessionType.
    • Dashboard uses admin endpoints and stores CSRF in insforge_admin_csrf_token (SameSite=Lax, Secure); ApiClient always sends Authorization and removes skipAuth.
    • OAuth/web flows issue CSRF-paired refresh tokens for web and plain refresh tokens for non-web.
    • Docs and openapi/auth.yaml updated; tests added for CSRF derivation, cookie paths, admin routes, and dashboard client.

Written for commit 1e5b302. Summary will update on new commits. Review in cubic

Note

Fix admin token refresh by adding CSRF persistence across session rotation

  • Adds dedicated admin auth routes (/api/auth/admin/refresh, /api/auth/admin/logout, /api/auth/admin/sessions, /api/auth/admin/sessions/exchange) in a new admin.routes.ts, separating admin auth from user auth.
  • Embeds csrfNonce and sessionType ('user' or 'admin') in refresh token payloads; CSRF tokens are now derived from these claims via HMAC, making them stable across token rotation.
  • Admin refresh tokens are stored in a separate httpOnly cookie scoped to /api/auth/admin, independent of the user cookie path.
  • Dashboard logout and refresh now call the admin-specific endpoints and send X-CSRF-Token on refresh requests.
  • Risk: Legacy refresh tokens without csrfNonce or sessionType claims are rejected, requiring re-authentication for any existing sessions.

Changes since #1307 opened

  • Replaced manual refresh token and CSRF token generation across all authentication routes with TokenManager.generateRefreshTokenWithCsrf [3b93929]
  • Fixed admin refresh token error handling to preserve refresh cookie on non-401 errors and updated session exchange error handling [3b93929]
  • Reimplemented LoginService.refreshAccessToken method in dashboard to use apiClient.request with CSRF token header [3b93929]
  • Added Secure attribute to CSRF cookie operations in dashboard ApiClient [3b93929]
  • Removed skipAuth option from ApiClient.request method and eliminated conditional Authorization header attachment [3b93929]
  • Modified /api/auth/refresh handler to conditionally generate CSRF-paired refresh tokens for web clients and non-CSRF refresh tokens for non-web clients, and changed error handling to clear refresh cookies only on 401 errors [678b391]
  • Changed visibility of TokenManager.generateCsrfNonce method from public to private [678b391]
  • Added test utilities and expanded test suite for admin authentication refresh behavior including CSRF handling, error scenarios, and cookie preservation [678b391]

Macroscope summarized 911ff30.

Summary by CodeRabbit

  • New Features

    • Admin dashboard: added admin refresh and logout endpoints with cookie+CSRF-based admin sessions; CSRF tokens now derived per-session type (user vs admin).
    • Dashboard client: switched to admin-specific refresh/logout flows and admin CSRF cookie name.
  • Documentation

    • API docs and OpenAPI updated to document admin refresh/logout and CSRF behavior; admin login responses include a CSRF token.
  • Tests

    • Added unit/regression tests for refresh cookies, CSRF derivation, and admin auth flows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Walkthrough

This PR refactors authentication to support separate user and admin sessions with session-type-aware refresh tokens. TokenManager now embeds sessionType and csrfNonce in refresh tokens and derives CSRF tokens from payload claims. User auth endpoints adopt the scheme; a dedicated admin router and admin refresh/logout endpoints are added. Dashboard client and docs are updated; tests cover cookies, TokenManager CSRF, and admin route regressions.

Changes

Admin Dashboard Authentication with Session-Type-Aware Token Refresh

Layer / File(s) Summary
Token Manager refresh token and CSRF refactoring
backend/src/infra/security/token.manager.ts, backend/tests/unit/token-manager-csrf.test.ts
RefreshTokenPayload now includes csrfNonce and sessionType. Generation APIs require sessionType and accept an optional csrfNonce. CSRF tokens are derived from payload claims and compared in constant time. Refresh verification enforces required claims; tests validate rotation stability and rejection scenarios.
Admin refresh token cookie infrastructure
backend/src/utils/cookies.ts, backend/tests/unit/auth-cookies.test.ts
Adds ADMIN_REFRESH_TOKEN_COOKIE_NAME, setAdminRefreshTokenCookie, and clearAdminRefreshTokenCookie. Admin cookie uses /api/auth/admin path and 7-day maxAge; tests verify set/clear behavior and options for admin vs user cookies.
Admin authentication endpoints
backend/src/api/routes/auth/admin.routes.ts
New admin router implementing /sessions/exchange, /sessions, /refresh, and /logout. Admin refresh enforces sessionType === 'admin', verifies CSRF against decoded refresh payload, checks admin role, rotates tokens preserving csrfNonce, and manages the admin refresh cookie.
User auth routes: sessionType='user' and payload CSRF
backend/src/api/routes/auth/index.routes.ts
User endpoints now generate/rotate refresh tokens with sessionType: 'user' and verify CSRF against decoded refresh payload. Admin session handlers removed from the user router and adminRouter is mounted at /admin.
OAuth exchange client-type-specific refresh
backend/src/api/routes/auth/oauth.routes.ts
OAuth /exchange defers refresh-token generation until after socket broadcast and issues session-type-aware refreshs: web clients receive cookie + csrfToken; non-web clients receive a refresh token in the body.
TokenManager CSRF unit tests
backend/tests/unit/token-manager-csrf.test.ts
Tests cover CSRF stability across refresh rotations, CSRF rejection across token families, sessionType preservation, generateRefreshTokenWithCsrf, sessionType influence on CSRF derivation, and error handling for legacy tokens missing claims.
Backend admin route regression tests
backend/tests/unit/auth-admin-routes.test.ts
Regression suite validates admin refresh error handling, cookie-preservation semantics on 403/500, cookie clearing on 401, and source-level patterns ensuring CSRF derivation comes from payloads and web-only branches generate CSRF.
Dashboard client: CSRF cookie + routing + tests
packages/dashboard/src/lib/api/client.ts, packages/dashboard/src/features/login/services/login.service.ts, packages/dashboard/src/features/login/services/login.service.test.ts, packages/dashboard/src/lib/api/client.test.ts, packages/dashboard/src/lib/services/health.service.ts
CSRF cookie renamed to insforge_admin_csrf_token (Secure); ApiClient.request removed skipAuth option and always attaches Authorization when token present; LoginService routes refresh/logout to admin endpoints and uses apiClient.request for refresh. Client tests validate CSRF cookie behavior and admin refresh request composition; HealthService now calls /health without explicit skipAuth.
API specification and documentation updates
openapi/auth.yaml, docs/sdks/rest/auth.mdx, docs/core-concepts/authentication/architecture.mdx, packages/shared-schemas/src/auth-api.schema.ts
OpenAPI: admin session response includes csrfToken; adds POST /api/auth/admin/refresh (requires X-CSRF-Token) and POST /api/auth/admin/logout. SDK and architecture docs updated to document admin refresh/logout and admin cookie usage; shared-schema comment clarifies separate refresh endpoints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • jwfing
  • tonychang04

Poem

🐰 A nonce tucked in secret grain,
Cookies hum along the lane,
Admin doors with CSRF keys,
Tokens spin with quieter pleas,
Rabbit hops — auth safe again.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: fixing admin refresh functionality while persisting CSRF tokens across sessions. It directly addresses the main objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-auth-flow

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

@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 19, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
InsForge-docs 🟢 Ready View Preview May 19, 2026, 7:45 PM

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR fixes two root-cause bugs in the admin dashboard session flow: CSRF tokens are now derived from stable JWT payload claims (csrfNonce, sessionType) rather than from the raw token string, so the token stays consistent across rotation; and admin sessions are isolated to a dedicated insforge_admin_refresh_token cookie scoped to /api/auth/admin, preventing cross-logout with end-user sessions.

  • CSRF stabilisation (token.manager.ts): generateCsrfToken switched from HMAC(JWT_string) to HMAC(sessionType:sub:csrfNonce). The nonce is embedded in the refresh token and preserved on rotation, so the dashboard CSRF cookie stays valid for the full session lifetime.
  • Admin route separation (admin.routes.ts): New router at /api/auth/admin with dedicated cookie helpers scoped to path /api/auth/admin; /refresh verifies sessionType === 'admin', CSRF header, and is_project_admin in the DB.
  • Dashboard client (client.ts, login.service.ts): Admin CSRF cookie renamed to insforge_admin_csrf_token with Secure flag; refreshAccessToken migrated from raw fetch to apiClient.request pointing at /auth/admin/refresh; skipAuth option removed.

Confidence Score: 4/5

The core CSRF and session-isolation logic is sound; the main open concern from a prior review (admin /logout has no CSRF guard while its cookie uses sameSite: none) remains unaddressed in this revision.

The CSRF derivation, nonce preservation across token rotation, and admin/user cookie path separation all look correct. The one unresolved issue from a previous review pass — the /logout endpoint accepting cross-site requests without any CSRF check — is still present in admin.routes.ts. Until that is addressed, a malicious page can force an admin logout at any time.

backend/src/api/routes/auth/admin.routes.ts — specifically the /logout handler which clears the admin cookie without validating X-CSRF-Token.

Important Files Changed

Filename Overview
backend/src/infra/security/token.manager.ts Core security change: CSRF token is now derived from JWT payload claims (sessionType + sub + csrfNonce) via HMAC instead of from the raw token string, making CSRF stable across token rotation. verifyRefreshToken now rejects legacy tokens lacking csrfNonce/sessionType, which forces all pre-deployment sessions to re-authenticate.
backend/src/api/routes/auth/admin.routes.ts New admin-only auth routes with dedicated cookie scoped to /api/auth/admin. The /refresh endpoint correctly checks sessionType, CSRF, and is_project_admin. The /logout endpoint clears the admin cookie but does not validate the X-CSRF-Token header, leaving it open to cross-site forced-logout requests (flagged in previous review).
backend/src/api/routes/auth/index.routes.ts User refresh flow updated: CSRF check moved to after JWT verification (harmless reorder), sessionType guard added to reject admin tokens on the user endpoint, cookie is now only cleared on 401 (not 403), and all login paths switched to generateRefreshTokenWithCsrf.
packages/dashboard/src/lib/api/client.ts Removes skipAuth option — Authorization header is now always attached when a token is in memory. CSRF cookie renamed to insforge_admin_csrf_token and gains Secure attribute.
packages/dashboard/src/features/login/services/login.service.ts refreshAccessToken migrated from raw fetch to apiClient.request targeting /auth/admin/refresh; logout and refresh endpoints now point to admin-specific routes. Logic is correct.

Reviews (3): Last reviewed commit: "remove endpoint test" | Re-trigger Greptile

Comment thread backend/src/infra/security/token.manager.ts
Comment thread backend/src/api/routes/auth/admin.routes.ts
Copy link
Copy Markdown
Contributor

@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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
packages/dashboard/src/features/login/services/login.service.ts (1)

89-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the configured API base URL for admin refresh requests.

Line 89 hardcodes /api/auth/admin/refresh, which bypasses getDashboardApiBaseUrl() used by the rest of the client. In non-default/proxied setups, this can break refresh and cause auth loops.

Suggested fix
 import { apiClient, REQUEST_TIMEOUT_MS } from '`#lib/api/client`';
+import { getDashboardApiBaseUrl } from '`#lib/config/runtime`';
 import type { UserSchema } from '`@insforge/shared-schemas`';
@@
-      const response = await fetch('/api/auth/admin/refresh', {
+      const response = await fetch(
+        `${getDashboardApiBaseUrl().replace(/\/$/, '')}/auth/admin/refresh`,
+        {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
           'X-CSRF-Token': csrfToken,
         },
         credentials: 'include',
         signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
-      });
+      });
🤖 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/dashboard/src/features/login/services/login.service.ts` around lines
89 - 97, The fetch call in login.service.ts uses a hardcoded
'/api/auth/admin/refresh' which bypasses getDashboardApiBaseUrl(); update the
code that builds the refresh URL to use getDashboardApiBaseUrl() (e.g., compute
const base = getDashboardApiBaseUrl(); then build the refresh endpoint as base +
'/api/auth/admin/refresh' or otherwise join paths to avoid double slashes) and
use that URL in the fetch call (the function performing the fetch is the admin
refresh request in login.service.ts). Ensure headers, credentials, and
AbortSignal.timeout(REQUEST_TIMEOUT_MS) remain unchanged.
backend/src/infra/security/token.manager.ts (1)

103-117: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Keep a legacy refresh path through the 7-day token horizon.

These guards now reject every refresh token minted before this rollout, and CSRF recomputation also depends on the new claims. On deploy, any still-active session will fail its next refresh and get logged out. Please keep a compatibility path long enough to exchange legacy refresh tokens for the new shape, then remove it after the old 7-day window has expired.

Also applies to: 208-225

🤖 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 `@backend/src/infra/security/token.manager.ts` around lines 103 - 117, The
strict guards in verifyRefreshToken are currently rejecting pre-rollout refresh
tokens; add a compatibility path that accepts legacy refresh tokens for a 7-day
window: detect a legacy shape (e.g., missing csrfNonce or missing/absent
sessionType or older claim layout) and allow verification if jwt.verify succeeds
and the token's iat/exp indicates it was issued within the last 7 days; for
legacy tokens, populate default values for csrfNonce/sessionType in the returned
RefreshTokenPayload (or include a legacyRefresh flag) so callers can force
rotation and skip CSRF recomputation for that flow. Apply the same compatibility
changes to the corresponding logic referenced around lines 208-225 so both
verification paths accept tokens from the 7-day horizon and mark them for
exchange.
🤖 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 `@backend/src/api/routes/auth/admin.routes.ts`:
- Around line 144-149: The catch block currently calls
clearAdminRefreshTokenCookie(res) for all non-403 errors which forces logout on
transient failures (e.g., failures in getUserById() or token issuance); change
it to only clear the cookie for explicit auth-invalidating failures by checking
the error type/status: call clearAdminRefreshTokenCookie(res) only when the
error indicates authentication is invalid (for example error instanceof AppError
&& error.statusCode === 401) or other known token-invalid errors, and otherwise
leave the cookie intact so transient backend errors do not log the admin out.
- Around line 53-59: The current catch converts every non-AppError into a 400
and includes the raw exception text; instead, preserve AppError propagation (if
error instanceof AppError then next(error)), otherwise log the original
exception (using the module logger or console.error) and call next(new
AppError('Failed to exchange admin session', 500,
ERROR_CODES.INTERNAL_SERVER_ERROR_OR_AUTH_ERROR)) with a generic 5xx status and
without appending error.message to the response; update the block around the
existing next(new AppError(...)) call in admin session exchange to implement
this behavior.

In `@packages/dashboard/src/lib/api/client.ts`:
- Line 3: The change to CSRF_COOKIE_NAME breaks existing sessions because
getCsrfToken() only reads the new key; update getCsrfToken() to try the new
constant CSRF_COOKIE_NAME first and, if not found, fall back to the previous
cookie name (the legacy key) so pre-existing sessions continue to work during
rollout; ensure any write/update logic (where CSRF_COOKIE_NAME is used)
continues to set the new key while reads accept both names, and reference
CSRF_COOKIE_NAME and getCsrfToken() when implementing the fallback.

---

Outside diff comments:
In `@backend/src/infra/security/token.manager.ts`:
- Around line 103-117: The strict guards in verifyRefreshToken are currently
rejecting pre-rollout refresh tokens; add a compatibility path that accepts
legacy refresh tokens for a 7-day window: detect a legacy shape (e.g., missing
csrfNonce or missing/absent sessionType or older claim layout) and allow
verification if jwt.verify succeeds and the token's iat/exp indicates it was
issued within the last 7 days; for legacy tokens, populate default values for
csrfNonce/sessionType in the returned RefreshTokenPayload (or include a
legacyRefresh flag) so callers can force rotation and skip CSRF recomputation
for that flow. Apply the same compatibility changes to the corresponding logic
referenced around lines 208-225 so both verification paths accept tokens from
the 7-day horizon and mark them for exchange.

In `@packages/dashboard/src/features/login/services/login.service.ts`:
- Around line 89-97: The fetch call in login.service.ts uses a hardcoded
'/api/auth/admin/refresh' which bypasses getDashboardApiBaseUrl(); update the
code that builds the refresh URL to use getDashboardApiBaseUrl() (e.g., compute
const base = getDashboardApiBaseUrl(); then build the refresh endpoint as base +
'/api/auth/admin/refresh' or otherwise join paths to avoid double slashes) and
use that URL in the fetch call (the function performing the fetch is the admin
refresh request in login.service.ts). Ensure headers, credentials, and
AbortSignal.timeout(REQUEST_TIMEOUT_MS) remain unchanged.
🪄 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: CHILL

Plan: Pro

Run ID: 14098402-43dc-4419-bb98-93642f851a33

📥 Commits

Reviewing files that changed from the base of the PR and between 376ec5b and 911ff30.

📒 Files selected for processing (13)
  • backend/src/api/routes/auth/admin.routes.ts
  • backend/src/api/routes/auth/index.routes.ts
  • backend/src/api/routes/auth/oauth.routes.ts
  • backend/src/infra/security/token.manager.ts
  • backend/src/utils/cookies.ts
  • backend/tests/unit/auth-cookies.test.ts
  • backend/tests/unit/token-manager-csrf.test.ts
  • docs/core-concepts/authentication/architecture.mdx
  • docs/sdks/rest/auth.mdx
  • openapi/auth.yaml
  • packages/dashboard/src/features/login/services/login.service.ts
  • packages/dashboard/src/lib/api/client.ts
  • packages/shared-schemas/src/auth-api.schema.ts

Comment thread backend/src/api/routes/auth/admin.routes.ts Outdated
Comment thread backend/src/api/routes/auth/admin.routes.ts
import { getDashboardApiBaseUrl } from '#lib/config/runtime';

const CSRF_COOKIE_NAME = 'insforge_csrf';
const CSRF_COOKIE_NAME = 'insforge_admin_csrf_token';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a temporary legacy CSRF cookie fallback for rollout safety.

Line 3 switches the cookie key, but getCsrfToken() will no longer read pre-existing sessions using the old key. That can force immediate re-auth after deployment.

Suggested fix
-const CSRF_COOKIE_NAME = 'insforge_admin_csrf_token';
+const ADMIN_CSRF_COOKIE_NAME = 'insforge_admin_csrf_token';
+const LEGACY_CSRF_COOKIE_NAME = 'insforge_csrf';
@@
-    document.cookie = `${CSRF_COOKIE_NAME}=${encodeURIComponent(csrfToken)}; expires=${expires}; path=/; SameSite=Lax`;
+    document.cookie = `${ADMIN_CSRF_COOKIE_NAME}=${encodeURIComponent(csrfToken)}; expires=${expires}; path=/; SameSite=Lax`;
@@
-    document.cookie = `${CSRF_COOKIE_NAME}=; max-age=0; path=/; SameSite=Lax`;
+    document.cookie = `${ADMIN_CSRF_COOKIE_NAME}=; max-age=0; path=/; SameSite=Lax`;
+    document.cookie = `${LEGACY_CSRF_COOKIE_NAME}=; max-age=0; path=/; SameSite=Lax`;
@@
-    const match = document.cookie.match(new RegExp(`(?:^|; )${CSRF_COOKIE_NAME}=([^;]*)`));
-    return match ? decodeURIComponent(match[1]) : null;
+    const adminMatch = document.cookie.match(
+      new RegExp(`(?:^|; )${ADMIN_CSRF_COOKIE_NAME}=([^;]*)`)
+    );
+    if (adminMatch) {
+      return decodeURIComponent(adminMatch[1]);
+    }
+    const legacyMatch = document.cookie.match(
+      new RegExp(`(?:^|; )${LEGACY_CSRF_COOKIE_NAME}=([^;]*)`)
+    );
+    return legacyMatch ? decodeURIComponent(legacyMatch[1]) : null;
🤖 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/dashboard/src/lib/api/client.ts` at line 3, The change to
CSRF_COOKIE_NAME breaks existing sessions because getCsrfToken() only reads the
new key; update getCsrfToken() to try the new constant CSRF_COOKIE_NAME first
and, if not found, fall back to the previous cookie name (the legacy key) so
pre-existing sessions continue to work during rollout; ensure any write/update
logic (where CSRF_COOKIE_NAME is used) continues to set the new key while reads
accept both names, and reference CSRF_COOKIE_NAME and getCsrfToken() when
implementing the fallback.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 13 files

Confidence score: 3/5

  • There is some merge risk because backend/src/api/routes/auth/admin.routes.ts currently clears the admin refresh cookie on non-403 errors, which can force unexpected logouts during transient backend failures (severity 6/10, high confidence).
  • Error handling in backend/src/api/routes/auth/admin.routes.ts also appears to expose raw internal failure messages and maps unexpected exchange failures to 400 INVALID_INPUT, which can misclassify server-side faults and leak internals.
  • Pay close attention to backend/src/api/routes/auth/admin.routes.ts - tighten cookie-clearing conditions and return a generic server error for unexpected exchange failures.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/src/api/routes/auth/admin.routes.ts">

<violation number="1" location="backend/src/api/routes/auth/admin.routes.ts:56">
P2: Avoid returning raw internal error messages and `400 INVALID_INPUT` for unexpected exchange failures; use a generic server error instead.</violation>

<violation number="2" location="backend/src/api/routes/auth/admin.routes.ts:147">
P2: Only clear the admin refresh cookie for authentication-invalidating failures (for example 401). Clearing it for every non-403 error turns transient backend failures into forced logout.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/src/api/routes/auth/admin.routes.ts Outdated
Comment thread backend/src/api/routes/auth/admin.routes.ts Outdated
Copy link
Copy Markdown
Member

@jwfing jwfing left a comment

Choose a reason for hiding this comment

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

Code Review — Fix admin refresh & persist CSRF along session

Summary: The approach is sound and the core security design is well-executed. The two root bugs (CSRF instability across rotation and admin/user cookie collision) are fixed correctly. No blocking issues; a few suggestions below.


Requirements context

No matching spec or plan was found under /docs/superpowers/specs/ or /docs/superpowers/plans/ for this change. Assessment is against the PR description and linked behaviour: (1) CSRF token must survive refresh-token rotation, (2) admin and user cookies must be independent.


Findings

Critical

(none)


Suggestion

[Software Engineering] Redundant verifyRefreshToken round-trip after generateRefreshToken
backend/src/api/routes/auth/admin.routes.ts:45 and :87

const csrfToken = tokenManager.generateCsrfToken(tokenManager.verifyRefreshToken(refreshToken));

The payload is already known (you just assembled it in generateRefreshToken). Calling verifyRefreshToken immediately after forces an unnecessary sign→verify round-trip. generateCsrfToken accepts a RefreshTokenPayload, so you can construct the struct inline instead:

const nonce = tokenManager.generateCsrfNonce();
const refreshToken = tokenManager.generateRefreshToken(result.user.id, 'admin', nonce);
const payload: RefreshTokenPayload = { sub: result.user.id, type: 'refresh', iss: 'insforge', csrfNonce: nonce, sessionType: 'admin' };
const csrfToken = tokenManager.generateCsrfToken(payload);

Or export a helper that returns both at once.


[Security] CSRF cookie missing Secure attribute
packages/dashboard/src/lib/api/client.ts:25

document.cookie = `${CSRF_COOKIE_NAME}=${encodeURIComponent(csrfToken)}; expires=${expires}; path=/; SameSite=Lax`;

The Secure flag is absent. The CSRF token would be transmitted in plaintext over HTTP. The production dashboard is presumably HTTPS-only, but adding ; Secure is cheap defence-in-depth and consistent with how the refresh cookies are configured.


[Software Engineering] No integration tests for the new HTTP endpoints
backend/src/api/routes/auth/admin.routes.ts (all four routes)

The unit tests in auth-cookies.test.ts and token-manager-csrf.test.ts cover the helpers well. But there are no tests exercising the actual routes (correct cookie header set, 401 on missing cookie, 403 on bad CSRF, cookie preserved on 403 but cleared on 401, is_project_admin guard, etc.). The PR description itself says "Testing in progress" — these should land before or alongside merge.


[Security] Stable CSRF nonce → stolen CSRF token valid for full 7-day session
backend/src/infra/security/token.manager.ts:133 (admin refresh) / index.routes.ts:612 (user refresh)

The nonce is intentionally kept constant across rotations to solve the flakiness bug (good call). The consequence is that a leaked CSRF token (e.g., read via devtools, XSS, or shared browser profile) remains valid for the entire 7-day session lifetime — it never rotates. This trade-off is reasonable given the bug being fixed, but is worth noting in a code comment or SECURITY.md for future maintainers.


Information

Breaking change: legacy refresh tokens rejected on deploy
backend/src/infra/security/token.manager.ts:111-117

verifyRefreshToken now rejects tokens that lack csrfNonce or sessionType. Any session established before this deploy (all current logged-in users and admins) will be rejected and forced to re-authenticate. The PR mentions this in the Macroscope note. Ops should plan for a coordinated deploy or an in-browser banner; no code change needed, just awareness.

No matching spec/plan in /docs/superpowers/
No existing spec or plan document covers admin session separation or CSRF persistence. Assessing against PR description alone.

Global rate limiter covers admin routes (3 000 req / 15 min / IP)
backend/src/server.ts:86-91 — the global rate limiter is in place, so POST /api/auth/admin/sessions is not completely unguarded. A tighter, credential-specific limiter would be better hardening but is outside the stated scope of this PR.


Verdict

approved (informational — explicit GitHub approval via the approve flow is a separate human action)

The CSRF stability fix (nonce embedded in token, HMAC-derived CSRF) and the admin/user cookie separation are both implemented correctly. timingSafeEqual, httpOnly, path-scoping, and session-type guards are all in the right places. The Suggestions above — especially integration tests and the Secure cookie attribute — should be addressed before or shortly after merge.

Copy link
Copy Markdown
Contributor

@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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/dashboard/src/lib/api/client.ts (1)

54-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep a way to make unauthenticated requests.

Removing skipAuth means every call now inherits the dashboard bearer token and the 401 refresh flow. That changes public endpoints like packages/dashboard/src/lib/services/health.service.ts Line 10 from pure liveness checks into auth-dependent calls, and it also prevents callers from intentionally omitting or overriding Authorization.

Also applies to: 62-70, 100-113

🤖 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/dashboard/src/lib/api/client.ts` around lines 54 - 59, The request
function removed the ability to make unauthenticated calls; restore an explicit
skipAuth (or allowUnauthenticated) flag in request's options signature and use
it when building headers so the dashboard bearer token / 401 refresh flow is
only applied when skipAuth is false; specifically, update the request(...)
options to include skipAuth?: boolean, and change the header logic in request
(and related branches around the token/refresh flow in the same file) to: if
skipAuth is truthy or an Authorization header is already present, do not inject
the dashboard bearer token or trigger the refresh flow, otherwise attach
Authorization and proceed with refresh-on-401 behavior.
🧹 Nitpick comments (1)
backend/tests/unit/auth-admin-routes.test.ts (1)

5-36: 🏗️ Heavy lift

Avoid source-text assertions for auth behavior.

These assertions are coupled to exact log strings and expressions, so they can pass while the handler behavior is wrong and fail on harmless refactors. Please exercise the route/cookie behavior directly in unit tests instead of inspecting source text.

🤖 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 `@backend/tests/unit/auth-admin-routes.test.ts` around lines 5 - 36, The test
uses brittle source-text assertions against admin.routes.ts and auth route files
(checking for logger.error strings, ERROR_CODES.INTERNAL_ERROR, AppError checks,
and specific calls like generateCsrfToken(tokenManager.verifyRefreshToken) or
verifyRefreshToken(newRefreshToken)) — replace these with behavioral unit tests
that exercise the route handlers instead: invoke the admin auth exchange and
assert the HTTP response is a generic server error and that internal error codes
are returned rather than relying on the exact logger message; simulate non-auth
transient refresh failures and assert the admin refresh cookie is preserved
(check Set-Cookie/no cookie deletion) and that 401 vs 403 behavior matches
expectations rather than string-matching on "error instanceof AppError"; for
CSRF/refresh token behavior, call the refresh/token issuance flow and assert you
do not perform an extra verify step on a freshly issued refresh token (i.e.,
generated refresh token is returned with CSRF derived via
generateRefreshTokenWithCsrf semantics) instead of searching for occurrences of
generateCsrfToken(tokenManager.verifyRefreshToken) or
verifyRefreshToken(newRefreshToken) in source text.
🤖 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 `@backend/tests/unit/auth-admin-routes.test.ts`:
- Around line 24-27: The assertion is checking for an auth failure (401) but the
test intends to verify non-auth transient refresh failures; update the
expectations in the test to look for a transient status (e.g., 503) instead of
401. Specifically, in the test that references adminRoutesSource, replace the
check that looks for 'error instanceof AppError && error.statusCode === 401'
with one that looks for 'error instanceof AppError && error.statusCode === 503',
and flip the negative assertion to ensure it does not contain 'error.statusCode
=== 401' so the test asserts non-auth transient behavior.

---

Outside diff comments:
In `@packages/dashboard/src/lib/api/client.ts`:
- Around line 54-59: The request function removed the ability to make
unauthenticated calls; restore an explicit skipAuth (or allowUnauthenticated)
flag in request's options signature and use it when building headers so the
dashboard bearer token / 401 refresh flow is only applied when skipAuth is
false; specifically, update the request(...) options to include skipAuth?:
boolean, and change the header logic in request (and related branches around the
token/refresh flow in the same file) to: if skipAuth is truthy or an
Authorization header is already present, do not inject the dashboard bearer
token or trigger the refresh flow, otherwise attach Authorization and proceed
with refresh-on-401 behavior.

---

Nitpick comments:
In `@backend/tests/unit/auth-admin-routes.test.ts`:
- Around line 5-36: The test uses brittle source-text assertions against
admin.routes.ts and auth route files (checking for logger.error strings,
ERROR_CODES.INTERNAL_ERROR, AppError checks, and specific calls like
generateCsrfToken(tokenManager.verifyRefreshToken) or
verifyRefreshToken(newRefreshToken)) — replace these with behavioral unit tests
that exercise the route handlers instead: invoke the admin auth exchange and
assert the HTTP response is a generic server error and that internal error codes
are returned rather than relying on the exact logger message; simulate non-auth
transient refresh failures and assert the admin refresh cookie is preserved
(check Set-Cookie/no cookie deletion) and that 401 vs 403 behavior matches
expectations rather than string-matching on "error instanceof AppError"; for
CSRF/refresh token behavior, call the refresh/token issuance flow and assert you
do not perform an extra verify step on a freshly issued refresh token (i.e.,
generated refresh token is returned with CSRF derived via
generateRefreshTokenWithCsrf semantics) instead of searching for occurrences of
generateCsrfToken(tokenManager.verifyRefreshToken) or
verifyRefreshToken(newRefreshToken) in source text.
🪄 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: CHILL

Plan: Pro

Run ID: bae9163a-4e6d-494e-bb95-3fe51903fd52

📥 Commits

Reviewing files that changed from the base of the PR and between 911ff30 and 3b93929.

📒 Files selected for processing (11)
  • backend/src/api/routes/auth/admin.routes.ts
  • backend/src/api/routes/auth/index.routes.ts
  • backend/src/api/routes/auth/oauth.routes.ts
  • backend/src/infra/security/token.manager.ts
  • backend/tests/unit/auth-admin-routes.test.ts
  • backend/tests/unit/token-manager-csrf.test.ts
  • packages/dashboard/src/features/login/services/login.service.test.ts
  • packages/dashboard/src/features/login/services/login.service.ts
  • packages/dashboard/src/lib/api/client.test.ts
  • packages/dashboard/src/lib/api/client.ts
  • packages/dashboard/src/lib/services/health.service.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/src/api/routes/auth/oauth.routes.ts
  • backend/src/api/routes/auth/admin.routes.ts
  • backend/src/infra/security/token.manager.ts
  • backend/tests/unit/token-manager-csrf.test.ts

Comment thread backend/tests/unit/auth-admin-routes.test.ts Outdated
Copy link
Copy Markdown
Member

@jwfing jwfing left a comment

Choose a reason for hiding this comment

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

Summary

Fixes flaky admin refresh and cross-session logout by embedding a stable csrfNonce + sessionType in every refresh JWT, and isolating the admin refresh cookie to /api/auth/admin. The approach is architecturally sound; no Critical blockers.

Requirements context

No spec or plan under /docs/superpowers/specs/ or /docs/superpowers/plans/ matched this PR's scope — assessing against the PR description alone.


Findings

Critical

(none)


Suggestion

Security

S1 — POST /api/auth/admin/sessions is unrate-limited (backend/src/api/routes/auth/admin.routes.ts:58–84)

This endpoint validates email+password against static env-variable credentials. It has no rate limiter. An attacker with network access to the backend can brute-force admin credentials without constraint. The pre-existing route in index.routes.ts was also unrate-limited, but the migration to a dedicated router is a clean opportunity to fix this. Consider adding a lightweight per-IP limiter (e.g., sendEmailOTPRateLimiter or a new adminLoginRateLimiter) as middleware on this route. The sessions/exchange OAuth path does not have this problem since it validates a cloud-issued JWT, not a password.

S2 — Logout endpoint can be CSRF-triggered without authentication (backend/src/api/routes/auth/admin.routes.ts:141–152)

POST /api/auth/admin/logout clears the admin refresh cookie unconditionally, with no CSRF token or valid-cookie check. Because the admin refresh cookie carries SameSite=none, a cross-origin page can trigger a logout request that includes the cookie and force the admin to re-authenticate. This is a session-disruption vector rather than a session-takeover, but it's cheap to close: either require X-CSRF-Token here (same as refresh), or at minimum verify the admin cookie exists before clearing it.

Software Engineering

S3 — auth-admin-routes.test.ts tests source text, not behavior (backend/tests/unit/auth-admin-routes.test.ts:1–37)

All three test cases read the .ts source file with readFileSync and assert on string literals (e.g., toContain("logger.error('[Auth:AdminSessionExchange]...")). This validates that a specific string exists in the source file, not that the route actually behaves correctly under those conditions. A log message rename or code reformat would fail the test with no real regression. These would be much more valuable as actual route-level tests (using supertest against an Express app, matching the pattern in other test files in this repo) that assert HTTP response codes and cookie behaviour.

S4 — generateCsrfNonce() is unintentionally public (backend/src/infra/security/token.manager.ts:250–252)

generateCsrfNonce() is an internal implementation detail of generateRefreshToken / generateRefreshTokenWithCsrf. Callers outside TokenManager have no reason to call it. Making it private keeps the interface clean and prevents future callers from accidentally generating nonces that bypass the standard token-creation path.


Information

I1 — Asymmetric cookie-clearing strategy between user and admin refresh handlers

  • User /refresh (index.routes.ts:615): clears cookie on all errors except 403 (CSRF mismatch).
  • Admin /refresh (admin.routes.ts:133): clears cookie only on 401; all other errors (including 500s) leave the cookie intact.

Both preserve the cookie on CSRF failure (the critical invariant), but the admin path is more conservative — a transient DB error won't end an admin session. This is a reasonable product decision, but the asymmetry is not documented and may surprise future maintainers. A brief comment in the admin catch block explaining why 500s are treated differently from user sessions would help.

I2 — generateRefreshTokenWithCsrf is called for non-web client token rotation (backend/src/api/routes/auth/index.routes.ts:591–592)

In the user /refresh endpoint, generateRefreshTokenWithCsrf is always called (even for non-web clients), and the resulting csrfToken is discarded for that branch. The cost is negligible (one HMAC), but using generateRefreshToken for the non-web path would be more explicit about intent and avoids computing a value that's immediately thrown away.

I3 — Breaking change: all active refresh tokens will be invalidated on deploy

verifyRefreshToken now hard-rejects any token missing csrfNonce or sessionType. Every existing session (user and admin) will fail its first refresh after this ships and require re-authentication. This is called out in the PR description, but worth ensuring is communicated in release notes / deployment runbook so operators aren't surprised by a support spike.


Verdict

approved (informational — no Critical findings; human approval still required via the approve flow)

The CSRF design is solid: nonce is embedded in the signed JWT (not derivable without the secret), HMAC input includes sessionType to prevent cross-session reuse, and timingSafeEqual is used for the comparison. The cookie-path separation correctly isolates admin and user sessions. The test coverage in token-manager-csrf.test.ts and auth-cookies.test.ts is good; the main gap is the source-text assertions in auth-admin-routes.test.ts (S3 above). Address S1 and S2 before or shortly after ship.

Copy link
Copy Markdown
Contributor

@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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
backend/tests/unit/auth-admin-routes.test.ts (1)

1-211: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix Prettier violations in this file before merge.

Pipeline reports formatting drift for this test file. Please run Prettier on this file to clear the lint-and-format warning.

🤖 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 `@backend/tests/unit/auth-admin-routes.test.ts` around lines 1 - 211, Run the
project's Prettier formatter on the test file to fix formatting drift: apply the
configured Prettier rules (e.g., via your local npm script like npm run format
or npx prettier --write) to the test suite containing describe('admin auth route
review regressions', ...) and the helper functions createAdminAuthApp and
postAdminRefresh so the file no longer reports lint/format violations; commit
the reformatted file.
🤖 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 `@backend/tests/unit/auth-admin-routes.test.ts`:
- Around line 67-83: The helper postAdminRefresh currently uses global fetch
which is being interfered with in tests; replace it to perform a test-local
request against the Express app returned by createAdminAuthApp instead (use
supertest or Node http.request) so you don't start a real listener or depend on
global fetch. Update postAdminRefresh to accept headers, call
createAdminAuthApp() and issue a POST to /api/auth/admin/refresh via supertest
(or create a local request against the app), then return the response.status and
response.headers['set-cookie'] (preserving the current return shape) and ensure
the server is not bound to a real port or global fetch is avoided.

---

Outside diff comments:
In `@backend/tests/unit/auth-admin-routes.test.ts`:
- Around line 1-211: Run the project's Prettier formatter on the test file to
fix formatting drift: apply the configured Prettier rules (e.g., via your local
npm script like npm run format or npx prettier --write) to the test suite
containing describe('admin auth route review regressions', ...) and the helper
functions createAdminAuthApp and postAdminRefresh so the file no longer reports
lint/format violations; commit the reformatted file.
🪄 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: CHILL

Plan: Pro

Run ID: 1ba4c428-7889-4b9a-a5dd-ad40285f2be2

📥 Commits

Reviewing files that changed from the base of the PR and between 3b93929 and 678b391.

📒 Files selected for processing (3)
  • backend/src/api/routes/auth/index.routes.ts
  • backend/src/infra/security/token.manager.ts
  • backend/tests/unit/auth-admin-routes.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/src/infra/security/token.manager.ts
  • backend/src/api/routes/auth/index.routes.ts

Comment on lines +67 to +83
async function postAdminRefresh(headers: Record<string, string>) {
const server = createAdminAuthApp().listen(0);
await new Promise<void>((resolve) => server.once('listening', resolve));

try {
const { port } = server.address() as AddressInfo;
const response = await fetch(`http://127.0.0.1:${port}/api/auth/admin/refresh`, {
method: 'POST',
headers,
});
await response.text();

return {
status: response.status,
setCookie: response.headers.get('set-cookie'),
};
} finally {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace global fetch in postAdminRefresh with a test-local request mechanism.

postAdminRefresh is currently the direct cause of CI failures (response is undefined at Line 77), which makes three regression tests fail before assertions run. Use supertest (or Node http.request) against the Express app to avoid global fetch interference in Vitest.

Proposed fix (Supertest-based helper)
+import request from 'supertest';
...
-async function postAdminRefresh(headers: Record<string, string>) {
-  const server = createAdminAuthApp().listen(0);
-  await new Promise<void>((resolve) => server.once('listening', resolve));
-
-  try {
-    const { port } = server.address() as AddressInfo;
-    const response = await fetch(`http://127.0.0.1:${port}/api/auth/admin/refresh`, {
-      method: 'POST',
-      headers,
-    });
-    await response.text();
-
-    return {
-      status: response.status,
-      setCookie: response.headers.get('set-cookie'),
-    };
-  } finally {
-    await new Promise<void>((resolve, reject) => {
-      server.close((error) => {
-        if (error) {
-          reject(error);
-          return;
-        }
-
-        resolve();
-      });
-    });
-  }
-}
+async function postAdminRefresh(headers: Record<string, string>) {
+  const app = createAdminAuthApp();
+  const response = await request(app).post('/api/auth/admin/refresh').set(headers).send();
+  const setCookieHeader = response.header['set-cookie'];
+
+  return {
+    status: response.status,
+    setCookie: Array.isArray(setCookieHeader) ? setCookieHeader.join('; ') : null,
+  };
+}
🧰 Tools
🪛 GitHub Actions: Unit Tests / 0_Unit Tests.txt

[error] 77-77: TypeError: Cannot read properties of undefined (reading 'text') in postAdminRefresh where the test awaits response.text().

🪛 GitHub Actions: Unit Tests / Unit Tests

[error] 77-77: Vitest test run failed in auth-admin-routes.test.ts (npm run test:backend > vitest run). TypeError: Cannot read properties of undefined (reading 'text') at postAdminRefresh; failing assertions at response.text().

🪛 GitHub Check: Unit Tests

[failure] 77-77: tests/unit/auth-admin-routes.test.ts > admin auth route review regressions > clears admin refresh cookies on auth-invalidating refresh failures
TypeError: Cannot read properties of undefined (reading 'text')
❯ postAdminRefresh tests/unit/auth-admin-routes.test.ts:77:20
❯ tests/unit/auth-admin-routes.test.ts:175:22


[failure] 77-77: tests/unit/auth-admin-routes.test.ts > admin auth route review regressions > preserves admin refresh cookies on CSRF rejection
TypeError: Cannot read properties of undefined (reading 'text')
❯ postAdminRefresh tests/unit/auth-admin-routes.test.ts:77:20
❯ tests/unit/auth-admin-routes.test.ts:161:22


[failure] 77-77: tests/unit/auth-admin-routes.test.ts > admin auth route review regressions > preserves admin refresh cookies on non-auth transient refresh failures
TypeError: Cannot read properties of undefined (reading 'text')
❯ postAdminRefresh tests/unit/auth-admin-routes.test.ts:77:20
❯ tests/unit/auth-admin-routes.test.ts:149:22

🤖 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 `@backend/tests/unit/auth-admin-routes.test.ts` around lines 67 - 83, The
helper postAdminRefresh currently uses global fetch which is being interfered
with in tests; replace it to perform a test-local request against the Express
app returned by createAdminAuthApp instead (use supertest or Node http.request)
so you don't start a real listener or depend on global fetch. Update
postAdminRefresh to accept headers, call createAdminAuthApp() and issue a POST
to /api/auth/admin/refresh via supertest (or create a local request against the
app), then return the response.status and response.headers['set-cookie']
(preserving the current return shape) and ensure the server is not bound to a
real port or global fetch is avoided.

@Fermionic-Lyu Fermionic-Lyu enabled auto-merge May 19, 2026 22:53
Copy link
Copy Markdown
Member

@jwfing jwfing left a comment

Choose a reason for hiding this comment

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

LGTM, approved.

@Fermionic-Lyu Fermionic-Lyu merged commit 67245c3 into main May 19, 2026
14 of 15 checks passed
@Fermionic-Lyu Fermionic-Lyu deleted the fix-auth-flow branch May 19, 2026 22:55
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.

2 participants