Skip to content

Test Microsoft Entra ID sign-in for Private Marketplace access #328699

Description

Refs: #325331

Complexity: 4

Authors: Michael Cummings (MSFT) (@mcumming), joshspicer

Create Issue


Summary

VS Code can be pointed at a private Extensions Marketplace via extensions.gallery.serviceUrl, and extensions.gallery.authProvider selects which account determines whether the user is allowed to use it:

  • github (default) — the ambient GitHub default account must be enterprise, or carry an entitlement SKU listed in product.extensionsGallery.accessSKUs. No bearer token is ever sent.
  • microsoft — a Microsoft (Entra ID) account must be signed in, and its tenant (tid) claim decides eligibility locally: a work/school (Entra) tenant is eligible, a personal Microsoft Account (MSA) is not. Only an eligible account's bearer token is carried.

Important

Scope of this change. This PR delivers the eligibility gate, sign-in UX, and marketplace status surfacing for Entra ID. A bearer token is presented only on the service-index discovery request; every subsequent gallery operation (search, query, download, install) remains unauthenticated, because extensionGalleryService is untouched by this PR.

VS Code therefore cannot yet operate against a marketplace whose gallery APIs require auth. The Entra cases below verify who is allowed in, not authenticated transport.

The microsoft provider is additionally gated on product.enableExtensionGalleryEntraAuth, which this PR does not set — see prerequisite 4.

Marketplace states surfaced to the user: Available, RequiresSignIn, AccessDenied, Unreachable, Misconfigured.

Set up / prerequisites

1. Marketplace endpoint. Use this test deployment, which serves its service index and gallery APIs without auth:

https://vscode-private-marketplace.jollypebble-4899baee.westus2.azurecontainerapps.io/api/v1

2. Settings. extensions.gallery.serviceUrl and extensions.gallery.authProvider are application-scoped and hidden from the Settings UI (included: false). They are honored from settings.json, but you get no autocomplete and an "Unknown Configuration Setting" squiggle — that is expected. Edit your user settings.json directly:

{
    "extensions.gallery.serviceUrl": "https://vscode-private-marketplace.jollypebble-4899baee.westus2.azurecontainerapps.io/api/v1"
}

3. A GitHub account entitled to the private marketplace (Cases 1–4, 10) — an enterprise account, or one whose entitlement SKU is listed in product.extensionsGallery.accessSKUs.

4. For the Entra cases (5–9) only: enableExtensionGalleryEntraAuth is absent from product.json in this PR, so a stock build never offers Microsoft sign-in. You need a dev build (.\scripts\code.bat / ./scripts/code.sh) with this added to product.json, plus "extensions.gallery.authProvider": "microsoft" in settings.json:

"enableExtensionGalleryEntraAuth": true

You also need two Microsoft accounts: a work/school (Entra ID) account (eligible) and a personal MSA@outlook.com / @hotmail.com / @live.com (ineligible).

Getting both signed in at once. VS Code supports multiple simultaneous Microsoft accounts, but this feature cannot add the second one — its sign-in command binds silently to the existing account whenever exactly one is signed in, and only offers "Sign in with a Different Account…" from two accounts onward. Add the second account through another consumer of Microsoft auth, then return to the marketplace:

  1. Sign in the first account via the marketplace welcome link or the Accounts menu entry.
  2. Install the Azure Resources extension (ms-azuretools.vscode-azureresourcegroups) and run Azure: Sign In. With one account already present, VS Code appends "Sign in to another account" to the picker — choose that and sign in as the second account.
  3. Re-invoke the marketplace sign-in; the account Quick Pick from Case 7 now appears.

The marketplace picker counts accounts regardless of what they were signed in for, so an account added via Azure is offered even though it has no marketplace session yet. Settings Sync is not a usable route here — its auth provider comes from product.json, which a dev build does not set.

5. Resetting between cases. There is no cached access verdict — access is re-resolved on every window. The only persisted state is the chosen Microsoft account (marketplace.account, APPLICATION scope), which is scoped by authProvider so switching provider ignores it. To change the outcome, sign out of / switch the relevant account from the Accounts menu.

6. Logs. Marketplace diagnostics are prefixed [Marketplace] in the window log. A failing service-index response includes the first 200 characters of the server's error body, which is useful when diagnosing a 4xx.

Test cases

Case 1 — Private marketplace via the GitHub path

  1. Set serviceUrl to the test endpoint from prerequisite 1, leave authProvider unset. Restart.
  2. Sign in with a GitHub account that is entitled (enterprise or listed SKU).
  3. ✅ Expected: the Extensions view loads against the private marketplace — search returns its catalog, and an extension installs from it.
  4. ❌ Not expected: the public marketplace catalog, a blank Extensions view, or any warning badge on the activity-bar icon.

