fix(auth): derive stable CSRF key from encryption key (closes #1120) - #1121
Conversation
Production built the auth service with no CSRFKey, so NewService minted a random per-process key. CSRF tokens are HMAC-SHA256(csrfKey, rawSessionToken) minted at login and validated on every write, so a token issued by one Lambda instance was rejected by another instance (and by the same instance after a cold-start). This surfaced as intermittent "CSRF validation failed" on plan create and admin user-group updates for both Standard and Admin users. Derive a stable CSRFKey from the deploy-provided credential encryption key via HKDF-SHA256 with a domain-separation label, so every instance and every cold-start derives the same key. The encryption key is already mandatory in production (the app refuses to start without it), so this fails closed without requiring a new secret. The key load is memoized via sync.Once, so seeding the CSRF key before the credential store adds no extra work. Add a cross-instance regression test: a token minted by instance A validates on instance B that shares the same master secret, and is rejected by an instance built from a different secret (the pre-fix random-key situation). DeriveCSRFKey also fails closed on an empty master secret.
|
Warning Review limit reached
More reviews will be available in 48 minutes and 50 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Independent adversarial re-review — VERIFIED-CORRECT ✅Traced the full request lifecycle against the committed head ( Routing: every write goes through the fixed-key serviceThe cold-start placeholder auth service (
So the placeholder/random-key service can never serve a real write. The only ensureDB-skipping path is HKDF + fail-closed
Validation/mint unchanged
Test red→green
Note (acceptable)Tokens minted before deploy (old random key) will fail until re-login — inherent to any CSRF-key rotation, one-time per user. Merge-readinessCode is merge-ready. CI: sanity green. Caveat: the CodeRabbit "pass" is a rate-limit auto-reply, not a substantive review — CR hit the per-developer rate limit (reviews available ~01:00 UTC). A real Verdict: VERIFIED-CORRECT. |
|
Your plan includes 5 PR reviews per hour. More reviews will be available in 26 minutes and 18 seconds. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Extract CSRF key derivation and auth.Service construction from reinitializeAfterConnect into a new (*Application).initAuthService helper. Reduces reinitializeAfterConnect cyclomatic complexity from 11 to 10 (project limit), with initAuthService at 3. No behavior, ordering, error handling, or fail-closed semantics changed.
|
Gocyclo fix landed in a7db5e1.
Verified locally:
@coderabbitai review |
|
✅ Action performedFull review finished. |
What
Fixes intermittent "CSRF validation failed" on deployed write operations (plan create, admin user-group update), for both Standard and Admin users. Closes #1120.
Root cause
Production constructed the auth service with no
CSRFKey, soauth.NewServicefell back to a random ephemeral key per process (internal/auth/service.go:107-118). CSRF tokens areHMAC-SHA256(csrfKey, rawSessionToken), minted at login and validated on every write. On a multi-instance Lambda fleet (and across cold-starts), a token minted by instance A was rejected by instance B because each had a different key. The frontend fetches the token once at login and reuses it (X-CSRF-Token), so it is only stable if the server key is stable.Both production construction sites omitted
CSRFKey:internal/server/app.go:460(cold-start placeholder) and:691(the request-serving service inreinitializeAfterConnect).Fix
Derive a stable
CSRFKeyfrom the deploy-provided credential encryption key (already mandatory in production) via HKDF-SHA256 with a domain-separation label (auth.DeriveCSRFKey), and pass it into the request-serving auth service. Every instance and cold-start now derives the same key, so a token minted on one instance validates on another.DeriveCSRFKeyalso rejects an empty master secret.sync.Once), so seeding the CSRF key before the credential store adds no extra work; it's loaded once and reused.Verification (red -> green)
TestCSRFKey_CrossInstanceStableasserts the cross-instance invariant: a token minted by instance A validates on instance B sharing the same master secret, and is rejected by an instance built from a different secret.CSRFKey-> random keys) made cross-instance validation fail (the exact QA symptom); with the stable derived key it passes. Temporary sim removed.go build ./...clean;go vetclean;go test ./internal/auth/ ./internal/server/= 868 passed.Notes
The unit test
TestAuthServiceAdapter_ValidateCSRFTokenreported as failing on base was already fixed on the currentfeat/multicloud-web-frontendtip (the mock pins a fixedCSRFKey); it passes here. The remaining and real defect was the production wiring, fixed by this PR.