Skip to content

Authenticate Entra ID marketplace API requests via RFC 9728 PRM negotiation - #325804

Merged
Karthik Nadig (karthiknadig) merged 7 commits into
microsoft:mainfrom
mcumming:entra-marketplace-pr2-prm-auth
Oct 21, 2026
Merged

Karthik Nadig (karthiknadig) merged 7 commits into
microsoft:mainfrom
mcumming:entra-marketplace-pr2-prm-auth

Conversation

@mcumming

@mcumming Michael Cummings (MSFT) (mcumming) commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Scope: Microsoft Entra ID only. The GitHub auth-enabled path called for in #325412 — RFC 8693 token exchange at the deployment's embedded Authorization Server, onDidChangeSessions('github') invalidation, and the GitHub test scenarios — is deferred to a follow-up PR. The GitHub path here is unchanged: it carries no bearer at all.

Builds on #325331 (now merged), which added Microsoft Entra ID sign-in as a licensing signal for a Private Marketplace but left the token it produced unused. Partially addresses #325412.

Why

A Private Marketplace running with authentication enforced refuses its service index outright unless a token minted for the marketplace itself is presented. Signing in is not enough: the sign-in token identifies the user, but its audience is not the marketplace.

The marketplace's resource identity cannot be shipped in product.json, because a self-hosted deployment has its own app registration and its own scopes — one value baked into the build cannot serve every enterprise. So the client has to learn it from the resource at runtime, which is exactly what RFC 9728 exists for.

What it does

  1. Read the service index with whatever bearer the account already carries.
  2. If the marketplace refuses it (401/403), read the Protected Resource Metadata it advertises at /.well-known/oauth-protected-resource to learn its authorization server and scopes.
  3. Ask the account for a resource-scoped token against that server (RFC 8707) and read the index again.
  4. Attach the accepted bearer to the requests that follow — extensionquery/search, control manifest, asset and VSIX downloads, extension resources (icons, README incl. embedded images, CHANGELOG), and the shared-process getManifest/VSIX path.

Whether a marketplace needs any of this is discovered, not configured: one that accepts what it is given is never asked what a token for it should look like, and never has the well-known endpoint fetched.

Discovery uses the well-known endpoint rather than the WWW-Authenticate challenge, because that header is not CORS-safelisted and the renderer's cross-origin index fetch usually cannot read it. The challenge is used only as a hint for an explicit resource_metadata URL.

Which origins may receive the bearer

IExtensionGalleryManifestService.getAccessToken() is replaced by:

getAuthorizationHeaders(targetUrl: string): Promise<Record<string, string>>;

Callers ask for headers instead of handling a raw token, so the rule deciding which origins may receive the bearer has one implementation, in the platform base class.

The rule is strict same-origin over https against the service index — the endpoint that demanded the token and that it was minted for — and it fails closed. This matters concretely: a marketplace serves assets from its own origin when proxying, but upstreamed extensions are fetched from the public Microsoft marketplace, which must never receive a private marketplace's Entra token. A parent-domain suffix match would be unsafe here, since these deployments can share a domain with unrelated tenants.

The bearer is dropped whenever the marketplace is retracted, so a sign-out, account switch, or configuration change cannot leave a usable one behind.

Commits

  1. Negotiate a resource-scoped token when the marketplace gates its index — discovery, negotiation, retry, and the origin rule.
  2. Authenticate the gallery API requests — search, control manifest, asset and VSIX downloads.
  3. Authenticate the extension resource requests — icons, README, CHANGELOG.
  4. Authenticate the marketplace requests the shared process makes — that process cannot negotiate, so the token and the index origin travel with the manifest over the existing channel.
  5. Consent to the marketplace resource during interactive sign-in — the access check is silent by contract and cannot prompt.
  6. Cover the gated service index — tests.

Behavior notes for reviewers

  • A marketplace that still refuses after negotiation now reports RequiresSignIn, not AccessDenied. AccessDenied renders as "contact your administrator" with no action; a missing consent grant is resolvable by the user, so sending them there was a dead end.
  • The bearer now also reaches the remote server, not just the shared process — both receive it over the manifest channel. The origin rule is applied identically in that process.
  • Open and public marketplaces are unaffected. No token is negotiated, none is attached, and the well-known endpoint is never fetched.

Testing

  • Unit tests cover the negotiation and the origin rule: reaching the marketplace through negotiation, skipping it when the marketplace accepts what it's given, refusing when no resource is advertised, dropping the bearer on retraction, and withholding it from a foreign origin, a parent-domain neighbour, cleartext, and an unparseable URL.
  • New extensionGalleryManifestServiceIpc.test.ts covers the cross-process channel, which is the one path where an argument-order mistake would silently leak or drop the bearer.
  • A manual pass against a Private Marketplace with authentication enforced is still worth doing before merge — see the test plan.

Copilot AI review requested due to automatic review settings July 14, 2026 14:35

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

Implements authenticated access to Private Marketplaces by discovering RFC 9728 Protected Resource Metadata (PRM) and negotiating a resource-scoped bearer token, then threading that token through all VS Code surfaces that perform marketplace requests (workbench, shared process/remote, gallery API, and extension resource loading).

Changes:

  • Add PRM discovery + resource-scoped token negotiation for auth-gated marketplace service indexes, exposing the negotiated token via IExtensionGalleryManifestService.getAccessToken().
  • Attach the negotiated token to marketplace API/asset requests and extension resource requests, guarded by same-secure-origin checks to prevent token leakage.
  • Extend marketplace status modeling + UX (new statuses, provider-aware sign-in) and add unit tests covering negotiation/error/cache paths.
Show a summary per file
File Description
src/vs/workbench/services/extensionManagement/test/electron-browser/extensionGalleryManifestService.test.ts Adds unit coverage for Microsoft/GitHub routing, negotiation (401→PRM), caching, and status transitions.
src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts Implements negotiation, caching, eligibility checks, token threading to IPC channels, and new status handling.
src/vs/workbench/contrib/extensions/common/extensions.ts Re-exports the marketplace auth-provider context key for workbench contributions.
src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts Updates welcome content and badges for new marketplace statuses and provider-specific sign-in messaging.
src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts Registers extensions.gallery.authProvider setting + policy, and updates the marketplace sign-in action to support Microsoft/PRM consent.
src/vs/platform/extensionResourceLoader/common/extensionResourceLoaderService.ts Passes resource URI into header computation for authenticated marketplace resource requests.
src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts Adds guarded Authorization header attachment for extension resource (README/etc.) fetches.
src/vs/platform/extensionResourceLoader/browser/extensionResourceLoaderService.ts Mirrors resource-aware header computation for browser fetch paths.
src/vs/platform/extensionManagement/common/extensionGalleryService.ts Attaches negotiated bearer token to extensionquery, stats/control, and asset download requests (same-secure-origin guarded).
src/vs/platform/extensionManagement/common/extensionGalleryManifestServiceIpc.ts Threads negotiated access token over the manifest IPC channel and exposes getAccessToken() in non-window processes.
src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts Adds a default getAccessToken() implementation returning undefined for open marketplaces.
src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts Introduces provider context key, new statuses, new config key/scopes, and PRM discovery helper/types.
src/vs/base/common/product.ts Adds enableExtensionGalleryEntraAuth product gate for Microsoft/Entra marketplace auth path.
product.json Extends product data to include a microsoft entry under the relevant auth access structure.
build/lib/policies/policyData.jsonc Updates generated policy catalog to include ExtensionGalleryAuthProvider.

Review details

  • Files reviewed: 15/15 changed files
  • Comments generated: 1
  • Review effort level: Low

@mcumming Michael Cummings (MSFT) (mcumming) changed the title Authenticate marketplace API requests via RFC 9728 PRM negotiation Authenticate Entra ID marketplace API requests via RFC 9728 PRM negotiation Jul 14, 2026
@vs-code-engineering

vs-code-engineering Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

Robo (@deepak1556)

Matched files:

  • src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts
  • src/vs/code/node/cliProcessMain.ts

@mcumming
Michael Cummings (MSFT) (mcumming) force-pushed the entra-marketplace-pr2-prm-auth branch 3 times, most recently from 1dc6076 to aa13d37 Compare September 1, 2026 18:30
@sandy081 Sandeep Somavarapu (sandy081) removed their assignment Oct 5, 2026
Michael Cummings (MSFT) (mcumming) added a commit to mcumming/vscode that referenced this pull request Oct 8, 2026
Implements the GitHub provider's marketplace token acquisition on top of the account-service
architecture from microsoft#325804: when the service index is gated, discover the protected resource
(RFC 9728), then exchange the default account's GitHub session token at the marketplace's
authorization server (RFC 8693). The subject token is only sent to a same-origin HTTPS token
endpoint, with redirects disabled. Also validates the RFC 8414 issuer, strips Authorization on
cross-origin redirects in the node request service, and re-validates on GitHub session changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b972512b-5cf0-4fb8-88b3-ef6b24a19441
@mcumming
Michael Cummings (MSFT) (mcumming) force-pushed the entra-marketplace-pr2-prm-auth branch 2 times, most recently from 53cd376 to 65d1118 Compare October 15, 2026 13:04
Discover RFC 9728 metadata and negotiate resource-scoped Entra tokens for protected marketplaces. Keep account selection, consent, and authorization lifecycle in the Account Service, with a shared read-only platform view and origin-restricted process replicas. Simplify access negotiation and cover authorization cleanup, process identity, and request routing with regression tests.

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

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3c5458c9-6cfb-4157-bdad-76383ffab22b
@mariaghiondea

Copy link
Copy Markdown

Sandeep Somavarapu (@sandy081) this is ready for your re-review

@mcumming

Copy link
Copy Markdown
Contributor Author

Post-merge implementation record and follow-ups

The description above predates the final review changes. This note corrects the record for the implementation merged as d71149f; it does not imply the follow-up fixes below have already landed.

  • The merged API is IExtensionGalleryManifestService.getAccessToken(targetUrl), not getAuthorizationHeaders. Callers construct their own Authorization headers.
  • Microsoft account resolution discovers protected-resource metadata before fetching the service index; it is not a 401-first negotiation. Consequently, the well-known endpoint can also be fetched for an index that accepts anonymous requests. The description's unconditional anonymous/no-discovery claims do not describe the merged Entra code.
  • Missing metadata or unavailable resource sessions currently fall back to the sign-in access token. That is not proof of marketplace audience and is the behavior removed by the scoped-token follow-up below.
  • The raw access token and service-index URL travel with the manifest to the shared process and any connected remote server. The HTTPS/authority guard controls request attachment, not access to the token by the receiving host.
  • The six-item commit narrative is stale: this PR contains seven commits and was squashed into d71149f. The final code, rather than that narrative, is the implementation reference.

The post-merge feedback is addressed in separate, ready-for-review upstream PRs to keep reviews focused:

Concern Follow-up
Cross-origin redirect credentials, including explicit Electron redirect handling #343690
Never substitute the Entra sign-in token or identity scopes for marketplace credentials #343689
Silent resource-token renewal, transient retry, cancellation, and snapshot rotation #343688
Preserve 403 AccessDenied instead of an ineffective sign-in loop #343686
Bounded/coalesced successful metadata discovery with URL/consent invalidation #343687

Transport clarification: the remote server uses the Node request implementation, while the shared process supplies Electron net.request. The standalone redirect PR explicitly handles Electron's manual-redirect event and includes real transport coverage; Node inheritance alone was not evidence of identical Chromium credential leakage.

The GitHub exchange feature in #325815 remains in review. Its existing Node hardening and GitHub-only renewal are not fixes already deployed on main, nor do they supply Entra renewal. Rebase that feature after the standalone transport change lands rather than duplicating its older redirect implementation. The first three fixes above are the deployment priorities.

Explicit security sign-off on the remote credential-delegation boundary has been requested from TylerLeonhardt in #343689 (comment). Approval is still pending; source checks and regressions do not grant it.

Drafted with GitHub Copilot.

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.

6 participants