Skip to content

Add "Continue with Microsoft" sign in for GitHub - #332948

Merged
TylerLeonhardt merged 5 commits into
mainfrom
tyleonha/continue-with-microsoft
Aug 27, 2026
Merged

Add "Continue with Microsoft" sign in for GitHub#332948
TylerLeonhardt merged 5 commits into
mainfrom
tyleonha/continue-with-microsoft

Conversation

@TylerLeonhardt

Copy link
Copy Markdown
Member

Lets someone already signed in to Microsoft reach Copilot without a second trip through the browser. The microsoft provider already holds an Entra token; this trades it for a GitHub one.

How the flow works

Two exchanges, on purpose.

The first buys a read:user discovery token, just enough to call GET /user and find out which GitHub account the Entra identity maps to. That account is shown to the user, and nothing is published until they confirm it. The second exchange then mints the scopes the caller actually asked for.

The discovery token is never persisted and never published as a session. Neither token is ever logged.

What survives a reload

Entra-brokered sessions live in memory for the life of the window and never touch the Keychain. What persists is the user's consent: a row in global state holding the GitHub account label, the Microsoft account label, and the GitHub user id. A fresh window mints the session again from that row without prompting, re-running discovery to confirm the row still points at the same account.

Rows are keyed by GitHub account label, because that is what VS Code itself keys an account by. getAccounts collapses sessions by label and the account preference is stored by label. A session read back from the Keychain can carry a placeholder id, an old numeric one, or one from a lookup that failed, so the id is no way to tell two accounts apart. The id has exactly one job here: checking that the token GitHub just returned belongs to the account the row names, which a label cannot do because logins get renamed and freed logins get taken.

Signing out

Signing out of GitHub drops the row. Signing out of Microsoft drops the sessions, since nothing can renew them, but deliberately leaves the rows alone.

That asymmetry matters. The Microsoft account list is a per-window cache that legitimately reads empty for a moment while it repopulates, and the rows are global state shared by every window. Acting on a blink of that list would sign the user out everywhere with no way back. Dropping only the in-memory sessions self-heals: the next read mints them again from the row that is still there. Getting it wrong costs one round trip instead of the account.

Testing

37 unit tests pass across accountLinks.test.ts, entraTokenExchange.test.ts, flows.test.ts, plus the workbench tests in chatSetup.test.ts and defaultAccount.test.ts. End to end this was exercised through sign in, window reload, cross-window sign in from the Agents window, and both sign-out paths.

Note for reviewers

extensions/github-authentication/src/config.ts is intentionally untouched here. The client secret this endpoint requires lives in vscode-distro, not in this repo.

Draft while the endpoint registration is finalized.

🤖 Generated with Claude Code

TylerLeonhardt and others added 3 commits August 26, 2026 22:59
Brokers a GitHub session from an Entra token the built-in `microsoft`
provider already holds, so someone signed in to Microsoft can reach
Copilot without a second browser round trip.

The flow is deliberately two exchanges. The first buys a `read:user`
discovery token, just enough to `GET /user` and show which GitHub account
the Entra identity maps to. Nothing is published until the user confirms
that identity. The second exchange then mints the scopes the caller
actually asked for. The discovery token is never persisted and never
published as a session.

Entra-brokered sessions live in memory for the life of the window and are
never written to the Keychain. What survives a reload is the user's
consent, recorded in global state as a GitHub label, a Microsoft label,
and the GitHub user id. A fresh window mints the session again from that
row, silently, re-verifying through discovery that the row still points
at the same account.

Rows are keyed by GitHub account label, because that is what VS Code
itself keys an account by: `getAccounts` collapses sessions by label and
the account preference is stored by label. The id is kept for one job
only, checking that the token GitHub just returned belongs to the account
the row names.

Signing out of the Microsoft account drops the sessions, since nothing
can renew them, but leaves the rows alone. The Microsoft account list is
a per-window cache that reads empty for a moment while it repopulates,
and the rows are global state shared by every window, so acting on a
blink of that list would sign the user out everywhere with no way back.
Dropping only the sessions self-heals: the next read mints them again
from the row that is still there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings August 27, 2026 15:17
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Screenshot Changes

Base: 7a0d4c7f Current: 59524771

Added (2)

sessions/signInDialog/sessionsSignInDialog/SignInWithMicrosoft/Dark

current

sessions/signInDialog/sessionsSignInDialog/SignInWithMicrosoft/Light

current

Copilot AI 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.

Pull request overview

Adds Microsoft-backed GitHub authentication by exchanging Entra tokens for temporary GitHub sessions.

Changes:

  • Adds token exchange, renewal, account-link persistence, and session restoration.
  • Adds the Microsoft option to Copilot sign-in UI behind an experimental setting.
  • Adds UI fixtures, icons, and unit coverage.
