Skip to content

Enable Microsoft Entra ID sign-in for Private Marketplace access#325331

Open
mcumming wants to merge 7 commits into
microsoft:mainfrom
mcumming:mcumming-entra-id-vss-marketplace-review
Open

Enable Microsoft Entra ID sign-in for Private Marketplace access#325331
mcumming wants to merge 7 commits into
microsoft:mainfrom
mcumming:mcumming-entra-id-vss-marketplace-review

Conversation

@mcumming

@mcumming mcumming commented Jul 10, 2026

Copy link
Copy Markdown

Addresses #280376Enable VS Code sign-in with Microsoft Entra ID to connect to Private Marketplace.

Why

Today a configured Private Marketplace (extensions.gallery.serviceUrl) is gated only through the GitHub/default account path (enterprise flag or entitlement SKU). Customers who manage identity in Microsoft Entra ID have no way to sign in and be authorized against their marketplace. This change makes marketplace access provider-aware and adds a new Microsoft/Entra path with server-enforced eligibility, so an Entra-signed-in user who is verified as eligible gets the configured marketplace as their active Extension Gallery.

What changes

The new path is entirely behind a product flag (product.jsonenableExtensionGalleryEntraAuth) and a per-marketplace setting/policy (extensions.gallery.authProvider, values github | microsoft). When the flag is off, microsoft is coerced back to github, so existing behavior is untouched.

  • Provider routing (extensionGalleryManifestService.ts): getEffectiveAuthProvider() selects the access strategy. The GitHub path is preserved as-is; a new Microsoft path acquires an existing Entra session silently (getSessions, never prompts), reads the service index, discovers the manifest-advertised EligibilityService endpoint, and POSTs the token to it. The server's boolean verdict decides access.
  • Access + error UX (extensionsViewlet.ts, extensions.contribution.ts, extensions.ts): provider-aware welcome view and activity badge for the new marketplace states, plus a provider-routed sign-in command (microsoft → Entra createSession, otherwise the existing default-account sign-in). A CONTEXT_MARKETPLACE_AUTH_PROVIDER context key backs the routing.
  • Status model (extensionGalleryManifest.ts): adds Misconfigured and Unreachable states (alongside RequiresSignIn / AccessDenied), the EligibilityService resource type, PRIVATE_MARKETPLACE_SCOPES, the authProvider config key, and a typed MarketplaceAuthRequiredError.
  • Policy / product (policyData.jsonc, product.ts, product.json): registers the authProvider admin policy and adds microsoft to trustedExtensionAuthAccess.

