src/vs/workbench/api/common/extHostAuthentication.ts

1040 LOC · 180 covered · 860 uncovered · 39 ranges · 38 concepts · 1 introducers · 33 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/workbench/api/common/extHostExtensionService.ts · 1285 LOCcommon/extHostExtensionS…src/vs/workbench/api/common/extHostFileSystemConsumer.ts · 260 LOCcommon/extHostFileSystem…src/vs/workbench/api/common/extHostLanguageModels.ts · 768 LOCcommon/extHostLanguageMo…src/vs/workbench/api/common/extHostLocalizationService.ts · 109 LOCcommon/extHostLocalizati…src/vs/workbench/api/common/extHostManagedSockets.ts · 121 LOCcommon/extHostManagedSoc…src/vs/workbench/api/common/extHostMemento.ts · 122 LOCcommon/extHostMemento.tssrc/vs/workbench/api/common/extHostProgress.ts · 107 LOCcommon/extHostProgress.t…src/vs/workbench/api/common/extHostSecretState.ts · 42 LOCcommon/extHostSecretStat…src/vs/workbench/api/common/extHostSecrets.ts · 51 LOCcommon/extHostSecrets.tssrc/vs/workbench/api/common/extHostStorage.ts · 75 LOCcommon/extHostStorage.tssrc/vs/workbench/api/common/extHostStoragePaths.ts · 95 LOCcommon/extHostStoragePat…src/vs/workbench/api/common/extHostUrls.ts · 70 LOCcommon/extHostUrls.tssrc/vs/workbench/api/common/extHostWindow.ts · 116 LOCcommon/extHostWindow.tssrc/vs/workbench/api/common/extHostXaaAuthProvider.ts · 402 LOCcommon/extHostXaaAuthPro…src/vs/workbench/services/extensions/common/workspaceContains.ts · 139 LOCcommon/workspaceContains…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 3 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 4 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×2 · 4 introduced LOCextHostMcp.ts ×2extHostMcp.ts ×1 · 1 introduced LOCextHostMcp.ts ×1extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×4 · 6 introduced LOCextHostMcp.ts ×4extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1, extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×1 · 6 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×4 · 13 introduced LOCextHostMcp.ts ×4extHostMcpNode.ts ×8 · 45 introduced LOCextHostMcpNode.ts ×8extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×5 · 25 introduced LOCextHostMcp.ts ×5extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · 0 introduced LOCextHostMcp.test|title=Ex…extHostMcp.ts ×3 · 23 introduced LOCextHostMcp.ts ×3extHostMcp.ts ×1 · 2 introduced LOCextHostMcp.ts ×1extHostMcp.ts ×8 · 60 introduced LOCextHostMcp.ts ×8extHostMcp.ts ×43 · 874 introduced LOCextHostMcp.ts ×43extensionHostMain.ts ×1 · 2 introduced LOCextensionHostMain.ts ×1reset prepareStackTrace-callback|occurrence=1, extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 prevent rewrapping|occurrence=1 · 0 introduced LOCreset prepareStackTrace-…extensionHostMain.ts ×5 · 26 introduced LOCextensionHostMain.ts ×5f0f2e182082072efdaf0f8e1537d2cce Restored, too many uses before restoration|occurrence=1 · 0 introduced LOCf0f2e182082072efdaf0f8e1…extensionHostMain.ts ×1 · 3 introduced LOCextensionHostMain.ts ×1extensionHostMain.ts ×1 · 3 introduced LOCextensionHostMain.ts ×1extensionHostMain.ts ×11 · 164 introduced LOCextensionHostMain.ts ×11extHostExtensionService.ts ×64 · 1246 introduced LOCextHostExtensionService.…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should allow undefined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata properties should expose readonly properties|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should handle multiple Bearer challenges and use first scope|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should ignore non-Bearer schemes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when no WWW-Authenticate header and scopes are already undefined|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are same but in different order|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return false when scopes are the same|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true and update scopes when WWW-Authenticate header contains new scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from defined scopes to undefined (no scope in header)|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp IAuthMetadata update() should return true when updating from undefined scopes to defined scopes|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should create IAuthMetadata with fetched server metadata|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should fall back to default metadata when server metadata fetch fails|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle empty scope string in WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle invalid JSON in resource metadata response|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle malformed WWW-Authenticate header gracefully|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should handle non-401 status codes in update()|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should pass launch headers when fetching metadata from same origin|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use resource_metadata challenge URL from WWW-Authenticate header|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported|occurrence=1extHostMcp.test|title=Ex…extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extHostMcp.test|title=ExtHostMcp createAuthMetadata should use scopes from WWW-Authenticate header when resource metadata has none|occurrence=1extHostMcp.test|title=Ex…extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 basics|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 basics|occurrence=1extensionHostMain.test|t…f0f2e182082072efdaf0f8e1537d2cce Never restored, separate operations|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 https://gist.github.com/thecrypticace/f0f2e182082072efdaf0f8e1537d2cce Never restored, separate operations|occurrence=1f0f2e182082072efdaf0f8e1…f0f2e182082072efdaf0f8e1537d2cce Restored, separate operations|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 https://gist.github.com/thecrypticace/f0f2e182082072efdaf0f8e1537d2cce Restored, separate operations|occurrence=1f0f2e182082072efdaf0f8e1…f0f2e182082072efdaf0f8e1537d2cce Restored, too many uses before restoration|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 https://gist.github.com/thecrypticace/f0f2e182082072efdaf0f8e1537d2cce Restored, too many uses before restoration|occurrence=1f0f2e182082072efdaf0f8e1…extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 prevent rewrapping|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 prevent rewrapping|occurrence=1extensionHostMain.test|t…reset prepareStackTrace-callback|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 set/reset prepareStackTrace-callback|occurrence=1reset prepareStackTrace-…extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 wrap prepareStackTrace-callback|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/common/extensionHostMain.test|title=ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slowdown and eventual stack overflow #184926 wrap prepareStackTrace-callback|occurrence=1extensionHostMain.test|t…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg does not add stray ^ to argument values|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg doubles embedded double quotes (cmd.exe convention)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg neutralizes cmd.exe metacharacters inside quotes (CVE-2024-27980)|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with parentheses without injecting ^|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg preserves paths with spaces|occurrence=1extHostMcpNode.test|titl…extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostMcpNode.test|title=extHostMcpNode - escapeCmdArg wraps simple values in double quotes|occurrence=1extHostMcpNode.test|titl…Focused file · src/vs/workbench/api/common/extHostAuthentication.ts · 1040 LOCcommon/extHostAuthentica…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- extHostExtensionService.ts ×64
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type * as vscode from 'vscode';
7 > import * as nls from '../../../nls.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { MainContext, MainThreadAuthenticationShape, ExtHostAuthenticationShape } from './extHost.protocol.js';
10 > import { Disposable, ProgressLocation } from './extHostTypes.js';
11 > import { IExtensionDescription, ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
12 > import { IAuthenticationGetSessionsOptions, IAuthenticationProviderSessionOptions, INTERNAL_AUTH_PROVIDER_PREFIX, isAuthenticationWwwAuthenticateRequest } from '../../services/authentication/common/authentication.js';
13 > import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
14 > import { IExtHostRpcService } from './extHostRpcService.js';
15 > import { URI, UriComponents } from '../../../base/common/uri.js';
16 > import { AuthorizationErrorType, fetchDynamicRegistration, getClaimsFromJWT, IAuthorizationJWTClaims, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, IAuthorizationTokenResponse, isAuthorizationErrorResponse, isAuthorizationTokenResponse } from '../../../base/common/oauth.js';
17 > import { IExtHostWindow } from './extHostWindow.js';
18 > import { IExtHostInitDataService } from './extHostInitDataService.js';
19 > import { ILogger, ILoggerService, ILogService } from '../../../platform/log/common/log.js';
20 > import { autorun, derivedOpts, IObservable, ISettableObservable, observableValue } from '../../../base/common/observable.js';
21 > import { stringHash } from '../../../base/common/hash.js';
22 > import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
23 > import { IExtHostUrlsService } from './extHostUrls.js';
24 > import { encodeBase64, VSBuffer } from '../../../base/common/buffer.js';
25 > import { equals as arraysEqual } from '../../../base/common/arrays.js';
26 > import { IExtHostProgress } from './extHostProgress.js';
27 > import { IProgressStep } from '../../../platform/progress/common/progress.js';
28 > import { CancellationError, isCancellationError } from '../../../base/common/errors.js';
29 > import { raceCancellationError, SequencerByKey } from '../../../base/common/async.js';
30 > import { XaaifyAuthProvider } from './extHostXaaAuthProvider.js';
31 >
32 > export interface IExtHostAuthentication extends ExtHostAuthentication { }
33 > export const IExtHostAuthentication = createDecorator<IExtHostAuthentication>('IExtHostAuthentication');
34 >
35 > interface ProviderWithMetadata {
36 > label: string;
37 > provider: vscode.AuthenticationProvider;
38 > disposable?: vscode.Disposable;
39 > options: vscode.AuthenticationProviderOptions;
40 > }
41 >
42 > export class ExtHostAuthentication implements ExtHostAuthenticationShape {
43 >
44 > declare _serviceBrand: undefined;
45 >
46 > protected readonly _dynamicAuthProviderCtor = DynamicAuthProvider;
47 > protected readonly _xaaAuthProviderCtor = XaaifyAuthProvider(DynamicAuthProvider);
48 >
49 > private _proxy: MainThreadAuthenticationShape;
50 > private _authenticationProviders: Map<string, ProviderWithMetadata> = new Map<string, ProviderWithMetadata>();
51 > private _providerOperations = new SequencerByKey<string>();
52 >
53 > private _onDidChangeSessions = new Emitter<vscode.AuthenticationSessionsChangeEvent & { extensionIdFilter?: string[] }>();
54 > private _getSessionTaskSingler = new TaskSingler<vscode.AuthenticationSession | undefined>();
55 >
56 > private _onDidDynamicAuthProviderTokensChange = new Emitter<{ authProviderId: string; clientId: string; tokens: IAuthorizationToken[] }>();
57 >
58 > constructor(
59 @IExtHostRpcService extHostRpc: IExtHostRpcService,
60 @IExtHostInitDataService private readonly _initData: IExtHostInitDataService,
61 @IExtHostWindow private readonly _extHostWindow: IExtHostWindow,
62 @IExtHostUrlsService private readonly _extHostUrls: IExtHostUrlsService,
63 @IExtHostProgress private readonly _extHostProgress: IExtHostProgress,
64 @ILoggerService private readonly _extHostLoggerService: ILoggerService,
65 @ILogService private readonly _logService: ILogService,
66 ) {
67 this._proxy = extHostRpc.getProxy(MainContext.MainThreadAuthentication);
68 }
70 > /**
71 > * This sets up an event that will fire when the auth sessions change with a built-in filter for the extensionId
72 > * if a session change only affects a specific extension.
73 > * @param extensionId The extension that is interested in the event.
74 > * @returns An event with a built-in filter for the extensionId
75 > */
76 > getExtensionScopedSessionsEvent(extensionId: string): Event<vscode.AuthenticationSessionsChangeEvent> {
77 const normalizedExtensionId = extensionId.toLowerCase();
78 return Event.chain(this._onDidChangeSessions.event, ($) => $
79 .filter(e => !e.extensionIdFilter || e.extensionIdFilter.includes(normalizedExtensionId))
80 .map(e => ({ provider: e.provider }))
81 );
82 }
84 > async getSession(requestingExtension: IExtensionDescription, providerId: string, scopesOrRequest: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options: vscode.AuthenticationGetSessionOptions & ({ createIfNone: true } | { forceNewSession: true } | { forceNewSession: vscode.AuthenticationForceNewSessionOptions })): Promise<vscode.AuthenticationSession>;
85 > async getSession(requestingExtension: IExtensionDescription, providerId: string, scopesOrRequest: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options: vscode.AuthenticationGetSessionOptions & { forceNewSession: true }): Promise<vscode.AuthenticationSession>;
86 > async getSession(requestingExtension: IExtensionDescription, providerId: string, scopesOrRequest: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options: vscode.AuthenticationGetSessionOptions & { forceNewSession: vscode.AuthenticationForceNewSessionOptions }): Promise<vscode.AuthenticationSession>;
87 > async getSession(requestingExtension: IExtensionDescription, providerId: string, scopesOrRequest: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options: vscode.AuthenticationGetSessionOptions): Promise<vscode.AuthenticationSession | undefined>;
88 > async getSession(requestingExtension: IExtensionDescription, providerId: string, scopesOrRequest: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options: vscode.AuthenticationGetSessionOptions = {}): Promise<vscode.AuthenticationSession | undefined> {
89 const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
90 const keys: (keyof vscode.AuthenticationGetSessionOptions)[] = Object.keys(options) as (keyof vscode.AuthenticationGetSessionOptions)[];
91 // TODO: pull this out into a utility function somewhere
92 const optionsStr = keys
93 .map(key => {
94 switch (key) {
95 case 'account':
96 return `${key}:${options.account?.id}`;
97 case 'createIfNone':
98 case 'forceNewSession': {
99 const value = typeof options[key] === 'boolean'
100 ? `${options[key]}`
101 : `'${options[key]?.detail}/${options[key]?.learnMore?.toString()}'`;
102 return `${key}:${value}`;
103 }
104 case 'authorizationServer':
105 return `${key}:${options.authorizationServer?.toString(true)}`;
106 default:
107 return `${key}:${!!options[key]}`;
108 }
109 })
110 .sort()
111 .join(', ');
112
113 let singlerKey: string;
114 if (isAuthenticationWwwAuthenticateRequest(scopesOrRequest)) {
115 const challenge = scopesOrRequest as vscode.AuthenticationWwwAuthenticateRequest;
116 const challengeStr = challenge.wwwAuthenticate;
117 const scopesStr = challenge.fallbackScopes ? [...challenge.fallbackScopes].sort().join(' ') : '';
118 singlerKey = `${extensionId} ${providerId} challenge:${challengeStr} ${scopesStr} ${optionsStr}`;
119 } else {
120 const sortedScopes = [...scopesOrRequest].sort().join(' ');
121 singlerKey = `${extensionId} ${providerId} ${sortedScopes} ${optionsStr}`;
122 }
123
124 return await this._getSessionTaskSingler.getOrCreate(singlerKey, async () => {
125 await this._proxy.$ensureProvider(providerId);
126 const extensionName = requestingExtension.displayName || requestingExtension.name;
127 return this._proxy.$getSession(providerId, scopesOrRequest, extensionId, extensionName, options);
128 });
129 }
131 > async getAccounts(providerId: string) {
132 await this._proxy.$ensureProvider(providerId);
133 return await this._proxy.$getAccounts(providerId);
134 }
136 > registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable {
137 // register
138 void this._providerOperations.queue(id, async () => {
139 // This use to be synchronous, but that wasn't an accurate representation because the main thread
140 // may have unregistered the provider in the meantime. I don't see how this could really be done
141 // synchronously, so we just say first one wins.
142 if (this._authenticationProviders.get(id)) {
143 this._logService.error(`An authentication provider with id '${id}' is already registered. The existing provider will not be replaced.`);
144 return;
145 }
146 const listener = provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(id, e));
147 this._authenticationProviders.set(id, { label, provider, disposable: listener, options: options ?? { supportsMultipleAccounts: false } });
148 await this._proxy.$registerAuthenticationProvider({
149 id,
150 label,
151 supportsMultipleAccounts: options?.supportsMultipleAccounts ?? false,
152 supportedAuthorizationServers: options?.supportedAuthorizationServers,
153 supportsChallenges: options?.supportsChallenges
154 });
155 });
156
157 // unregister
158 return new Disposable(() => {
159 void this._providerOperations.queue(id, async () => {
160 const providerData = this._authenticationProviders.get(id);
161 if (providerData) {
162 providerData.disposable?.dispose();
163 this._authenticationProviders.delete(id);
164 await this._proxy.$unregisterAuthenticationProvider(id);
165 }
166 });
167 });
168 }
170 > $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
171 return this._providerOperations.queue(providerId, async () => {
172 const providerData = this._authenticationProviders.get(providerId);
173 if (providerData) {
174 options.authorizationServer = URI.revive(options.authorizationServer);
175 return await providerData.provider.createSession(scopes, options);
176 }
177
178 throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
179 });
180 }
182 > $removeSession(providerId: string, sessionId: string): Promise<void> {
183 return this._providerOperations.queue(providerId, async () => {
184 const providerData = this._authenticationProviders.get(providerId);
185 if (providerData) {
186 return await providerData.provider.removeSession(sessionId);
187 }
188
189 throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
190 });
191 }
193 > $getSessions(providerId: string, scopes: ReadonlyArray<string> | undefined, options: IAuthenticationGetSessionsOptions): Promise<ReadonlyArray<vscode.AuthenticationSession>> {
194 return this._providerOperations.queue(providerId, async () => {
195 const providerData = this._authenticationProviders.get(providerId);
196 if (providerData) {
197 options.authorizationServer = URI.revive(options.authorizationServer);
198 return await providerData.provider.getSessions(scopes, options);
199 }
200
201 throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
202 });
203 }
205 > $getSessionsFromChallenges(providerId: string, constraint: vscode.AuthenticationConstraint, options: vscode.AuthenticationProviderSessionOptions): Promise<ReadonlyArray<vscode.AuthenticationSession>> {
206 return this._providerOperations.queue(providerId, async () => {
207 const providerData = this._authenticationProviders.get(providerId);
208 if (providerData) {
209 const provider = providerData.provider;
210 // Check if provider supports challenges
211 if (typeof provider.getSessionsFromChallenges === 'function') {
212 options.authorizationServer = URI.revive(options.authorizationServer);
213 return await provider.getSessionsFromChallenges(constraint, options);
214 }
215 throw new Error(`Authentication provider with handle: ${providerId} does not support getSessionsFromChallenges`);
216 }
217
218 throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
219 });
220 }
222 > $createSessionFromChallenges(providerId: string, constraint: vscode.AuthenticationConstraint, options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
223 return this._providerOperations.queue(providerId, async () => {
224 const providerData = this._authenticationProviders.get(providerId);
225 if (providerData) {
226 const provider = providerData.provider;
227 // Check if provider supports challenges
228 if (typeof provider.createSessionFromChallenges === 'function') {
229 options.authorizationServer = URI.revive(options.authorizationServer);
230 return await provider.createSessionFromChallenges(constraint, options);
231 }
232 throw new Error(`Authentication provider with handle: ${providerId} does not support createSessionFromChallenges`);
233 }
234
235 throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
236 });
237 }
239 > $onDidChangeAuthenticationSessions(id: string, label: string, extensionIdFilter?: string[]) {
240 // Don't fire events for the internal auth providers
241 if (!id.startsWith(INTERNAL_AUTH_PROVIDER_PREFIX)) {
242 this._onDidChangeSessions.fire({ provider: { id, label }, extensionIdFilter });
243 }
244 return Promise.resolve();
245 }
247 > $onDidUnregisterAuthenticationProvider(id: string): Promise<void> {
248 return this._providerOperations.queue(id, async () => {
249 const providerData = this._authenticationProviders.get(id);
250 if (providerData) {
251 providerData.disposable?.dispose();
252 this._authenticationProviders.delete(id);
253 }
254 });
255 }
257 > async $registerDynamicAuthProvider(
258 authorizationServerComponents: UriComponents,
259 serverMetadata: IAuthorizationServerMetadata,
260 resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
261 clientId: string | undefined,
262 clientSecret: string | undefined,
263 initialTokens: IAuthorizationToken[] | undefined
264 ): Promise<string> {
265 if (!clientId) {
266 const authorizationServer = URI.revive(authorizationServerComponents);
267 if (serverMetadata.registration_endpoint) {
268 try {
269 const registration = await fetchDynamicRegistration(serverMetadata, this._initData.environment.appName, resourceMetadata?.scopes_supported);
270 clientId = registration.client_id;
271 clientSecret = registration.client_secret;
272 } catch (err) {
273 this._logService.warn(`Dynamic registration failed for ${authorizationServer.toString()}: ${err.message}. Prompting user for client ID and client secret...`);
274 }
275 }
276 // Still no client id so dynamic client registration was either not supported or failed
277 if (!clientId) {
278 this._logService.info(`Prompting user for client registration details for ${authorizationServer.toString()}`);
279 const clientDetails = await this._proxy.$promptForClientRegistration(authorizationServer.toString());
280 if (!clientDetails) {
281 throw new Error('User did not provide client details');
282 }
283 clientId = clientDetails.clientId;
284 clientSecret = clientDetails.clientSecret;
285 this._logService.info(`User provided client registration for ${authorizationServer.toString()}`);
286 if (clientSecret) {
287 this._logService.trace(`User provided client secret for ${authorizationServer.toString()}`);
288 } else {
289 this._logService.trace(`User did not provide client secret for ${authorizationServer.toString()}`);
290 }
291 }
292 }
293 const provider = new this._dynamicAuthProviderCtor(
294 this._extHostWindow,
295 this._extHostUrls,
296 this._initData,
297 this._extHostProgress,
298 this._extHostLoggerService,
299 this._proxy,
300 URI.revive(authorizationServerComponents),
301 serverMetadata,
302 resourceMetadata,
303 clientId,
304 clientSecret,
305 this._onDidDynamicAuthProviderTokensChange,
306 initialTokens || []
307 );
308
309 // Use the sequencer to ensure dynamic provider registration is serialized
310 await this._providerOperations.queue(provider.id, async () => {
311 this._authenticationProviders.set(
312 provider.id,
313 {
314 label: provider.label,
315 provider,
316 disposable: Disposable.from(
317 provider,
318 provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(provider.id, e)),
319 provider.onDidChangeClientId(() => this._proxy.$sendDidChangeDynamicProviderInfo({
320 providerId: provider.id,
321 clientId: provider.clientId,
322 clientSecret: provider.clientSecret
323 }))
324 ),
325 options: { supportsMultipleAccounts: true }
326 }
327 );
328
329 await this._proxy.$registerDynamicAuthenticationProvider({
330 id: provider.id,
331 label: provider.label,
332 supportsMultipleAccounts: true,
333 authorizationServer: authorizationServerComponents,
334 resourceServer: resourceMetadata ? URI.parse(resourceMetadata.resource) : undefined,
335 clientId: provider.clientId,
336 clientSecret: provider.clientSecret
337 });
338 });
339
340
341
342
343 return provider.id;
344 }
346 > async $registerXaaAuthProvider(
347 issuerComponents: UriComponents,
348 serverMetadata: IAuthorizationServerMetadata,
349 clientId: string | undefined,
350 clientSecret: string | undefined,
351 initialTokens: IAuthorizationToken[] | undefined
352 ): Promise<string> {
353 const issuer = URI.revive(issuerComponents);
354 // XAA does not use Dynamic Client Registration — the IdP must already trust the requesting
355 // app for the target audience(s). Always require an admin-provisioned client_id (and
356 // typically client_secret).
357 if (!clientId) {
358 this._logService.info(`Prompting user for client registration details for XAA issuer ${issuer.toString()}`);
359 const clientDetails = await this._proxy.$promptForClientRegistration(issuer.toString());
360 if (!clientDetails) {
361 throw new Error('User did not provide client details');
362 }
363 clientId = clientDetails.clientId;
364 clientSecret = clientDetails.clientSecret;
365 }
366 const provider = new this._xaaAuthProviderCtor(
367 this._extHostWindow,
368 this._extHostUrls,
369 this._initData,
370 this._extHostProgress,
371 this._extHostLoggerService,
372 this._proxy,
373 issuer,
374 serverMetadata,
375 /* resourceMetadata */ undefined,
376 clientId,
377 clientSecret,
378 this._onDidDynamicAuthProviderTokensChange,
379 initialTokens || []
380 );
381
382 await this._providerOperations.queue(provider.id, async () => {
383 this._authenticationProviders.set(
384 provider.id,
385 {
386 label: provider.label,
387 provider,
388 disposable: Disposable.from(
389 provider,
390 provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(provider.id, e)),
391 provider.onDidChangeClientId(() => this._proxy.$sendDidChangeDynamicProviderInfo({
392 providerId: provider.id,
393 clientId: provider.clientId,
394 clientSecret: provider.clientSecret
395 }))
396 ),
397 options: { supportsMultipleAccounts: true }
398 }
399 );
400
401 await this._proxy.$registerDynamicAuthenticationProvider({
402 id: provider.id,
403 label: provider.label,
404 supportsMultipleAccounts: true,
405 authorizationServer: issuerComponents,
406 resourceServer: undefined,
407 clientId: provider.clientId,
408 clientSecret: provider.clientSecret
409 });
410 });
411
412 return provider.id;
413 }
415 > async $onDidChangeDynamicAuthProviderTokens(authProviderId: string, clientId: string, tokens: IAuthorizationToken[]): Promise<void> {
416 this._onDidDynamicAuthProviderTokensChange.fire({ authProviderId, clientId, tokens });
417 }
419 >
420 class TaskSingler<T> {
421 private _inFlightPromises = new Map<string, Promise<T>>();
422 > getOrCreate(key: string, promiseFactory: () => Promise<T>) { extHostExtensionService.ts ×64
423 const inFlight = this._inFlightPromises.get(key);
424 if (inFlight) {
425 return inFlight;
426 }
427
428 const promise = promiseFactory().finally(() => this._inFlightPromises.delete(key));
429 this._inFlightPromises.set(key, promise);
430
431 return promise;
432 }
434 >
435 > export class DynamicAuthProvider implements vscode.AuthenticationProvider {
436 > id: string;
437 > readonly label: string;
438 >
439 > private _onDidChangeSessions = new Emitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
440 > readonly onDidChangeSessions = this._onDidChangeSessions.event;
441 >
442 > private readonly _onDidChangeClientId = new Emitter<void>();
443 > readonly onDidChangeClientId = this._onDidChangeClientId.event;
444 >
445 > private readonly _tokenStore: TokenStore;
446 >
447 > protected readonly _createFlows: Array<{
448 > label: string;
449 > handler: (scopes: string[], progress: vscode.Progress<{ message: string }>, token: vscode.CancellationToken) => Promise<IAuthorizationTokenResponse>;
450 > }>;
451 >
452 > protected readonly _logger: ILogger;
453 > private readonly _disposable: DisposableStore;
454 >
455 > constructor(
456 @IExtHostWindow protected readonly _extHostWindow: IExtHostWindow,
457 @IExtHostUrlsService protected readonly _extHostUrls: IExtHostUrlsService,
458 @IExtHostInitDataService protected readonly _initData: IExtHostInitDataService,
459 @IExtHostProgress private readonly _extHostProgress: IExtHostProgress,
460 @ILoggerService loggerService: ILoggerService,
461 protected readonly _proxy: MainThreadAuthenticationShape,
462 readonly authorizationServer: URI,
463 protected readonly _serverMetadata: IAuthorizationServerMetadata,
464 protected readonly _resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
465 protected _clientId: string,
466 protected _clientSecret: string | undefined,
467 onDidDynamicAuthProviderTokensChange: Emitter<{ authProviderId: string; clientId: string; tokens: IAuthorizationToken[] }>,
468 initialTokens: IAuthorizationToken[],
469 private readonly _fetch: typeof fetch = fetch,
470 ) {
471 const stringifiedServer = authorizationServer.toString(true);
472 // Auth Provider Id is a combination of the authorization server and the resource, if provided.
473 this.id = _resourceMetadata?.resource
474 ? stringifiedServer + ' ' + _resourceMetadata?.resource
475 : stringifiedServer;
476 // Auth Provider label is just the resource name if provided, otherwise the authority of the authorization server.
477 this.label = _resourceMetadata?.resource_name ?? this.authorizationServer.authority;
478
479 this._logger = loggerService.createLogger(this.id, { name: `Auth: ${this.label}` });
480 this._disposable = new DisposableStore();
481 this._disposable.add(this._onDidChangeSessions);
482 this._disposable.add(this._onDidChangeClientId);
483 const scopedEvent = Event.chain(onDidDynamicAuthProviderTokensChange.event, $ => $
484 .filter(e => e.authProviderId === this.id && e.clientId === _clientId)
485 .map(e => e.tokens)
486 );
487 this._tokenStore = this._disposable.add(new TokenStore(
488 {
489 onDidChange: scopedEvent,
490 set: (tokens) => _proxy.$setSessionsForDynamicAuthProvider(this.id, this.clientId, tokens),
491 },
492 initialTokens,
493 this._logger
494 ));
495 this._disposable.add(this._tokenStore.onDidChangeSessions(e => this._onDidChangeSessions.fire(e)));
496 // Will be extended later to support other flows
497 this._createFlows = [];
498 if (_serverMetadata.authorization_endpoint) {
499 this._createFlows.push({
500 label: nls.localize('url handler', "URL Handler"),
501 handler: (scopes, progress, token) => this._createWithUrlHandler(scopes, progress, token)
502 });
503 }
504 }
506 > get clientId(): string {
507 return this._clientId;
508 }
510 > get clientSecret(): string | undefined {
511 return this._clientSecret;
512 }
514 > async getSessions(scopes: readonly string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession[]> {
515 this._logger.info(`Getting sessions for scopes: ${scopes?.join(' ') ?? 'all'}`);
516 if (!scopes) {
517 return this._tokenStore.sessions;
518 }
519 // The oauth spec says tthat order doesn't matter so we sort the scopes for easy comparison
520 // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
521 // TODO@TylerLeonhardt: Do this for all scope handling in the auth APIs
522 const sortedScopes = [...scopes].sort();
523 const scopeStr = scopes.join(' ');
524 let sessions = this._tokenStore.sessions.filter(session => arraysEqual([...session.scopes].sort(), sortedScopes));
525 this._logger.info(`Found ${sessions.length} sessions for scopes: ${scopeStr}`);
526 if (sessions.length) {
527 const newTokens: IAuthorizationToken[] = [];
528 const removedTokens: IAuthorizationToken[] = [];
529 const tokenMap = new Map<string, IAuthorizationToken>(this._tokenStore.tokens.map(token => [token.access_token, token]));
530 for (const session of sessions) {
531 const token = tokenMap.get(session.accessToken);
532 if (token && token.expires_in) {
533 const now = Date.now();
534 const expiresInMS = token.expires_in * 1000;
535 // Check if the token is about to expire in 5 minutes or if it is expired
536 if (now > token.created_at + expiresInMS - (5 * 60 * 1000)) {
537 this._logger.info(`Token for session ${session.id} is about to expire, refreshing...`);
538 removedTokens.push(token);
539 if (!token.refresh_token) {
540 // No refresh token available, cannot refresh
541 this._logger.warn(`No refresh token available for scopes ${session.scopes.join(' ')}. Throwing away token.`);
542 continue;
543 }
544 try {
545 const newToken = await this.exchangeRefreshTokenForToken(token.refresh_token, options.silent !== true);
546 // TODO@TylerLeonhardt: When the core scope handling doesn't care about order, this check should be
547 // updated to not care about order
548 if (newToken.scope !== scopeStr) {
549 this._logger.warn(`Token scopes '${newToken.scope}' do not match requested scopes '${scopeStr}'. Overwriting token with what was requested...`);
550 newToken.scope = scopeStr;
551 }
552 this._logger.info(`Successfully created a new token for scopes ${session.scopes.join(' ')}.`);
553 newTokens.push(newToken);
554 } catch (err) {
555 this._logger.error(`Failed to refresh token: ${err}`);
556 }
557
558 }
559 }
560 }
561 if (newTokens.length || removedTokens.length) {
562 this._tokenStore.update({ added: newTokens, removed: removedTokens });
563 // Since we updated the tokens, we need to re-filter the sessions
564 // to get the latest state
565 sessions = this._tokenStore.sessions.filter(session => arraysEqual([...session.scopes].sort(), sortedScopes));
566 }
567 this._logger.info(`Found ${sessions.length} sessions for scopes: ${scopeStr}`);
568 return sessions;
569 }
570 return [];
571 }
573 > async createSession(scopes: string[], _options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
574 this._logger.info(`Creating session for scopes: ${scopes.join(' ')}`);
575 let token: IAuthorizationTokenResponse | undefined;
576 for (let i = 0; i < this._createFlows.length; i++) {
577 const { handler } = this._createFlows[i];
578 try {
579 token = await this._extHostProgress.withProgressFromSource(
580 { label: this.label, id: this.id },
581 {
582 location: ProgressLocation.Notification,
583 title: nls.localize('authenticatingTo', "Authenticating to '{0}'", this.label),
584 cancellable: true
585 },
586 (progress, token) => handler(scopes, progress, token));
587 if (token) {
588 break;
589 }
590 } catch (err) {
591 const nextMode = this._createFlows[i + 1]?.label;
592 if (!nextMode) {
593 break; // No more flows to try
594 }
595 const message = isCancellationError(err)
596 ? nls.localize('userCanceledContinue', "Having trouble authenticating to '{0}'? Would you like to try a different way? ({1})", this.label, nextMode)
597 : nls.localize('continueWith', "You have not yet finished authenticating to '{0}'. Would you like to try a different way? ({1})", this.label, nextMode);
598
599 const result = await this._proxy.$showContinueNotification(message);
600 if (!result) {
601 throw new CancellationError();
602 }
603 this._logger.error(`Failed to create token via flow '${nextMode}': ${err}`);
604 }
605 }
606 if (!token) {
607 throw new Error('Failed to create authentication token');
608 }
609 if (token.scope !== scopes.join(' ')) {
610 this._logger.warn(`Token scopes '${token.scope}' do not match requested scopes '${scopes.join(' ')}'. Overwriting token with what was requested...`);
611 token.scope = scopes.join(' ');
612 }
613
614 // Store session for later retrieval
615 this._tokenStore.update({ added: [{ ...token, created_at: Date.now() }], removed: [] });
616 const session = this._tokenStore.sessions.find(t => t.accessToken === token.access_token)!;
617 this._logger.info(`Created ${token.refresh_token ? 'refreshable' : 'non-refreshable'} session for scopes: ${token.scope}${token.expires_in ? ` that expires in ${token.expires_in} seconds` : ''}`);
618 return session;
619 }
621 > async removeSession(sessionId: string): Promise<void> {
622 this._logger.info(`Removing session with id: ${sessionId}`);
623 const session = this._tokenStore.sessions.find(session => session.id === sessionId);
624 if (!session) {
625 this._logger.error(`Session with id ${sessionId} not found`);
626 return;
627 }
628 const token = this._tokenStore.tokens.find(token => token.access_token === session.accessToken);
629 if (!token) {
630 this._logger.error(`Failed to retrieve token for removed session: ${session.id}`);
631 return;
632 }
633 this._tokenStore.update({ added: [], removed: [token] });
634 this._logger.info(`Removed token for session: ${session.id} with scopes: ${session.scopes.join(' ')}`);
635 }
637 > dispose(): void {
638 this._disposable.dispose();
639 }
641 > private async _createWithUrlHandler(scopes: string[], progress: vscode.Progress<IProgressStep>, token: vscode.CancellationToken): Promise<IAuthorizationTokenResponse> {
642 if (!this._serverMetadata.authorization_endpoint) {
643 throw new Error('Authorization Endpoint required');
644 }
645 if (!this._serverMetadata.token_endpoint) {
646 throw new Error('Token endpoint not available in server metadata');
647 }
648
649 // Generate PKCE code verifier (random string) and code challenge (SHA-256 hash of verifier)
650 const codeVerifier = this.generateRandomString(64);
651 const codeChallenge = await this.generateCodeChallenge(codeVerifier);
652
653 // Generate a random state value to prevent CSRF
654 const nonce = this.generateRandomString(32);
655 const callbackUri = URI.parse(`${this._initData.environment.appUriScheme}://dynamicauthprovider/${this.authorizationServer.authority}/authorize?nonce=${nonce}`);
656 let state: URI;
657 try {
658 state = await this._extHostUrls.createAppUri(callbackUri);
659 } catch (error) {
660 throw new Error(`Failed to create external URI: ${error}`);
661 }
662
663 // Prepare the authorization request URL
664 const authorizationUrl = new URL(this._serverMetadata.authorization_endpoint);
665 authorizationUrl.searchParams.append('client_id', this._clientId);
666 authorizationUrl.searchParams.append('response_type', 'code');
667 authorizationUrl.searchParams.append('state', state.toString());
668 authorizationUrl.searchParams.append('code_challenge', codeChallenge);
669 authorizationUrl.searchParams.append('code_challenge_method', 'S256');
670 const scopeString = scopes.join(' ');
671 if (scopeString) {
672 // If non-empty scopes are provided, include scope parameter in the request
673 authorizationUrl.searchParams.append('scope', scopeString);
674 }
675 if (this._resourceMetadata?.resource) {
676 // If a resource is specified, include it in the request
677 authorizationUrl.searchParams.append('resource', this._resourceMetadata.resource);
678 }
679
680 // Use a redirect URI that matches what was registered during dynamic registration
681 const redirectUri = 'https://vscode.dev/redirect';
682 authorizationUrl.searchParams.append('redirect_uri', redirectUri);
683
684 const promise = this.waitForAuthorizationCode(callbackUri);
685
686 // Open the browser for user authorization
687 this._logger.info(`Opening authorization URL for scopes: ${scopeString}`);
688 this._logger.trace(`Authorization URL: ${authorizationUrl.toString()}`);
689 const opened = await this._extHostWindow.openUri(authorizationUrl.toString(), {});
690 if (!opened) {
691 throw new CancellationError();
692 }
693 progress.report({
694 message: nls.localize('completeAuth', "Complete the authentication in the browser window that has opened."),
695 });
696
697 // Wait for the authorization code via a redirect
698 let code: string | undefined;
699 try {
700 const response = await raceCancellationError(promise, token);
701 code = response.code;
702 } catch (err) {
703 if (isCancellationError(err)) {
704 this._logger.info('Authorization code request was cancelled by the user.');
705 throw err;
706 }
707 this._logger.error(`Failed to receive authorization code: ${err}`);
708 throw new Error(`Failed to receive authorization code: ${err}`);
709 }
710 this._logger.info(`Authorization code received for scopes: ${scopeString}`);
711
712 // Exchange the authorization code for tokens
713 const tokenResponse = await this.exchangeCodeForToken(code, codeVerifier, redirectUri);
714 return tokenResponse;
715 }
717 > protected generateRandomString(length: number): string {
718 const array = new Uint8Array(length);
719 crypto.getRandomValues(array);
720 return Array.from(array)
721 .map(b => b.toString(16).padStart(2, '0'))
722 .join('')
723 .substring(0, length);
724 }
726 > protected async generateCodeChallenge(codeVerifier: string): Promise<string> {
727 const encoder = new TextEncoder();
728 const data = encoder.encode(codeVerifier);
729 const digest = await crypto.subtle.digest('SHA-256', data);
730
731 // Base64url encode the digest
732 return encodeBase64(VSBuffer.wrap(new Uint8Array(digest)), false, false)
733 .replace(/\+/g, '-')
734 .replace(/\//g, '_')
735 .replace(/=+$/, '');
736 }
738 > private async waitForAuthorizationCode(expectedState: URI): Promise<{ code: string }> {
739 const result = await this._proxy.$waitForUriHandler(expectedState);
740 // Extract the code parameter directly from the query string. NOTE, URLSearchParams does not work here because
741 // it will decode the query string and we need to keep it encoded.
742 const codeMatch = /[?&]code=([^&]+)/.exec(result.query || '');
743 if (!codeMatch || codeMatch.length < 2) {
744 // No code parameter found in the query string
745 throw new Error('Authentication failed: No authorization code received');
746 }
747 return { code: codeMatch[1] };
748 }
750 > protected async exchangeCodeForToken(code: string, codeVerifier: string, redirectUri: string): Promise<IAuthorizationTokenResponse> {
751 if (!this._serverMetadata.token_endpoint) {
752 throw new Error('Token endpoint not available in server metadata');
753 }
754
755 const tokenRequest = new URLSearchParams();
756 tokenRequest.append('client_id', this._clientId);
757 tokenRequest.append('grant_type', 'authorization_code');
758 tokenRequest.append('code', code);
759 tokenRequest.append('redirect_uri', redirectUri);
760 tokenRequest.append('code_verifier', codeVerifier);
761
762 // Add resource indicator if available (RFC 8707)
763 if (this._resourceMetadata?.resource) {
764 tokenRequest.append('resource', this._resourceMetadata.resource);
765 }
766
767 // Add client secret if available
768 if (this._clientSecret) {
769 tokenRequest.append('client_secret', this._clientSecret);
770 }
771
772 this._logger.info('Exchanging authorization code for token...');
773 this._logger.trace(`Url: ${this._serverMetadata.token_endpoint}`);
774 this._logger.trace(`Token request body: ${tokenRequest.toString()}`);
775 let response: Response;
776 try {
777 response = await this._fetch(this._serverMetadata.token_endpoint, {
778 method: 'POST',
779 headers: {
780 'Content-Type': 'application/x-www-form-urlencoded',
781 'Accept': 'application/json'
782 },
783 body: tokenRequest.toString()
784 });
785 } catch (err) {
786 this._logger.error(`Failed to exchange authorization code for token: ${err}`);
787 throw new Error(`Failed to exchange authorization code for token: ${err}`);
788 }
789
790 if (!response.ok) {
791 const text = await response.text();
792 throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${text}`);
793 }
794
795 const result = await response.json();
796 if (isAuthorizationTokenResponse(result)) {
797 this._logger.info(`Successfully exchanged authorization code for token.`);
798 return result;
799 } else if (isAuthorizationErrorResponse(result) && result.error === AuthorizationErrorType.InvalidClient) {
800 this._logger.warn(`Client ID (${this._clientId}) was invalid, generated a new one.`);
801 await this._generateNewClientId();
802 throw new Error(`Client ID was invalid, generated a new one. Please try again.`);
803 }
804 throw new Error(`Invalid authorization token response: ${JSON.stringify(result)}`);
805 }
807 > protected async exchangeRefreshTokenForToken(refreshToken: string, allowClientRegistration: boolean): Promise<IAuthorizationToken> {
808 if (!this._serverMetadata.token_endpoint) {
809 throw new Error('Token endpoint not available in server metadata');
810 }
811
812 const tokenRequest = new URLSearchParams();
813 tokenRequest.append('client_id', this._clientId);
814 tokenRequest.append('grant_type', 'refresh_token');
815 tokenRequest.append('refresh_token', refreshToken);
816
817 // Add resource indicator if available (RFC 8707)
818 if (this._resourceMetadata?.resource) {
819 tokenRequest.append('resource', this._resourceMetadata.resource);
820 }
821
822 // Add client secret if available
823 if (this._clientSecret) {
824 tokenRequest.append('client_secret', this._clientSecret);
825 }
826
827 const response = await this._fetch(this._serverMetadata.token_endpoint, {
828 method: 'POST',
829 headers: {
830 'Content-Type': 'application/x-www-form-urlencoded',
831 'Accept': 'application/json'
832 },
833 body: tokenRequest.toString()
834 });
835
836 const result = await response.json();
837 if (isAuthorizationTokenResponse(result)) {
838 return {
839 ...result,
840 created_at: Date.now(),
841 };
842 } else if (isAuthorizationErrorResponse(result) && result.error === AuthorizationErrorType.InvalidClient) {
843 if (!allowClientRegistration) {
844 this._logger.warn(`Client ID (${this._clientId}) was invalid while silently refreshing the token.`);
845 throw new Error(`Client ID was invalid while silently refreshing the token.`);
846 }
847 this._logger.warn(`Client ID (${this._clientId}) was invalid, generated a new one.`);
848 await this._generateNewClientId();
849 throw new Error(`Client ID was invalid, generated a new one. Please try again.`);
850 }
851 throw new Error(`Invalid authorization token response: ${JSON.stringify(result)}`);
852 }
854 > protected async _generateNewClientId(): Promise<void> {
855 try {
856 const registration = await fetchDynamicRegistration(this._serverMetadata, this._initData.environment.appName, this._resourceMetadata?.scopes_supported);
857 this._clientId = registration.client_id;
858 this._clientSecret = registration.client_secret;
859 this._onDidChangeClientId.fire();
860 } catch (err) {
861 // When DCR fails, try to prompt the user for a client ID and client secret
862 this._logger.info(`Dynamic registration failed for ${this.authorizationServer.toString()}: ${err}. Prompting user for client ID and client secret.`);
863
864 try {
865 const clientDetails = await this._proxy.$promptForClientRegistration(this.authorizationServer.toString());
866 if (!clientDetails) {
867 throw new Error('User did not provide client details');
868 }
869 this._clientId = clientDetails.clientId;
870 this._clientSecret = clientDetails.clientSecret;
871 this._logger.info(`User provided client ID for ${this.authorizationServer.toString()}`);
872 if (clientDetails.clientSecret) {
873 this._logger.info(`User provided client secret for ${this.authorizationServer.toString()}`);
874 } else {
875 this._logger.info(`User did not provide client secret for ${this.authorizationServer.toString()} (optional)`);
876 }
877
878 this._onDidChangeClientId.fire();
879 } catch (promptErr) {
880 this._logger.error(`Failed to fetch new client ID and user did not provide one: ${err}`);
881 throw new Error(`Failed to fetch new client ID and user did not provide one: ${err}`);
882 }
883 }
884 }
886 >
887 > export type IAuthorizationToken = IAuthorizationTokenResponse & {
888 > /**
889 > * The time when the token was created, in milliseconds since the epoch.
890 > */
891 > created_at: number;
892 > };
893 >
894 > export class TokenStore implements Disposable {
895 > private readonly _tokensObservable: ISettableObservable<IAuthorizationToken[]>;
896 > private readonly _sessionsObservable: IObservable<vscode.AuthenticationSession[]>;
897 >
898 > private readonly _onDidChangeSessions = new Emitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
899 > readonly onDidChangeSessions = this._onDidChangeSessions.event;
900 >
901 > private readonly _disposable: DisposableStore;
902 >
903 > constructor(
904 private readonly _persistence: { onDidChange: Event<IAuthorizationToken[]>; set: (tokens: IAuthorizationToken[]) => void },
905 initialTokens: IAuthorizationToken[],
906 private readonly _logger: ILogger
907 ) {
908 this._disposable = new DisposableStore();
909 this._tokensObservable = observableValue<IAuthorizationToken[]>('tokens', initialTokens);
910 this._sessionsObservable = derivedOpts(
911 { equalsFn: (a, b) => arraysEqual(a, b, (a, b) => a.accessToken === b.accessToken) },
912 (reader) => this._tokensObservable.read(reader).map(t => this._getSessionFromToken(t))
913 );
914 this._disposable.add(this._registerChangeEventAutorun());
915 this._disposable.add(this._persistence.onDidChange((tokens) => this._tokensObservable.set(tokens, undefined)));
916 }
918 > get tokens(): IAuthorizationToken[] {
919 return this._tokensObservable.get();
920 }
922 > get sessions(): vscode.AuthenticationSession[] {
923 return this._sessionsObservable.get();
924 }
926 > dispose() {
927 this._disposable.dispose();
928 }
930 > update({ added, removed }: { added: IAuthorizationToken[]; removed: IAuthorizationToken[] }): void {
931 this._logger.trace(`Updating tokens: added ${added.length}, removed ${removed.length}`);
932 const currentTokens = [...this._tokensObservable.get()];
933 for (const token of removed) {
934 const index = currentTokens.findIndex(t => t.access_token === token.access_token);
935 if (index !== -1) {
936 currentTokens.splice(index, 1);
937 }
938 }
939 for (const token of added) {
940 const index = currentTokens.findIndex(t => t.access_token === token.access_token);
941 if (index === -1) {
942 currentTokens.push(token);
943 } else {
944 currentTokens[index] = token;
945 }
946 }
947 if (added.length || removed.length) {
948 this._tokensObservable.set(currentTokens, undefined);
949 void this._persistence.set(currentTokens);
950 }
951 this._logger.trace(`Tokens updated: ${currentTokens.length} tokens stored.`);
952 }
954 > private _registerChangeEventAutorun(): IDisposable {
955 let previousSessions: vscode.AuthenticationSession[] = [];
956 return autorun((reader) => {
957 this._logger.trace('Checking for session changes...');
958 const currentSessions = this._sessionsObservable.read(reader);
959 if (previousSessions === currentSessions) {
960 this._logger.trace('No session changes detected.');
961 return;
962 }
963
964 if (!currentSessions || currentSessions.length === 0) {
965 // If currentSessions is undefined, all previous sessions are considered removed
966 this._logger.trace('All sessions removed.');
967 if (previousSessions.length > 0) {
968 this._onDidChangeSessions.fire({
969 added: [],
970 removed: previousSessions,
971 changed: []
972 });
973 previousSessions = [];
974 }
975 return;
976 }
977
978 const added: vscode.AuthenticationSession[] = [];
979 const removed: vscode.AuthenticationSession[] = [];
980
981 // Find added sessions
982 for (const current of currentSessions) {
983 const exists = previousSessions.some(prev => prev.accessToken === current.accessToken);
984 if (!exists) {
985 added.push(current);
986 }
987 }
988
989 // Find removed sessions
990 for (const prev of previousSessions) {
991 const exists = currentSessions.some(current => current.accessToken === prev.accessToken);
992 if (!exists) {
993 removed.push(prev);
994 }
995 }
996
997 // Fire the event if there are any changes
998 if (added.length > 0 || removed.length > 0) {
999 this._logger.trace(`Sessions changed: added ${added.length}, removed ${removed.length}`);
1000 this._onDidChangeSessions.fire({ added, removed, changed: [] });
1001 }
1002
1003 // Update previous sessions reference
1004 previousSessions = currentSessions;
1005 });
1006 }
1008 > private _getSessionFromToken(token: IAuthorizationTokenResponse): vscode.AuthenticationSession {
1009 let claims: IAuthorizationJWTClaims | undefined;
1010 if (token.id_token) {
1011 try {
1012 claims = getClaimsFromJWT(token.id_token);
1013 } catch (e) {
1014 // log
1015 }
1016 }
1017 if (!claims) {
1018 try {
1019 claims = getClaimsFromJWT(token.access_token);
1020 } catch (e) {
1021 // log
1022 }
1023 }
1024 // An explicit empty `token.scope` is authoritative (createSession/refresh stamp the requested scopes onto the token); only fall back to the JWT claims when scope is genuinely absent.
1025 const scopes = token.scope !== undefined
1026 ? (token.scope ? token.scope.split(' ') : [])
1027 : (claims?.scope ? claims.scope.split(' ') : []);
1028 return {
1029 id: stringHash(token.access_token, 0).toString(),
1030 accessToken: token.access_token,
1031 account: {
1032 id: claims?.sub || 'unknown',
1033 // TODO: Don't say MCP...
1034 label: claims?.preferred_username || claims?.name || claims?.email || 'MCP',
1035 },
1036 scopes: scopes,
1037 idToken: token.id_token
1038 };
1039 }