-
Notifications
You must be signed in to change notification settings - Fork 0
Tech Story: Switch auth tokens to httpOnly cookies #93
Copy link
Copy link
Closed
Labels
apiPublic/internal API endpointsPublic/internal API endpointsbackendBackend services and logicBackend services and logicfrontendFrontend app and dashboardFrontend app and dashboardsecuritySecurity, auth, and permissionsSecurity, auth, and permissionstech-storyTechnical implementation storyTechnical implementation story
Milestone
Metadata
Metadata
Assignees
Labels
apiPublic/internal API endpointsPublic/internal API endpointsbackendBackend services and logicBackend services and logicfrontendFrontend app and dashboardFrontend app and dashboardsecuritySecurity, auth, and permissionsSecurity, auth, and permissionstech-storyTechnical implementation storyTechnical implementation story
Tech Story
As a platform engineer, I want access and refresh tokens stored in httpOnly cookies so that they cannot be read or exfiltrated by JavaScript, eliminating the XSS attack surface introduced by localStorage.
Context
Currently both tokens are stored in
localStorageand injected manually intoAuthorization: Bearerheaders. Any XSS vector (compromised dependency, injected script) can trivially steal both tokens. Storing tokens inhttpOnly; Secure; SameSite=Strictcookies removes this surface entirely — the browser sends them automatically and JS cannot read them.Acceptance Criteria
POST /auth/loginsetsaccess_tokenandrefresh_tokenashttpOnly; Secure; SameSite=Strictcookies; response body returns user info only (no tokens)POST /auth/refreshreads the refresh token from its cookie, issues new token pair as cookiesPOST /auth/logoutreads the refresh token from its cookie, revokes it, clears both cookiesJwtStrategyextracts the access token from theaccess_tokencookie (not Authorization header)RefreshTokenStrategyandRefreshTokenAuthGuardremoved — refresh/logout read cookies directlyapiClientsendswithCredentials: true; all manual token reads/writes from localStorage removedProtectedRouteusesAuthContext(backed byGET /auth/me) instead oflocalStoragecheckGET /auth/meendpoint added — returns{ id, username }for authenticated users, 401 otherwiseapi.service.ts(hardcoded wrong port, unused) deletedTechnical Elaboration
cookie-parsermiddleware inmain.ts; installcookie-parserand@types/cookie-parserhttpOnly: true,secure: true(env-gated for local dev),sameSite: 'strict'JwtStrategy: changejwtFromRequesttoExtractJwt.fromExtractors([(req) => req?.cookies?.access_token])AuthContext(React): callsGET /auth/meon mount; exposes{ user, loading, logout };ProtectedRoutereads from contextapiClient401 interceptor: callPOST /auth/refreshwithwithCredentials: true; on success retry original request; on failure redirect to /logincredentials: trueand explicitorigin(wildcard*is incompatible with credentialed cookies)Notes
secureflag should beNODE_ENV === 'production'so local dev over HTTP still works