microsoft-authentication: an account with a terminally expired refresh token is never pruned, so signing in cannot recover it
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
Does this issue occur when all extensions are disabled?: N/A — the defect is in the built-in `microsoft-authentication` extension. Observing it only requires some consumer to request a Microsoft session; in my case C# Dev Kit, but Settings Sync or any other consumer would do.
- VS Code Version: 1.134.0 (110a328ea54b42367b803ec53ee0bf52ef26b419, x64)
- OS Version: Ubuntu 26.04 LTS, kernel 7.0.0-30-generic, x86_64, Wayland/GNOME
Steps to Reproduce:
1. Sign in to a Microsoft account, then leave its refresh token unused long enough to expire by inactivity (90 days), so silent acquisition fails with `AADSTS700082` / `invalid_grant` / `subError: bad_token`. A maintainer can shortcut this by revoking the refresh token server-side.
2. Open a workspace where an extension requests a Microsoft session — C# Dev Kit requests three distinct scope sets.
3. An Accounts badge appears. Click **Sign in with Microsoft to use \** and complete the flow.
4. Observe that the sign-in succeeds for the one scope set it was invoked with, the badge does not clear, and every other scope set continues to fail on the expired token indefinitely. Repeating step 3 never converges.
### The account is never pruned after a terminal failure
In the shipped provider, `getAllSessionsForPca` treats every silent-acquisition failure identically:
```js
catch(l){
l instanceof xe ? this._telemetryReporter.sendTelemetryClientAuthErrorEvent(l)
: this._telemetryReporter.sendTelemetryErrorEvent(l),
this._logger.info(`[getAllSessionsForPca] [${r.scopeStr}] [${u.username}] failed to acquire token silently, skipping account`, JSON.stringify(l));
continue
}
```
There is no branch on `errorCode` or `subError`. A terminal `invalid_grant` / `bad_token` — the credential is dead and no retry will ever succeed — is handled exactly like a transient network blip: log at `info`, send telemetry, `continue`. The account entry stays in the list and is retried on every `getSessions` for every scope, indefinitely, each attempt a real round trip to AAD (200 ms to 1.8 s in my logs).
### Why the sign-in the UI offers cannot recover it
Because the stale entry survives, an interactive sign-in adds a session for only the scope set it was invoked with. Every other scope set keeps resolving to the same dead entry:
```
12:17:55.349 [getSessions] [VSCODE_TENANT:organizations] returned 0 session(s) <- AADSTS700082
12:17:55.568 [getSessions] [6f1cc985-…/.default] returned 0 session(s) <- AADSTS700082
12:26:25.298 [createSession] [6f1cc985-…/.default] starting <- badge clicked
12:26:30.610 [createSession] [6f1cc985-…/.default] returned session
12:26:30.816 [getSessions] [6f1cc985-…/.default] returned 1 session(s) <- this scope now fine
12:26:32.083 [getSessions] [499b84ac-…/.default] returned 0 session(s) <- still the dead entry
```
Every AADSTS700082 payload across all of it cites the same original token (`issued on 2026-05-21T07:09:46Z`, `inactive for 90.00:00:00`), including after successful interactive sign-ins.
Two things follow that made this genuinely unfixable from the UI:
- The badge re-offers the same scope set it already satisfied, so clicking it repeatedly looks like the sign-in is failing when it is succeeding.
- `getSessions [all]` keeps returning 1 session throughout, so the Accounts menu lists the account as present and signed in while every scoped request fails.
### What actually resolved it
Signing out of the account — Accounts menu, the account, **Sign Out** — rather than clicking the badge's sign-in prompt again. The sign-out is offered per-extension ("The account '…' has been used by: … Sign out from these extensions?"). With no stale entry left to fall back on, a single fresh sign-in then produced sessions for all three scope sets, and the failures stopped:
```
12:37:54.503 [getSessions] [VSCODE_TENANT:organizations] returned 1 session(s)
12:37:54.508 [getSessions] [6f1cc985-…/.default] returned 1 session(s)
12:37:55.886 [getSessions] [499b84ac-…/.default] returned 1 session(s)
```
This survived a full restart with no `AADSTS700082` at all. Nothing in the UI suggests that signing out first is the required action, and "sign in again" — the remedy the badge offers — provably cannot work on its own.
### Suggested fix
Branch on the error in that `catch`. For a terminal `invalid_grant` / `bad_token`, either drop the account from the PCA cache, or flag it so the sign-in request carries the account identity and re-consents it fully instead of adding one scope alongside a dead credential. Logging a permanently dead credential at `info` and retrying it forever seems clearly wrong regardless of which remedy is chosen.
### Related
- #271268 — same `AADSTS700082` inactivity expiry, open, labelled `upstream`. That issue asks why fresher refresh tokens are not issued; this one is about the client-side handling once a token *has* terminally expired, which is separate and fixable locally.
- #291146 — same symptom (stale account never removed, repeated sign-outs), closed as `info-needed` without the mechanism being examined.
- microsoft/vscode-dotnettools#1512 — the C# Dev Kit side of what I was originally chasing. Independent of this; noting it only because its logs show this same pattern.
_Drafted by Claude (Anthropic AI assistant)._
Contributor guide
Assessment
This issue has not been assessed yet.