uttrflow / uttrflow/uttrflow-swift
Concurrent account and avatar reads spend the same rotating refresh token twice
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## Problem
`authorised()` checks the access token under a mutex, then independently awaits `renew()`. The network renewal itself is not shared or serialized. Two initial readers can submit the same refresh token concurrently. The session contract says the previous refresh token becomes invalid after rotation; a refused competing refresh calls `forgetSession()` and can discard the session the successful request just stored.
## Evidence
Reviewed `main` at `8d93f5b8574f3c7b8c30086d9c817bf7f5d4827d`. Reproduced against the unchanged production source in an isolated Swift package on macOS, with fake transport or temporary stores as appropriate. These checks assert the observed defect; they are not a claim that the full app test suite passed.
Use a paused fake `BackendTransport`, an `InMemoryTokenStore` holding `old-example`, and no cached access token:
1. Start `currentProfile(ifChangedFrom: nil)`; suspend its `/refresh` response.
2. Start `avatar(at: "/v1/me/avatar")` before releasing the first response.
3. Two `/refresh` requests arrive, both with `{"refreshToken":"old-example"}`.
This reproduces concurrent spending, without live credentials. The exact server-side refusal ordering is not exercised by this fake transport. Launch invokes `refreshAccount()` and shows the main window, whose refresh can request an avatar, so these operations are not restricted to sequential test calls.
## Relevant code
- [Sources/UttrflowAccount/HTTPAuthenticationService.swift:350](https://github.com/uttrflow/uttrflow-swift/blob/8d93f5b8574f3c7b8c30086d9c817bf7f5d4827d/Sources/UttrflowAccount/HTTPAuthenticationService.swift#L350)
- [Sources/UttrflowAccount/HTTPAuthenticationService.swift:360](https://github.com/uttrflow/uttrflow-swift/blob/8d93f5b8574f3c7b8c30086d9c817bf7f5d4827d/Sources/UttrflowAccount/HTTPAuthenticationService.swift#L360)
- [Sources/Uttrflow/AppDelegate.swift:219](https://github.com/uttrflow/uttrflow-swift/blob/8d93f5b8574f3c7b8c30086d9c817bf7f5d4827d/Sources/Uttrflow/AppDelegate.swift#L219)
- [Docs/account-session.md:24](https://github.com/uttrflow/uttrflow-swift/blob/8d93f5b8574f3c7b8c30086d9c817bf7f5d4827d/Docs/account-session.md#L24)
## Fix and acceptance criteria
Share one in-flight renewal per session and have all callers await it. Include a session generation so obsolete responses cannot replace a newer session. Test overlapping profile/avatar requests with a rotating-token transport: exactly one refresh request, both callers use its new access token, and no spurious sign-out.
## Existing work checked
#756 covers branches after token rejection, not concurrent refresh-token rotation.
Contributor guide
Research direction
Start in Sources/UttrflowAccount/HTTPAuthenticationService.swift around authorised() and renew(), then read the session contract in Docs/account-session.md. Review the launch path in Sources/Uttrflow/AppDelegate.swift and reproduce overlapping profile/avatar requests with a paused or rotating-token transport. Done means one shared refresh, both callers using the new access token, and no spurious sign-out.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- authentication, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100