Show a summary per file
File Description
sessionsSignInDialog.fixture.ts Adds Microsoft sign-in fixture.
defaultAccount.test.ts Tests additive sign-in scopes.
chatSetup.test.ts Tests Microsoft setup UI and routing.
microsoft-light.svg Adds light Microsoft icon.
microsoft-dark.svg Adds dark Microsoft icon.
chatSetup.css Styles Microsoft provider icon.
chatSetupRunner.ts Adds Microsoft button and strategy.
chatSetupProviders.ts Adds provider fallback metadata.
chatSetupController.ts Adds provider fallback metadata.
chatSetup.ts Defines Microsoft setup strategy.
chat.shared.contribution.ts Registers the experimental setting.
chatSettings.ts Defines the setting identifier.
product.ts Extends product provider configuration.
product.json Configures provider and trusted access.
testMemento.ts Adds in-memory test storage.
flows.test.ts Tests provider validation.
entraTokenExchange.test.ts Tests exchange and renewal behavior.
accountLinks.test.ts Tests persisted account mappings.
httpClient.ts Adds injectable HTTP implementation.
githubServer.ts Integrates Microsoft token exchange.
github.ts Manages transient sessions and restoration.
flows.ts Adds Microsoft provider typing.
entraTokenExchange.ts Implements two-stage token exchange.
microsoftAuthentication.ts Wraps Microsoft authentication APIs.
http.ts Defines HTTP abstractions.
gitHubAccount.ts Extracts GitHub account lookup.
accountLinks.ts Persists Microsoft–GitHub mappings.

Review details

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (2)

extensions/github-authentication/src/github.ts:380

  • A successful ID check can return a renamed GitHub login. The session is then stored under the new label while the link remains keyed by the old label, so every later unscoped getSessions considers that row missing and performs another exchange, accumulating duplicate transient sessions. Rewrite the link when the verified login changed before publishing the restored session.
		const session = this.sessionFor(renewed.account, renewed.token, [...renewed.scopes]);
		this._transientSessions.set(session.id, { session, expiresAt: Date.now() + renewed.expiresIn * 1000 });

extensions/github-authentication/src/github.ts:507

  • The renewal deliberately returns the freshly verified account, but spreading the old session discards it. If the GitHub login or avatar changed while the numeric ID stayed the same, the published session and account-link row remain stale, contrary to the ID-based rename handling. Update the session account and rewrite the link from renewed.account before firing changed.
		// The same session with a new token, so it keeps its id and is reported as changed rather
		// than as one session going away and another arriving.
		const next: vscode.AuthenticationSession = { ...session, accessToken: renewed.token };
		this._transientSessions.set(next.id, { session: next, expiresAt: Date.now() + renewed.expiresIn * 1000 });
  • Files reviewed: 25/27 changed files
  • Comments generated: 7
  • Review effort level: Balanced

Comment thread extensions/github-authentication/src/github.ts Outdated
Comment thread extensions/github-authentication/src/github.ts
Comment thread extensions/github-authentication/src/common/accountLinks.ts
Comment thread extensions/github-authentication/src/github.ts
Comment thread extensions/github-authentication/src/entraTokenExchange.ts
Comment thread extensions/github-authentication/src/github.ts Outdated
Comment thread extensions/github-authentication/src/entraTokenExchange.ts Outdated
TylerLeonhardt and others added 2 commits August 27, 2026 08:33
The hygiene job fails on eslint warnings, and both warnings were in
entraTokenExchange.test.ts: an `in` operator check and a double-quoted
string outside of localization. The harness override is now a positive
`noExchangeEndpoint` boolean, and the assertion uses single quotes.

The browser test broke because main added @INativeManagedSettingsService
to the DefaultAccountProvider constructor. The signIn helper now stubs
both managed-settings services with their existing Null implementations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verify the granted token against the account the user confirmed, not just
the discovery token, and give that mismatch its own failure kind so a
restore only forgets a link when GitHub positively names somebody else.

Make a failed unlink write stick for the window that did it, so a sign out
cannot leave a row behind that silently signs the user back in.

Discard a token whose Microsoft account was signed out while the exchange
was in flight, settle every expired session rather than only those with
nothing to hand back, and warn when GitHub grants fewer scopes than asked.

Adds a provider-level test suite driven through the real getSessions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TylerLeonhardt
TylerLeonhardt marked this pull request as ready for review August 27, 2026 16:19
@TylerLeonhardt
TylerLeonhardt enabled auto-merge (squash) August 27, 2026 16:19
@TylerLeonhardt
TylerLeonhardt merged commit 5ca078d into main Aug 27, 2026
54 of 55 checks passed
@TylerLeonhardt
TylerLeonhardt deleted the tyleonha/continue-with-microsoft branch August 27, 2026 17:32
@vs-code-engineering vs-code-engineering Bot added this to the 1.136.0 milestone Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants