Harden Entra marketplace access: cache scoping, race guards, error ha… · microsoft/vscode@bd44656 · GitHub
Skip to content

Commit bd44656

Browse files
mcummingCopilot
andcommitted
Harden Entra marketplace access: cache scoping, race guards, error handling
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>
1 parent 94a70a7 commit bd44656

6 files changed

Lines changed: 1148 additions & 142 deletions

File tree

‎src/vs/base/common/product.ts‎

Lines changed: 9 additions & 0 deletions

‎src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,21 @@ export const enum ExtensionGalleryManifestStatus {
6969
Available = 'available',
7070
RequiresSignIn = 'requiresSignIn',
7171
AccessDenied = 'accessDenied',
72-
Unavailable = 'unavailable'
72+
Unavailable = 'unavailable',
73+
/**
74+
* A marketplace is configured, and the user is (or is presumed) eligible, but its
75+
* gallery manifest could not be fetched — a transient network/server error. Unlike
76+
* {@link Unavailable} (which also means "no gallery configured"), this state is only
77+
* ever set after a failed fetch of a configured marketplace, so it is safe to surface
78+
* an informative message without affecting builds that have no gallery at all.
79+
*/
80+
Unreachable = 'unreachable',
81+
/**
82+
* The marketplace is configured for Microsoft (Entra ID) authentication, but the
83+
* gallery manifest does not advertise an EligibilityService resource. Access is
84+
* refused (no silent fallback to another provider) until the server is corrected.
85+
*/
86+
Misconfigured = 'misconfigured'
7387
}
7488

7589
export const IExtensionGalleryManifestService = createDecorator<IExtensionGalleryManifestService>('IExtensionGalleryManifestService');
@@ -103,13 +117,13 @@ export const ExtensionGalleryServiceUrlConfigKey = 'extensions.gallery.serviceUr
103117
export const ExtensionGalleryAuthProviderConfigKey = 'extensions.gallery.authProvider';
104118

105119
/**
106-
* Scope for requesting tokens against the Private Marketplace's own Entra app registration.
107-
* Produces tokens with `aud = api://{private-marketplace-client-id}`, matching the server's
108-
* `Marketplace:Authorization:Entra:Audience` config.
120+
* Scopes requested when signing in with Microsoft (Entra ID) to establish the
121+
* user's identity for the Private Marketplace eligibility check.
109122
*
110-
* Do NOT use `https://marketplace.visualstudio.com/.default` — that produces
111-
* `aud: marketplace.visualstudio.com` and will be rejected with 401.
112-
*
113-
* `{private-marketplace-client-id}` is the Client ID from the deployment-time app registration.
123+
* Only standard OpenID Connect sign-in scopes are requested — enough to obtain a
124+
* Microsoft session that identifies the user. This intentionally does NOT request a
125+
* resource-scoped token (e.g. `api://<client-id>/access_as_user`). Acquiring resource
126+
* tokens for Private Marketplace API calls, per the server's Protected Resource
127+
* Metadata (RFC 9728), is deferred to a follow-up change.
114128
*/
115-
export const PRIVATE_MARKETPLACE_SCOPE = 'api://{private-marketplace-client-id}/access_as_user';
129+
export const PRIVATE_MARKETPLACE_SCOPES: string[] = ['openid', 'profile', 'email', 'offline_access'];

‎src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { CommandsRegistry, ICommandService } from '../../../../platform/commands
2424
import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
2525
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
2626
import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
27-
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryAuthProviderConfigKey, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService, PRIVATE_MARKETPLACE_SCOPE } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
27+
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryAuthProviderConfigKey, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService, PRIVATE_MARKETPLACE_SCOPES } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
2828
import { EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, ExtensionRequestsTimeoutConfigKey, ExtensionsLocalizedLabel, FilterType, IExtensionGalleryService, IExtensionManagementService, PreferencesLocalizedLabel, SortBy, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js';
2929
import { areSameExtensions, getIdAndVersion } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
3030
import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js';
@@ -364,10 +364,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
364364
},
365365
[ExtensionGalleryAuthProviderConfigKey]: {
366366
type: 'string',
367-
enum: ['github', 'microsoft'],
368-
enumDescriptions: [
367+
enum: product.enableExtensionGalleryEntraAuth ? ['github', 'microsoft'] : ['github'],
368+
enumDescriptions: product.enableExtensionGalleryEntraAuth ? [
369369
localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
370370
localize('extensions.gallery.authProvider.microsoft', "Authenticate to the Extensions Marketplace using a Microsoft (Entra ID) account."),
371+
] : [
372+
localize('extensions.gallery.authProvider.github', "Authenticate to the Extensions Marketplace using GitHub."),
371373
],
372374
description: localize('extensions.gallery.authProvider', "Configure the authentication provider for the Extensions Marketplace"),
373375
default: '',
@@ -376,7 +378,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
376378
policy: {
377379
name: 'ExtensionGalleryAuthProvider',
378380
category: PolicyCategory.Extensions,
379-
minimumVersion: '1.99',
381+
minimumVersion: '1.121',
380382
localization: {
381383
description: {
382384
key: 'extensions.gallery.authProvider',
@@ -2162,13 +2164,14 @@ registerAction2(class ExtensionsGallerySignInAction extends Action2 {
21622164
}
21632165
async run(accessor: ServicesAccessor): Promise<void> {
21642166
const configurationService = accessor.get(IConfigurationService);
2167+
const productService = accessor.get(IProductService);
21652168
const authProvider = configurationService.getValue<string>(ExtensionGalleryAuthProviderConfigKey);
21662169

2167-
if (authProvider === 'microsoft') {
2170+
if (authProvider === 'microsoft' && productService.enableExtensionGalleryEntraAuth) {
21682171
const authenticationService = accessor.get(IAuthenticationService);
21692172
await authenticationService.createSession(
21702173
'microsoft',
2171-
[PRIVATE_MARKETPLACE_SCOPE]);
2174+
PRIVATE_MARKETPLACE_SCOPES);
21722175
} else {
21732176
const commandService = accessor.get(ICommandService);
21742177
await commandService.executeCommand(DEFAULT_ACCOUNT_SIGN_IN_COMMAND);

‎src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts‎

Lines changed: 22 additions & 1 deletion

0 commit comments

Comments
 (0)