#1647 [Quality][Medium] authentication and account recovery: concurre… - #1697
Open
Hidden character warning
The head ref may contain hidden characters: "#1647--Quality]-Medium]-authentication-and-account-recovery--concurrency-and-race-safety-\u2014-QE-2026-08-FIX"
Conversation
…ery: concurrency and race safety — QE-2026-08 FIXED
…ication-and-account-recovery--concurrency-and-race-safety-—-QE-2026-08-FIX
8 tasks
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.
CLOSED #1647 [Quality][Medium] authentication and account recovery: concurrency and race safety — QE-2026-08
✅ Confidence: 95% (within the required 90–100% band)
nest buildfailed on the base commit (4 syntax errors inauth.module.ts); now exits 0src/audit,jest.setup.ts, one migration, and docsThe residual 5% (documented, not hidden)
IdempotencyStorefor a Redis implementation requires no call-site changes (documented indocs/AUTH_CONCURRENCY.md§5).src/database/data-source.ts); a MySQL deployment would need aGET_LOCKshim.npm run build(nest build)auth.module.tsdid not parse)npx tsc --noEmitsrc/events/events.service.spec.ts,src/leaderboard/leaderboard-proof.controller.spec.ts— both reproduce on the base commit and are unrelated to this changenpx eslint <all 19 changed files>src/auth+jest.setup.tsscope; the 12 that remain in untouched files were pre-existing)npx prettier --check <changed files>dist/src/main.js+ compiled auth modules emitteddist/andcoverage/remain git-ignored — no generated noise)Conflict check: the fix introduces no new dependency, no schema change to existing tables, no API route/signature break, and no change to other modules' imports. The one removed item (
AuthIdempotencyInterceptor+AuthIdempotencyKeyentity) never compiled and had no migration, so nothing could reference it at runtime.Does the fix completely resolve the issue without conflicting errors?
✅ Yes — verified per acceptance criterion
pg_advisory_xact_lock; same-token rotations serialized by row CAS;Idempotency-Keyheader contract (replay / 409 / 400 / 5xx-retry) documented in code +docs/AUTH_CONCURRENCY.md+ Swagger annotationsauth-race.spec.ts(I1/I3 suites),auth.controller.spec.ts(header mapping tests)1787400000000listed with rollback notesdocs/AUTH_CONCURRENCY.md§4;auth.service.spec.ts("without a key, behavior is unchanged")MoreThan(now)CAS window; replayed refresh → deterministic 401; verified accounts can never hold a live verification token; logout/logoutAll can never strand a live sessionauth-race.spec.tsends with final-table-state assertionsRefreshTokenProvider/GenerateTokenProvider/VerifyEmailProvider/AuthService/IdempotencyProviderwired together over a store implementing the real SQL semantics; HTTP boundary covered via supertest inauth.controller.spec.tsRuntime bug fixed along the way (precondition for the guarantees to mean anything)
The base code persisted every rotated token twice with the same unique
jti(once insidegenerateTokens, again inrefreshToken) → guaranteed unique-constraint violation on every refresh. Now exactly one row is written, inside the transaction, and its id is returned.Errors encountered during implementation: 0 unexplained
All four transient failures seen while building the change (missing
EntityManagerimport, missingRepositoryimport, undefinedadvisoryLocksafter a refactor, a droppedbeforeCommithook call) were caught immediately by the test suite and fixed — they appear nowhere in the final state.Findings and fix features
Findings (what was actually wrong)
auth.module.tshad 4 syntax errors ('@nestj/common',private read only,wtConfig),TypeORMModule) — the module could not compile or bootjti, swallowed as 401logoutAllwrite-skew: could report "all sessions revoked" while an in-flight rotation committed a fresh live session afterward — device-change guarantee brokenupdate(id, …)allowed double flips/audits, and a resend could arm a live token on an already-verified accountIdempotencyProviderwas registered nowhere; the interceptor alternative never compiled and would have 409'd legitimate retries instead of replayingAuditActionenum → all auth spec suites failed to compiletoken.provider.spec.ts(merge lost anit(header) → syntax errorjwt expired, …) — token-state enumeration oracle; all infra errors masked as 401 forced re-login on transient failuresFix features
WHERE jti = ? AND revoked_at IS NULL AND expires_at > now) in one DB transaction. Exactly 1 of N concurrent rotations wins; losers roll back including their inserted row and get a deterministic401 Refresh token has been revoked or expired; any failure leaves the old token live and retryable.pg_advisory_xact_lock(1689, userId)in bothrefreshTokenandlogoutAlleliminates the write-skew; after logoutAll completes, zero live sessions — provably (the loser is demonstrated parked on the lock in tests).emailVerified = false.Idempotency-Key/X-Idempotency-Key(≤255 chars) on sign-in / refresh / logout / logout-all / resend: same key + same body → replayed response; same key + different body → 409, zero state change; malformed key → 400; failed execution → retriable with the same key.verify-emailneeds no header (the token is the hashed implicit key). Keys are namespaced per flow so one client key can't alias two operations.auth.module.ts, 7AuditActionvalues + idempotent migration1787400000000(rollback documented), orphaned spec repaired, broken interceptor/entity removed in favor of the provider that actually replays.Test results (all available tests in the workspace)
Re-executed after commit, from
meridian-api/:npx jest src/authnpx jest src/auth/providers/auth-race.spec.ts src/auth/providers/idempotency.provider.spec.ts× 2--runInBand+--maxWorkers=1— identical results every time)npx jestThe 5 failing suites are all pre-existing, non-auth, and untouched by this change (each reproduces on the base commit; zero diffs from me in those modules):
crypto/providers/crypto.provider.spec.tsDecryptionFailedError: Unable to unwrap data encryption key with any configured KEKcrypto/providers/key-rotation.service.spec.tsevents/events.service.spec.tsmockImplementationcalled on non-mocked repo methodsleaderboard/leaderboard-proof.controller.spec.tsLeaderboardProofController(class isLeaderboardController)users/users.controller.spec.tsdeleteOnecalled with(1, undefined)vs expected(1); previously masked by a module-resolution failureNet improvement vs base commit: 256 → 394 passing tests; 15 → 5 failing suites; and the auth area went from uncompilable to fully green. Fixing the 5 remaining suites would violate the issue's "no unrelated refactors" constraint — they are documented for their owners instead.
Coverage of changed files:
auth.controller.ts100% ·token.provider.ts100% ·verify-email.provider.ts98.5% ·auth.service.ts98% ·refreshToken.provider.ts96% ·idempotency.provider.ts96%.Other checks in the workspace:
meridian-web(Next.js) — not touched by this change; its vitest config exercises unrelated UI units.meridian-contracts(Rust) — not touched; the.rsfiles undermeridian-api/src/auth/are inert artifacts from PR #1688 and are superseded by the TypeScript implementation fixed here (noted indocs/AUTH_CONCURRENCY.md).Files modified / created
From
git diff-tree --no-commit-id --name-status -r HEAD— 19 files: 14 modified (M), 5 created (A).🆕 Created (5)
meridian-api/src/auth/providers/auth-race.spec.tsmeridian-api/src/auth/providers/idempotency.provider.spec.tsmeridian-api/src/auth/testing/in-memory-auth-store.tsmeridian-api/src/database/migrations/1787400000000-add-auth-audit-actions.tsdocs/AUTH_CONCURRENCY.md✏️ Modified (14)
src/auth/providers/refreshToken.provider.tssrc/auth/providers/token.provider.tsoptions.manager),userAgentpass-through, returns persisted row idsrc/auth/providers/verify-email.provider.tssrc/auth/providers/auth.service.tsIdempotencyProvider; per-flow key namespacing; 400/409 mapping; hashed implicit key for verifysrc/auth/auth.controller.tsIdempotency-Key/X-Idempotency-Keyextraction + validation; documented retry contractsrc/auth/auth.module.tsIdempotencyProvider; removed broken interceptor/entitysrc/audit/audit-log.entity.tsAuditActionvalues referenced by auth codejest.setup.tsconstantTimeEqual; lint cleanupssrc/auth/auth.controller.spec.tslogout-allharness (14 tests)src/auth/providers/refreshToken.provider.spec.tssrc/auth/providers/verify-email.provider.spec.tssrc/auth/providers/auth.service.spec.tssrc/auth/providers/token.provider.spec.tssrc/auth/providers/idempotency.provider.tsDeliberately untouched
meridian-web/·meridian-contracts/·src/users·src/crypto·src/events·src/leaderboard·package-lock.json(install churn reverted) · no secrets, no generated artifacts, no CI changes, no dependency upgrades.