Case 2 — Not signed in / not entitled

  1. Same config as Case 1, but sign out of GitHub (or use an account with no entitlement).
  2. ✅ Expected with no account: the Extensions view shows the welcome "Sign in with GitHub to access the Extensions Marketplace." (the leading phrase is the link), and the Accounts menu offers "Sign in to access Extensions Marketplace".
  3. ✅ Expected when signed in but not entitled: "Your account does not have access to the Extensions Marketplace. Please contact your administrator.", plus a warning badge on the activity-bar Extensions icon reading "Access denied to marketplace".
  4. ✅ Expected: the Accounts menu entry is offered for both the signed-out and access-denied states, so the user can always get to a sign-in.
  5. Sign in with an entitled account → ✅ the marketplace becomes available without a manual reload.

Case 3 — Access is re-resolved on every window

  1. Reach the working state of Case 1, then fully restart VS Code.
  2. ✅ Expected: access is checked again from scratch and the marketplace ends up available. There is no cached verdict, so a change made while VS Code was closed (entitlement granted or revoked) is picked up on the next start.
  3. Open a second window → ✅ it resolves independently to the same state.
  4. ❌ Not expected: a stale verdict surviving a restart, or the marketplace appearing available when the account no longer qualifies.

Case 4 — Failure taxonomy: unreachable vs denied vs signed out

