feat(auth): server-side Steward token refresh in edge proxy#461
feat(auth): server-side Steward token refresh in edge proxy#461
Conversation
Moves the refresh race out of client JS and into the edge proxy. When a protected request arrives with an expired access token and a valid refresh token cookie, the proxy calls Steward's /auth/refresh server-side, rotates both cookies, and forwards the request with the new bearer so downstream auth works on the same round trip. - proxy.ts: refresh flow with structured [steward-auth] logging, transient-failure tolerance (5xx/network errors log but don't redirect), 401 clears both cookies - app/api/auth/steward-session/route.ts: sets both steward-token and steward-refresh-token httpOnly cookies, DELETE clears both - packages/lib/providers/StewardProvider.tsx: syncs refresh token to the bridge alongside access token, drops visibilitychange handler (middleware handles return-from-idle now) - app/login/steward-login-section.tsx: minor body-shape update for the new session bridge POST signature - packages/tests/unit/steward-proxy-refresh.test.ts: coverage for success/401/5xx/network-error paths + same-request auth forwarding Fixes the 15-30 min silent-logout bug. Co-authored-by: wakesync <shadow@shad0w.xyz>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review: feat(auth) — server-side Steward token refresh in edge proxyThis is a well-structured fix for a real UX problem. The architecture (refresh in middleware, same-request bearer forwarding, transient-failure tolerance) is sound. A few issues worth addressing before merge. Bugs / Correctness
// proxy.ts
function getStewardDeleteCookies(): string[] {
return ["steward-token", "steward-refresh-token", "steward-authed"];
}Zero-TTL access token sets an immediately-expired cookie
maxAge: typeof tokenTtl === "number" && tokenTtl > 0 ? tokenTtl : undefined,(The condition is already there in SecuritySoft-fail on invalid refresh payload is too permissive In Refresh token still lives in
refreshToken ?? (typeof window !== "undefined" ? localStorage.getItem("steward_refresh_token") : null)The httpOnly cookie is the secure store; localStorage is XSS-accessible. Now that the httpOnly cookie is the authoritative path, it's worth tracking whether the localStorage copy can be dropped in a follow-up — it's currently necessary for the OAuth callback flow, but it's extra attack surface. Observability
Minor
The refactored Test: The last test ( What's working well
The core design is good. The |
Auto-fix from biome check --write. Co-authored-by: wakesync <shadow@shad0w.xyz>
Code Review: feat(auth): server-side Steward token refresh in edge proxyOverall: This is a solid fix for the silent-logout bug. The architecture is correct — moving refresh into the edge proxy eliminates the client race condition. The code is well-structured with good test coverage for the main paths. A few issues worth addressing before merge. Bugs1. In // proxy.ts (tryRefreshStewardSession)
logStewardAuth("skipped", ttl, { reason: "missing-refresh-token" });
return { kind: "401" };Suggest logging 2.
3. Empty refresh token accepted from Steward API if (typeof body.token !== "string" || typeof body.refreshToken !== "string") {An empty string passes this check. Security4. Refresh token in localStorage
Code Quality5. // proxy.ts AND route.ts — both have:
let stewardAuthMetricCounter = 0;In serverless/edge environments, this counter resets per cold start and is per-instance. The metric will drift unpredictably and can't be aggregated. If this is intended for operational visibility, use a structured log field instead of an incrementing counter, or at minimum add a comment explaining the limitation. Also, the same 6. Old code guarded on if (options?.requestHeaders) {
options.requestHeaders.forEach(...); // sets headers
headers.set("x-middleware-override-headers", ...); // always set
}If an empty 7.
TestsGood coverage for the four proxy paths. A few gaps:
Minor
Summary: Fix the |
Co-authored-by: wakesync <shadow@shad0w.xyz>
Code Review — feat(auth): server-side Steward token refresh in edge proxySummary: This PR fixes a meaningful UX bug (silent logout after ~30 min idle) by moving token refresh from client JS into the edge proxy. The approach is sound — storing the refresh token in a second What works well
Issues1.
|
TypeScript strict mode rejected Mock<() => Promise<Response>> as typeof fetch without an intermediate unknown cast (preconnect is missing). Fix test file to: - cast all globalThis.fetch mocks via 'as unknown as typeof fetch' - type Array.some/every/find callback params as string (previously implicit any) - cast fetchMock.mock.calls[0] as unknown[] to avoid tuple length 0 error Unit tests continue to pass (5/5). Co-authored-by: wakesync <shadow@shad0w.xyz>
Code Review: feat(auth) — server-side Steward token refresh in edge proxyGood overall approach. Moving refresh out of client JS and into the edge middleware is the right call for solving the idle-logout race condition. The diff is clean and the PR description documents edge cases honestly. A few issues worth addressing before merge: Critical / High
// proxy.ts — getStewardDeleteCookies
function getStewardDeleteCookies(): string[] {
return ["steward-token", "steward-refresh-token", "steward-authed"]; // add this
}Note: the
if (!refreshToken) {
logStewardAuth("skipped", ttl, { reason: "missing-refresh-token" });
return { kind: "401" }; // ← treated same as a real 401 from Steward
}Existing users who logged in before this PR will have no Recommend returning a distinct MediumModule-level // proxy.ts
let stewardAuthMetricCounter = 0;
// route.ts
let stewardAuthMetricCounter = 0;Edge middleware isolates can handle concurrent requests in the same V8 isolate. The counter is mutation-only (no read–modify–write race that changes behavior), so it won't corrupt business logic, but the metric will increment non-deterministically under load. If this is meant as a request count for observability, use
The
Low / Style
The helper is defined but called in only one place (
Called on every TestsThe four test paths (success, 401, 5xx, network error) are solid. The same-request auth forwarding test directly validates the
Summary
The cookie clear and missing-refresh-token issues are the most likely to cause visible user impact at deploy time. |
Fixes the 15-30 min silent-logout bug.
The bug
Users get logged out after ~30 min of idle. Return to tab, refresh, bounced to /login. Client-side refresh (visibilitychange, eager refresh on mount, login-page recovery) was insufficient — race between middleware checking cookies and client JS waking up.
The fix
Move refresh out of client JS and into the edge proxy. Store the refresh token in a second httpOnly cookie (
steward-refresh-token, 30d). When middleware sees expired access token + valid refresh cookie → call Steward's/auth/refreshserver-side, rotate both cookies, forward the fresh bearer on the SAME request. User never sees a login page for the refresh-token-still-valid case.Changes
proxy.ts: refresh flow with structured[steward-auth]logging, transient-failure tolerance (5xx/network log but don't redirect), 401 clears cookies. Forwards the new bearer on the same request viax-middleware-request-authorizationheader so downstream auth works on the same round trip (no hop to client).app/api/auth/steward-session/route.ts: accepts{ token, refreshToken }, sets both httpOnly cookies. DELETE clears both.packages/lib/providers/StewardProvider.tsx: syncs refresh token alongside access token, drops aggressive visibilitychange handler (middleware handles return-from-idle now).app/login/steward-login-section.tsx: minor body-shape update for new POST signature.packages/tests/unit/steward-proxy-refresh.test.ts: unit coverage for success/401/5xx/network-error paths + same-request auth forwarding.Env vars
STEWARD_API_URL(server-side, for proxy refresh) — falls back toNEXT_PUBLIC_STEWARD_API_URL, final fallbackhttps://eliza.steward.fiNEXT_PUBLIC_STEWARD_API_URLstill used by client providerKnown edge cases (documented)
expis treated as usable (refresh skipped) — rare but worth flaggingsteward-token+steward-refresh-tokenonly, notsteward-authedTests
Bun unit tests cover the four proxy paths. Run in CI.
Co-authored-by: wakesync shadow@shad0w.xyz