Make MFA token redemption atomic to prevent multiple sessions#49359
Make MFA token redemption atomic to prevent multiple sessions#49359dantecatalfamo wants to merge 2 commits into
Conversation
The MFA token redemption path (POST /api/latest/fleet/sessions) read the one-time verification token with a non-locking SELECT on the read replica, then created a session and deleted the token in a separate transaction without verifying the token was still present. Concurrent requests with the same token each passed the SELECT and each minted a distinct session, breaking the single-use guarantee (Aikido PT-30). SessionByMFAToken now consumes the token and creates the session inside one transaction: the token row is locked with SELECT ... FOR UPDATE, deleted, and the delete's rows-affected is confirmed to be non-zero before the session is created. Concurrent redemptions serialize on the row lock, and the loser finds no row and aborts. The user is still loaded before the transaction so a missing user or transient read error leaves the token intact for retry. Adds a datastore test that fires concurrent redemptions of one token and asserts exactly one session is created. Claude-Session: https://claude.ai/code/session_01JFBwsx8rkiHXnPPRCNGEg1
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #49359 +/- ##
=======================================
Coverage 67.97% 67.97%
=======================================
Files 3823 3823
Lines 241031 241120 +89
Branches 12702 12702
=======================================
+ Hits 163831 163904 +73
- Misses 62325 62338 +13
- Partials 14875 14878 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
This PR fixes a race in MFA one-time token redemption by making token consumption and session creation atomic, preventing multiple concurrent POST /api/latest/fleet/sessions requests from minting multiple sessions with the same token.
Changes:
- Update
SessionByMFATokentoSELECT ... FOR UPDATEthe verification token row inside a transaction, delete it, verify rows affected, then create the session. - Add a concurrency regression test that attempts multiple concurrent redemptions of the same MFA token and asserts only one session is minted.
- Add a user-visible changelog entry (content excluded by policy).
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| server/datastore/mysql/sessions.go | Make MFA token redemption atomic with row locking + delete rows-affected verification before session creation. |
| server/datastore/mysql/sessions_test.go | Add concurrency test ensuring only one redemption succeeds and the token is consumed. |
| changes/16770-mfa-token-race | User-visible changelog entry (content excluded; not reviewed). |
Files excluded by content exclusion policy (1)
- changes/16770-mfa-token-race
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Consume the token before creating the session, and confirm exactly one | ||
| // row was deleted. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughMFA token redemption now locks and rechecks the verification token within the transaction, deletes it before session creation, verifies the delete affected a row, and creates the session using the locked token’s user ID. Tests concurrently redeem the same token and assert that exactly one request succeeds, one session is created, and subsequent reuse fails. The change note documents the atomic redemption behavior. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Related issue: Resolves https://github.com/fleetdm/confidential/issues/16770 (Aikido-PT-30)
Summary
The MFA login token redemption path (
POST /api/latest/fleet/sessions) read the one-time verification token with a non-lockingSELECTon the read replica, then created a session and deleted the token in a separate transaction without verifying the token was still present. Concurrent requests carrying the same token each passed theSELECTand each minted a distinct session, breaking the single-use guarantee.SessionByMFATokennow consumes the token and creates the session inside a single transaction:SELECT ... FOR UPDATE, then deleted, and the delete's rows-affected count is confirmed non-zero before the session is created.Checklist for submitter
Changes file added for user-visible changes in
changes/.Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Testing
server/datastore/mysql/sessions_test.gogains a concurrency case intestMFAthat fires 8 concurrent redemptions of one token and asserts exactly one session is created and the token is consumed. Verified this test fails (8 sessions created) against the pre-fix code and passes with the fix.Manual test plan for reviewer
To confirm it exercises the race, temporarily revert
SessionByMFATokento its previous body (create session, then delete token, noFOR UPDATE/rows-affected check) and re-run — the concurrency case fails with "exactly one concurrent redemption should succeed: expected 1, actual 8".Summary by CodeRabbit