Security and correctness

  • The Entra token is only ever sent to an HTTPS, exact same-origin target as the admin-configured service index (isSafeTokenTarget), so a compromised/misconfigured manifest can't redirect the token to a foreign or cleartext origin.
  • Token-bearing requests never follow redirects (the request service would forward the Authorization header across hops).
  • 401 vs 403 are distinct: 401 (missing/expired/wrong-audience token) → RequiresSignIn, never cached; 403 (identity accepted but forbidden) → AccessDenied, cached as ineligible.
  • The cached verdict is scoped to authProvider + accountId + serviceUrl and dropped on any mismatch, so a stale allow/deny can't leak across accounts or marketplaces.
  • A monotonic validation epoch guards every await, so a session/account/config change mid-validation supersedes an in-flight result instead of racing it.
  • Transient failures (auth service down, marketplace unreachable, non-2xx, or a 200 that isn't a valid manifest) surface Unreachable rather than leaving a configured marketplace on a blank view or throwing.

Testing

  • 45 unit tests in extensionGalleryManifestService.test.ts covering provider routing, the eligibility handshake, cache scoping/invalidation, 401/403/5xx/malformed classification, and the epoch race paths.
  • npm run compile-check-ts-native, the gallery unit suite, and npm run valid-layers-check all pass.

Copilot AI review requested due to automatic review settings July 10, 2026 17:15
@mcumming mcumming changed the title Enable Microsoft Entra ID sign-in for Private Marketplace access (PR1) Enable Microsoft Entra ID sign-in for Private Marketplace access Jul 10, 2026
mcumming and others added 5 commits July 10, 2026 13:19
…ntext key

Introduce the `extensions.gallery.authProvider` policy that selects which
identity provider (github or microsoft) gates Private Marketplace access, and
register it in the exported policy data. Add the marketplace auth-provider
context key and the Entra ID resource scope constant used to acquire a
Private Marketplace-audienced token.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve the marketplace access strategy in the workbench gallery manifest
service: cache-first startup, provider-routed access handling, Microsoft
eligibility probing against the eligibility resource from the gallery
manifest, the GitHub DefaultAccount path, the marketplace auth-provider
context key, and access telemetry.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Surface a provider-aware sign-in prompt and access-denied state in the
extensions viewlet, driven by the marketplace auth-provider context key so
the correct identity provider is presented to the user.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Allow the built-in extensions gallery to silently use Microsoft (Entra ID)
authentication sessions for Private Marketplace access.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover provider selection, cache-first startup, Microsoft eligibility
handling, and the GitHub access path in the gallery manifest service.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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 provider-aware Private Marketplace authentication using Microsoft Entra ID, including eligibility validation, secure token transport, caching, status UX, and policy configuration.

Changes:

  • Adds Microsoft session and eligibility-service authentication.
  • Adds provider-aware sign-in, marketplace statuses, and error UX.
  • Adds configuration policy, product gating, and unit coverage.
Show a summary per file
File Description
extensionGalleryManifestService.test.ts Tests routing, eligibility, caching, and races.
extensionGalleryManifestService.ts Implements authentication and eligibility flow.
extensions.ts Defines marketplace provider context.
extensionsViewlet.ts Adds status-specific marketplace UX.
extensions.contribution.ts Registers policy and sign-in action.
extensionGalleryManifest.ts Adds statuses, resource type, and scopes.
product.ts Defines the Entra product gate.
product.json Adds Microsoft authentication-provider metadata.
policyData.jsonc Exports the new enterprise policy.

Review details

  • Files reviewed: 9/9 changed files
  • Comments generated: 8
  • Review effort level: Medium

Comment on lines +333 to +339
enum: product.enableExtensionGalleryEntraAuth ? ['github', 'microsoft'] : ['github'],
enumDescriptions: product.enableExtensionGalleryEntraAuth ? [
localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
localize('extensions.gallery.authProvider.microsoft', "Authenticate to the Extensions Marketplace using a Microsoft (Entra ID) account."),
] : [
localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
],

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The schema enum/enumDescriptions are now unconditionally ['github','microsoft']; the Entra gate is enforced at runtime in getEffectiveAuthProvider() and the setting stays hidden (included: false), so a clean policy export stays valid and nothing new is advertised in the UI.


This reply was generated by Copilot on my behalf.

Comment on lines +270 to +271
this.clearCachedAccess();
validate();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. On a Microsoft session change we now revoke the active manifest (update(null)) before revalidating, so a transient index/eligibility failure on the new session can no longer preserve the previous session's Available status.


This reply was generated by Copilot on my behalf.

Comment on lines +279 to +281
this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => {
this.clearCachedAccess();
validate();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed the same way on the GitHub onDidChangeDefaultAccount listener — the manifest is revoked (update(null)) before revalidation, so the old account's authorization can't leak into the new one on a transient failure.


This reply was generated by Copilot on my behalf.

Comment on lines +157 to +158
Promise.resolve().then(() => this.getExtensionGalleryManifest()).then(manifest => {
return Promise.all(channels.map(channel => channel.call('setExtensionGalleryManifest', [manifest])));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already handled: the constructor subscribes the shared-process/remote channels to onDidChangeExtensionGalleryManifest, so later manifest changes (including the null → available transition on the Microsoft path) are forwarded to those processes, not just the bootstrap manifest.


This reply was generated by Copilot on my behalf.

Comment on lines +836 to +844
if (!Array.isArray(extensionGalleryManifest.resources)) {
// A 200 whose body is valid JSON but not a service index (e.g. a server error
// object, or an HTML/JSON captive-portal page) must not be treated as a
// manifest — `resources` is required to discover gallery endpoints (including
// the EligibilityService). Reject here so callers classify it as a failed
// fetch, rather than letting resource-URI discovery throw on a non-iterable
// `resources` outside this try/catch.
throw new Error('Service index response is not a valid extension gallery manifest.');
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. getExtensionGalleryManifestFromServiceUrl now rejects a 200 whose resources contain malformed entries (missing string id/type), not just a non-array resources, so getExtensionGalleryManifestResourceUri can't throw outside the fetch's error-classification block — a malformed body now yields Unreachable. Added a unit test for this path.


This reply was generated by Copilot on my behalf.

Comment on lines +169 to +171
private _authenticationService: IAuthenticationService | undefined;
private get authenticationService(): IAuthenticationService {
return this._authenticationService ??= this.instantiationService.invokeFunction(accessor => accessor.get(IAuthenticationService));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Acknowledged, and this is deliberate. Eager constructor injection of IAuthenticationService forms a real DI cycle (IAuthenticationServiceIExtensionService → gallery → this service) that throws RECURSIVELY instantiating service at startup. The lazy accessor is documented in-code as the cycle-breaker. We're keeping it for PR1 rather than introducing an init-API refactor; happy to revisit that approach in a follow-up if preferred.


This reply was generated by Copilot on my behalf.

import { IHostService } from '../../host/browser/host.js';
import { IDefaultAccount } from '../../../../base/common/defaultAccount.js';
import { AuthenticationSession, IAuthenticationService } from '../../authentication/common/authentication.js';
import { CONTEXT_MARKETPLACE_AUTH_PROVIDER } from '../../../contrib/extensions/common/extensions.js';

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. Moved CONTEXT_MARKETPLACE_AUTH_PROVIDER to platform/extensionManagement/common/extensionGalleryManifest.ts; the service imports it from there and the Extensions contribution re-exports it for existing consumers. valid-layers-check passes.


This reply was generated by Copilot on my behalf.

});

viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
content: localize('access denied microsoft', "Your Microsoft account does not have access to the Extensions Marketplace. An Entra ID (work or school) account or Visual Studio Subscription is required. Please contact your administrator."),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The Microsoft AccessDenied message is now generic ("…does not have access to the Extensions Marketplace. Please contact your administrator."), no longer asserting a specific remedy the client can't verify from a bare 403.


This reply was generated by Copilot on my behalf.

mcumming and others added 2 commits July 10, 2026 13:23
…ndling

Address rubber-duck review findings on the Entra ID marketplace path:

- Scope the cached access verdict to the marketplace it was computed against
  (authProvider + accountId + serviceUrl), rejecting stale caches on any mismatch.
- Guard cache application and background validation with a monotonic epoch so a
  session/account/config change mid-validation supersedes an in-flight result.
- Register session/account listeners before applying the cache, and the config
  listener before initial validation, closing startup TOCTOU windows.
- Route transient auth-service and marketplace-fetch failures to Unreachable
  instead of leaving a configured marketplace on a blank Unavailable view.
- Split 401 (missing/expired token -> RequiresSignIn, not cached) from 403
  (durable denial -> AccessDenied, cached ineligible).
- Never follow redirects on token-bearing requests; only send the Entra token to
  an HTTPS same-origin target; reject non-2xx and non-manifest 200 responses
  before parsing.
- Restore the galleryservice:custom:marketplace telemetry on the GitHub path and
  drop the unused server-provided eligibility reason from persisted cache.

Expand unit coverage to 45 tests across provider routing, eligibility, caching,
error classification, and the epoch race paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…g, resource validation, UX copy

- Policy: make the `extensions.gallery.authProvider` schema enum and enumDescriptions
  unconditional (`github`, `microsoft`). Gating the enum on the Entra product flag left the
  policy metadata exporting two enum descriptions against a single-value enum, which fails the
  policy-artifact generator's equal-length requirement on a clean export. The Entra gate is
  already enforced at runtime in getEffectiveAuthProvider(), and the setting is hidden
  (included: false), so this advertises nothing new in the UI.

- Cross-account authorization leak: on Microsoft session change and GitHub default-account
  change, revoke the active manifest (drop `Available`) before revalidating. Previously the
  active status stayed `Available`, so a transient index/eligibility failure on the new account
  preserved the prior account's access.

- Layering: move CONTEXT_MARKETPLACE_AUTH_PROVIDER down to the platform extensionGalleryManifest
  module so the workbench service no longer imports from a workbench/contrib module. The
  Extensions contribution re-exports it for existing consumers.

- Resource validation: reject a 200 service index whose `resources` entries are malformed
  (missing string `id`/`type`), not just a non-array `resources`. Endpoint discovery calls
  `resource.type.split()` outside the fetch try/catch, so an undefined `type` would crash
  initialization instead of surfacing `Unreachable`.

- UX: make the Microsoft AccessDenied welcome message generic. A bare 403 gives no typed reason,
  so asserting that an Entra ID account or Visual Studio Subscription is required could tell an
  already-signed-in user to obtain access they already have.

Adds a unit test covering the malformed-resources -> Unreachable path. All 47 gallery tests pass;
typecheck-client and valid-layers-check are clean.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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