The status must match the kind of failure. Each of these is a separate check:

  1. Network down / unresolvable host (e.g. https://no-such-marketplace.invalid/api/v1) → ✅ "The Extensions Marketplace is currently unavailable. Check your network connection and try again.""try again" is a link that reloads the window — with the "Marketplace is currently unavailable" badge.
  2. Server error (5xx), or a 200 whose body is not a valid gallery manifest → ✅ the same unavailable message. A JSON error body or a captive-portal page must never be accepted as a manifest.
  3. The marketplace rejects the client with a 4xx other than 401/403 (e.g. 400) → ✅ access-denied, not unavailable. This is a durable rejection, so telling the user to check their network would be wrong.
  4. Not signed in → ✅ the sign-in welcome, never the "check your network connection" message.
  5. No downgrade on transient failure: from a working marketplace, cause a network drop or 5xx → ✅ the available marketplace is preserved, not blanked. (A 4xx denial does apply immediately — it is durable.)
  6. Restore connectivity and click try again → ✅ the marketplace recovers.

Case 5 — Entra eligibility, work/school account

Requires the enableExtensionGalleryEntraAuth product flag (prerequisite 4). This case verifies the eligibility gate and sign-in UX, not authenticated transport.

  1. Set "extensions.gallery.authProvider": "microsoft" with the serviceUrl from prerequisite 1. Restart with no Microsoft session signed in.
  2. ✅ Expected: the Extensions view shows the welcome "Sign in with your Microsoft account to access the Extensions Marketplace.", and the Accounts menu shows "Sign in to access Extensions Marketplace".
  3. Click either affordance → a Microsoft sign-in flow starts, requesting scopes openid profile email offline_access.
  4. Sign in with the work/school (Entra) account.
  5. ✅ Expected: the account is judged eligible and the marketplace becomes available — the welcome disappears, the catalog loads, and search/install work.
  6. ❌ Not expected: a repeated sign-in prompt, a blank Extensions view, or a lingering warning badge.

Case 6 — Personal Microsoft Account is rejected

  1. Same config as Case 5, but sign in with the personal MSA.
  2. ✅ Expected: "Your account does not have access to the Extensions Marketplace. Please contact your administrator.", with the "Access denied to marketplace" activity-bar badge.
  3. ✅ Expected: the denial is decided locally from the token, with no request to the marketplace at all — the ineligible identity's token is never transmitted. Confirm no service-index request is made (window log / network trace).
  4. ✅ Expected: the Accounts menu still offers "Sign in to access Extensions Marketplace" so the user can switch accounts.
  5. Recovering from the denied state. With the MSA as the only signed-in Microsoft account, invoking that entry re-binds to the same account and appears to do nothing — switching requires signing out first. Sign out of the MSA from the Accounts menu, then invoke the sign-in affordance again → ✅ a fresh interactive login appears, and signing in with the work/school account restores access.
  6. ❌ Not expected: a sign-in prompt loop. The verdict is stable across restarts as long as the same account is selected.
  7. Because the check precedes any network call, this reproduces against any HTTPS serviceUrl.

Case 7 — Multiple Microsoft accounts → account picker

  1. Sign in to two or more Microsoft accounts (see prerequisite 4 for how), then configure the Entra path and restart.
  2. ✅ Expected: with several accounts signed in and no remembered choice, the client does not pick one silently — the Extensions view shows the sign-in welcome.
  3. Invoke the sign-in affordance.
  4. ✅ Expected: a Quick Pick titled "Select the account to use for the Extensions Marketplace" listing each signed-in account plus "Sign in with a Different Account…". The Quick Pick only appears from two accounts onward; with one account the command binds to it directly with no prompt.
  5. Pick the work/school account → ✅ the marketplace becomes available, and the choice is remembered across restarts (restart and confirm no re-prompt).
  6. Sign out of only the remembered account while others remain → ✅ back to the sign-in welcome; it must not silently switch to another signed-in account.
  7. Re-invoke the sign-in command and pick the MSA → ✅ the status flips to access-denied for the newly chosen account.

Case 8 — Sign out and account switch

  1. From the working state of Case 5, open the Accounts menu and sign out of the Microsoft account.
  2. ✅ Expected: the marketplace immediately drops the authorized catalog and returns to the sign-in welcome. It must not remain available.
  3. Switch between two different eligible (work/school) accounts.
  4. ✅ Expected: the catalog is republished for the newly selected account. The catalog is account-scoped, so the previous account's contents must not linger even though both accounts are eligible.
  5. Switch between an eligible and an ineligible account → ✅ the status reflects the new account. ❌ Never a stale verdict carried over.
  6. ✅ Expected: a sign-out that lands while a slow service-index fetch is still running still wins — the in-flight result must not restore access after sign-out.
  7. Timing note: Microsoft session plumbing is connected at the AfterRestored workbench phase, so a brief sign-in state on a cold start that self-corrects is acceptable. One that persists is a bug.

Case 9 — Misconfigured deployment (non-HTTPS)

  1. With "extensions.gallery.authProvider": "microsoft", point serviceUrl at an http:// endpoint such as http://localhost:8080/api/v1. Restart, signed in with the eligible work/school account.
  2. ✅ Expected: "The Extensions Marketplace is not configured correctly and cannot be reached. Please contact your administrator.", with an activity-bar badge reading "Marketplace is misconfigured".
  3. ✅ Expected: no request is issued at all — the token is withheld rather than sent over plaintext. There is no silent fallback to the GitHub path.
  4. Repeat with the ineligible MSA account on the same http:// URL → ✅ access-denied (the eligibility check runs first), not misconfigured.
  5. Repeat on the GitHub path (authProvider unset) with the same http:// URL → ✅ not misconfigured — that path never carries a token, so it is treated as an ordinary unreachable/failed endpoint.
  6. ❌ Not expected: an "unavailable" or "sign in" message in place of the misconfigured one in step 2.

Case 10 — authProvider: microsoft without the product flag

  1. Set "extensions.gallery.authProvider": "microsoft" in a build where enableExtensionGalleryEntraAuth is absent — i.e. a build straight from this PR, with no product.json edit. Restart.
  2. ✅ Expected: the microsoft provider is ignored and the GitHub path runs instead — the welcome reads "Sign in with GitHub to access the Extensions Marketplace." and the Accounts-menu entry routes to GitHub sign-in.
  3. ✅ Expected: with an entitled GitHub account, the marketplace behaves exactly as in Case 1.
  4. ❌ Not expected: any Microsoft sign-in prompt or Microsoft-worded UI.

Case 11 — Changing marketplace settings prompts a restart

  1. From any working state, change either extensions.gallery.serviceUrl or extensions.gallery.authProvider in settings.json.
  2. ✅ Expected: a dialog appears — "Visual Studio Code is now configured to a different Marketplace. Please restart to apply the changes." — offering a Restart button.
  3. Accept the restart → ✅ the new configuration takes effect.
  4. Dismiss the dialog instead → ✅ the window keeps running against the previously resolved marketplace until restarted; the change is applied on the next start.

Case 12 — Public marketplace unaffected (regression)

  1. Remove both settings (no serviceUrl, no authProvider) and restart.
  2. ✅ Expected: the public Extensions Marketplace behaves exactly as it does today — no sign-in gating, no warning badges, no change in search or install.

Notes for testers

  • This PR only decides eligibility; gallery traffic beyond the service index is still unauthenticated, so authenticated transport is not covered by these cases.
  • Status precision is the point of this feature. When something fails, check that the message matches the kind of failure per Case 4 — "check your network" for transient problems only, "contact your administrator" for durable rejections, and the sign-in prompt when there is simply no account.
  • There is no cached access verdict; access is re-resolved on every window. Only the chosen Microsoft account is remembered.
  • After changing either setting, use the restart prompt from Case 11 rather than assuming a plain Reload Window applies it.
  • Both settings are hidden, so type them manually into settings.json; the squiggle and missing autocomplete are expected.
  • If you cannot produce a build with enableExtensionGalleryEntraAuth, cover Cases 1–4 and 10–12 and note which build you used.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions