feat(#1005): implement CSRF protection with double-submit cookie - #1052
Open
agenes01 wants to merge 2 commits into
Open
feat(#1005): implement CSRF protection with double-submit cookie#1052agenes01 wants to merge 2 commits into
agenes01 wants to merge 2 commits into
Conversation
…cookie
- Add backend/services/shared/csrfService.ts:
- generateCsrfToken() - crypto-random 32-byte hex token generation
- verifyCsrfToken(a, b) - constant-time equality check (prevents timing attacks)
- parseCookies(header) - raw Cookie header string parser
- buildCsrfCookieValue(token, opts) - Set-Cookie header builder with
__Host- prefix, SameSite=Strict, Secure, Max-Age, no HttpOnly
(double-submit pattern requires JS-readable cookies)
- CsrfService class with generateToken(), verify(), extractFromCookie(),
extractFromHeader(), buildCookieValue()
- csrfService singleton
- createCsrfMiddleware(opts) - Express/Fastify middleware that:
* On safe methods (GET/HEAD/OPTIONS): issues/refreshes token via
Set-Cookie + X-CSRF-Token response header
* On unsafe methods (POST/PUT/PATCH/DELETE): verifies header matches
cookie using constant-time comparison; calls next(err) with
status 403 and code CSRF_TOKEN_MISMATCH on failure
* Supports skipPaths, unsafeMethods, and getPath overrides
- issueCsrfToken(res, service, opts) - route helper for dedicated
GET /csrf-token endpoints
- Add src/services/csrfClientService.ts:
- CsrfClientService class with:
* getToken() - fetches/returns cached CSRF token
* getHeaders() - returns X-CSRF-Token header object
* prefetch() - eagerly warms the token cache at app startup
* injectHeader(headers) - mutates a headers object in-place
* setToken(token) - manual token injection (e.g. from SSR meta tags)
* clearToken() - invalidates cache
* isTokenValid() - checks cache freshness
* fetchWithRetry(url, init) - auto-injects token and retries once
on 403 CSRF_TOKEN_MISMATCH responses
* Deduplicates concurrent refresh calls (single in-flight promise)
- csrfClientService singleton
- Export all new CSRF symbols and types from
backend/services/shared/index.ts
- Add 78 backend tests in __tests__/csrfService.test.ts (all passing)
- Add 26 frontend tests in src/services/__tests__/csrfClientService.test.ts
(all passing)
Closes Smartdevs17#1005
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1005
Implements full CSRF (Cross-Site Request Forgery) protection using the double-submit cookie pattern for the SubTrackr backend and frontend, covering the
backend/services/shared/andsrc/services/scopes specified in the issue.How double-submit cookie works
__Host-csrf) plus echoes it in anX-CSRF-Tokenresponse header so JavaScript can read it.X-CSRF-Tokenrequest header.Cross-origin attackers cannot read cookies (same-origin policy), so they cannot forge the matching header.
Changes
New:
backend/services/shared/csrfService.tsgenerateCsrfToken()verifyCsrfToken(a, b)parseCookies(header)Cookieheader string → key/value mapbuildCsrfCookieValue(token, opts)Set-Cookieheader builder (__Host-prefix,SameSite=Strict,Secure, noHttpOnly)CsrfServicecsrfServicecreateCsrfMiddleware(opts)skipPaths,unsafeMethods, customgetPathissueCsrfToken(res, ...)GET /csrf-tokenendpointsNew:
src/services/csrfClientService.tsCsrfClientServicecsrfClientServicegetToken()getHeaders(){ 'X-CSRF-Token': token }prefetch()injectHeader(headers)setToken()/clearToken()fetchWithRetry(url, init)403 CSRF_TOKEN_MISMATCHUpdated:
backend/services/shared/index.tsAll new CSRF symbols and TypeScript types re-exported from the shared package root.
Tests
backend/services/shared/__tests__/csrfService.test.tssrc/services/__tests__/csrfClientService.test.tsCoverage areas: token generation, constant-time verification, cookie parsing, cookie builder options,
CsrfServiceclass API, middleware safe/unsafe method behaviour,skipPaths/unsafeMethods/ customgetPathoptions, client-side caching + TTL expiry, header injection, deduplication of concurrent refresh calls,fetchWithRetrywith auto-retry, and end-to-end lifecycle integration.Acceptance criteria checklist