Skip to content

fix(desktop): force token refresh when Docker Desktop returns an expired JWT#3773

Merged
dgageot merged 3 commits into
docker:mainfrom
dgageot:fix/desktop-token-refresh
Jul 21, 2026
Merged

fix(desktop): force token refresh when Docker Desktop returns an expired JWT#3773
dgageot merged 3 commits into
docker:mainfrom
dgageot:fix/desktop-token-refresh

Conversation

@dgageot

@dgageot dgageot commented Jul 21, 2026

Copy link
Copy Markdown
Member

Docker Desktop's auth v2 stack (behind the SIGN_IN_V2 feature flag) never refreshes the JWT it holds on a GET /registry/token request — it returns whatever its in-memory token source currently contains. When Desktop's background refresher stalls on a permanent 4xx (e.g. a PAT/hub token error), it keeps serving the same expired JWT indefinitely, and retrying the endpoint from docker-agent's side never helps.

desktop.GetToken now parses the JWT exp claim (with 30s clock-skew leeway). When the token is expired, it forces a Desktop-side refresh by POSTing /registry/credstore-updated — the same hook the Docker CLI triggers after docker login, which makes Desktop reload its session from the OS credential store and issue a fresh token — then polls GET /registry/token until a non-expired token appears. A single detached goroutine performs the refresh under a hard 10s budget; concurrent callers share its result via singleflight and can bail on their own context cancellation, falling back to the stale token (preserving the previous behavior). After a successful refresh a 30s cooldown applies; after failure, a 2-minute backoff. Non-JWT tokens, empty tokens, and tokens without an exp claim pass through unchanged. raw_client.go gains a Post method to support the new endpoint.

Tests use an in-memory fake Desktop backend over net.Pipe and cover refresh success and failure, backoff, cached-result reuse, 8-way concurrent refresh sharing, caller cancellation, and non-JWT/empty token pass-through.

@dgageot
dgageot requested a review from a team as a code owner July 21, 2026 15:55

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

1 medium-severity finding in the new forced-refresh path.

Lower-confidence findings (not posted inline)

  • [low] pkg/desktop/login.go:120 — goroutine closes done channel after releasing lock (brief window where new caller observes inflight==nil before channel is closed) (confidence: weak 43/100)
  • [low] pkg/desktop/login.go:150 — poll loop fetches token immediately after POST before async Desktop refresh could occur (minor inefficiency) (confidence: weak 30/100)
  • [low] pkg/desktop/login.go:52 — expiry leeway is applied as a 30s grace period after expiry rather than before — may serve tokens expired up to 30s ago (confidence: weak 30/100)
  • [low] pkg/desktop/login_test.go:205 — cleanup ordering may expose detached goroutine to restored global values (potential flakiness) (confidence: weak 30/100)

Comment thread pkg/desktop/raw_client.go
@aheritier aheritier added area/registry OCI registry, agent sharing, push/pull kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 21, 2026
…budget

Cap the POST /registry/credstore-updated call to refreshBudget/3 via a new
postRefreshNudge helper so a slow Desktop can't exhaust the whole refresh
budget before polling begins. Also move close(done) under the refreshState
lock to close a brief window where a new caller could observe inflight==nil
before the channel is closed.

Assisted-By: Claude
@dgageot
dgageot merged commit f48c21f into docker:main Jul 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/registry OCI registry, agent sharing, push/pull kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants