Enable Microsoft Entra ID sign-in for Private Marketplace access#325331
Enable Microsoft Entra ID sign-in for Private Marketplace access#325331mcumming wants to merge 7 commits into
Conversation
…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>
There was a problem hiding this comment.
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
| 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."), | ||
| ], |
There was a problem hiding this comment.
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.
| this.clearCachedAccess(); | ||
| validate(); |
There was a problem hiding this comment.
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.
| this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => { | ||
| this.clearCachedAccess(); | ||
| validate(); |
There was a problem hiding this comment.
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.
| Promise.resolve().then(() => this.getExtensionGalleryManifest()).then(manifest => { | ||
| return Promise.all(channels.map(channel => channel.call('setExtensionGalleryManifest', [manifest]))); |
There was a problem hiding this comment.
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.
| 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.'); | ||
| } |
There was a problem hiding this comment.
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.
| private _authenticationService: IAuthenticationService | undefined; | ||
| private get authenticationService(): IAuthenticationService { | ||
| return this._authenticationService ??= this.instantiationService.invokeFunction(accessor => accessor.get(IAuthenticationService)); |
There was a problem hiding this comment.
Acknowledged, and this is deliberate. Eager constructor injection of IAuthenticationService forms a real DI cycle (IAuthenticationService → IExtensionService → 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'; |
There was a problem hiding this comment.
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."), |
There was a problem hiding this comment.
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.
…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>
1a134d7 to
282c38d
Compare
Addresses #280376 — Enable 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.json→enableExtensionGalleryEntraAuth) and a per-marketplace setting/policy (extensions.gallery.authProvider, valuesgithub|microsoft). When the flag is off,microsoftis coerced back togithub, so existing behavior is untouched.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.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→ EntracreateSession, otherwise the existing default-account sign-in). ACONTEXT_MARKETPLACE_AUTH_PROVIDERcontext key backs the routing.extensionGalleryManifest.ts): addsMisconfiguredandUnreachablestates (alongsideRequiresSignIn/AccessDenied), theEligibilityServiceresource type,PRIVATE_MARKETPLACE_SCOPES, theauthProviderconfig key, and a typedMarketplaceAuthRequiredError.policyData.jsonc,product.ts,product.json): registers theauthProvideradmin policy and addsmicrosofttotrustedExtensionAuthAccess.Security and correctness
isSafeTokenTarget), so a compromised/misconfigured manifest can't redirect the token to a foreign or cleartext origin.Authorizationheader across hops).RequiresSignIn, never cached; 403 (identity accepted but forbidden) →AccessDenied, cached as ineligible.authProvider+accountId+serviceUrland dropped on any mismatch, so a stale allow/deny can't leak across accounts or marketplaces.await, so a session/account/config change mid-validation supersedes an in-flight result instead of racing it.Unreachablerather than leaving a configured marketplace on a blank view or throwing.Testing
extensionGalleryManifestService.test.tscovering 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, andnpm run valid-layers-checkall pass.