uttrflow / uttrflow/uttrflow-swift

Concurrent account and avatar reads spend the same rotating refresh token twice

Open
#771 0 comments 0 reactions 0 assignees View on GitHub
area:account bug P1
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.