Repository navigation
Revoke every refresh token when a used one is replayed - #135
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughRefresh-token replay now revokes all refresh tokens held by the user. The handler returns 500 if revocation fails. Tests and documentation cover replay, concurrent rotation loss, and the effect on existing access tokens. ChangesRefresh-token replay revocation
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Client
participant RefreshHandler
participant RefreshStore
Client->>RefreshHandler: Submit a spent refresh token
RefreshHandler->>RefreshStore: Delete all refresh tokens for the user
RefreshStore-->>RefreshHandler: Return deletion result
RefreshHandler-->>Client: Return 401, or 500 if deletion failed
Merge Risk: ⚪ Minimal · up to Replay revokes the affected user’s refresh sessions, and a revocation failure is reported as a server error. No actionable merge-blocking risk remains after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change improves replay containment and limits deletion to the token owner's refresh sessions. Account-wide invalidation on benign retries is deliberate, and already-issued access tokens remain usable until expiry. The core rotation and deletion ordering is supported, but overlapping fresh logins, client recovery behavior, and deployment conditions are not fully demonstrated. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 3 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches🧪 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 |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Thanks, Divesh. Revoking on both the UsedAt != nil path and the lost-race path is right, since the lost race is the case where a thief could otherwise keep a fresh token. Returning 500 when revocation fails, so a retry tries again, is a careful touch. Merging.
One thing to settle before the Android app starts refreshing: with no grace window, a client that retries a refresh after losing the response logs the driver out everywhere. Please document single-flight refresh for client authors. That's a follow-up, not a blocker.
Before this, replaying a used refresh token got a 401 and a log line, and the thief's copy kept working.
Now a replay revokes every refresh token the user holds, through the existing DeleteRefreshTokensForUser. I chose that over a per-login family id because only the Android app and the simulator use refresh tokens, and a driver has one device, so both are the same set. A family id would cost a migration for no gain here.
The handler finds a spent token in two places and both revoke: the UsedAt check, and the !rotated branch where a concurrent request won the compare-and-set. Revoking only in the first leaves a hole. A thief racing the real client can win the rotation and get token B, the client logs in again, and nobody ever replays B, so it is never revoked. This only works because #129 made DeleteRefreshTokensForUser take the user lock, so a revoke that lands mid-rotation deletes the replacement too.
A failed revoke returns 500, not 401. A 401 would send the real client to log in again while the thief's copy survived, and a retry after a 500 tries the revocation again. Otherwise the caller sees the same 401 as before, and the revocation is logged at Warn with sub and ip.
Out of scope: the thief's current access token works until it expires (24h today), since the server doesn't know which jti the thief holds.
Mutation checks: removing the revoke from the UsedAt branch fails TestHandleRefresh_RejectsReusedToken and TestRefresh_ReplayRevokesEveryToken, removing it from the !rotated branch fails TestHandleRefresh_ConcurrentLossRevokes, and returning 401 on a failed revoke fails TestHandleRefresh_ReuseRevokeFailureIs500 (one subtest per branch).
go build, vet, fmt and test are clean, and with DATABASE_URL set the store and refresh tests ran (135 passed, 0 skipped). #134 isn't merged, so CI skips the new DB test until it is.
Part of #118
Summary by CodeRabbit