Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 380 additions & 0 deletions src/vs/base/common/oauth.ts

Large diffs are not rendered by default.

89 changes: 86 additions & 3 deletions src/vs/workbench/api/browser/mainThreadAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import { URI, UriComponents } from '../../../base/common/uri.js';
import { IOpenerService } from '../../../platform/opener/common/opener.js';
import { CancellationError } from '../../../base/common/errors.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { ExtensionHostKind } from '../../services/extensions/common/extensionHostKind.js';
import { IURLService } from '../../../platform/url/common/url.js';
import { DeferredPromise, Queue, raceTimeout } from '../../../base/common/async.js';
import { ISecretStorageService } from '../../../platform/secrets/common/secrets.js';
import { IAuthorizationTokenResponse, isAuthorizationTokenResponse } from '../../../base/common/oauth.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';

export interface AuthenticationInteractiveOptions {
detail?: string;
Expand Down Expand Up @@ -86,7 +92,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
@IExtensionService private readonly extensionService: IExtensionService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IOpenerService private readonly openerService: IOpenerService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IURLService private readonly urlService: IURLService,
@ISecretStorageService private readonly secretStorageService: ISecretStorageService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
Expand All @@ -98,6 +107,33 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
const providerInfo = this.authenticationService.getProvider(e.providerId);
this._proxy.$onDidChangeAuthenticationSessions(providerInfo.id, providerInfo.label, e.extensionIds);
}));
this._register(authenticationService.registerAuthenticationProviderHostDelegate({
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
create: async (serverMetadata) => {
const clientId = storageService.get(`dynamicAuthClientId/${serverMetadata.issuer}`, StorageScope.APPLICATION, undefined);
let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;
if (clientId) {
initialTokens = await this._getSessionsForDynamicAuthProvider(serverMetadata.issuer, clientId);
}
return this._proxy.$registerDynamicAuthProvider(serverMetadata, clientId, initialTokens);
}
}));
const queue = new Queue<void>();
this._register(this.secretStorageService.onDidChangeSecret(async key => {
let payload: { isDynamicAuthProvider: boolean; authProviderId: string; clientId: string } | undefined;
try {
payload = JSON.parse(key);
} catch (error) {
// Ignore errors... must not be a dynamic auth provider
}
if (payload?.isDynamicAuthProvider) {
void queue.queue(async () => {
const tokens = await this._getSessionsForDynamicAuthProvider(payload.authProviderId, payload.clientId);
this._proxy.$onDidChangeDynamicAuthProviderTokens(payload.authProviderId, payload.clientId, tokens);
});
}
}));
}

async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers: UriComponents[] = []): Promise<void> {
Expand All @@ -118,7 +154,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this.authenticationService.registerAuthenticationProvider(id, provider);
}

$unregisterAuthenticationProvider(id: string): void {
async $unregisterAuthenticationProvider(id: string): Promise<void> {
this._registrations.deleteAndDispose(id);
this.authenticationService.unregisterAuthenticationProvider(id);
}
Expand All @@ -129,7 +165,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
}
}

$sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void {
async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void> {
const obj = this._registrations.get(providerId);
if (obj instanceof Emitter) {
obj.fire(event);
Expand All @@ -139,6 +175,53 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
$removeSession(providerId: string, sessionId: string): Promise<void> {
return this.authenticationService.removeSession(providerId, sessionId);
}

async $waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents> {
const deferredPromise = new DeferredPromise<UriComponents>();
const disposable = this.urlService.registerHandler({
handleURL: async (uri: URI) => {
if (uri.scheme !== expectedUri.scheme || uri.authority !== expectedUri.authority || uri.path !== expectedUri.path) {
return false;
}
deferredPromise.complete(uri);
disposable.dispose();
return true;
}
});
const result = await raceTimeout(deferredPromise.p, 5 * 60 * 1000); // 5 minutes
if (!result) {
throw new Error('Timed out waiting for URI handler');
}
return await deferredPromise.p;
}

async $registerDynamicAuthenticationProvider(id: string, label: string, issuer: UriComponents, clientId: string): Promise<void> {
await this.$registerAuthenticationProvider(id, label, false, [issuer]);
this.storageService.store(`dynamicAuthClientId/${id}`, clientId, StorageScope.APPLICATION, StorageTarget.MACHINE);
}

private async _getSessionsForDynamicAuthProvider(authProviderId: string, clientId: string): Promise<(IAuthorizationTokenResponse & { created_at: number })[] | undefined> {
const key = JSON.stringify({ isDynamicAuthProvider: true, authProviderId, clientId });
const value = await this.secretStorageService.get(key);
if (value) {
const parsed = JSON.parse(value);
if (!Array.isArray(parsed) || !parsed.every((t) => typeof t.created_at === 'number' && isAuthorizationTokenResponse(t))) {
this.logService.error(`Invalid session data for ${authProviderId} (${clientId}) in secret storage:`, parsed);
this.secretStorageService.delete(key);
return undefined;
}
return parsed;
}
return undefined;
}

async $setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {
const key = JSON.stringify({ isDynamicAuthProvider: true, authProviderId, clientId });
const value = JSON.stringify(sessions);
await this.secretStorageService.set(key, value);
this.logService.trace(`Set session data for ${authProviderId} (${clientId}) in secret storage:`, sessions);
}

private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationInteractiveOptions): Promise<boolean> {
let message: string;

Expand Down
14 changes: 10 additions & 4 deletions src/vs/workbench/api/browser/mainThreadMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js';
import { CancellationError } from '../../../base/common/errors.js';
import { IAuthorizationServerMetadata } from '../../../base/common/oauth.js';

@extHostNamedCustomer(MainContext.MainThreadMcp)
export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
Expand Down Expand Up @@ -137,17 +138,22 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
this._servers.get(id)?.pushMessage(message);
}

async $getTokenFromServerMetadata(id: number, metadata: { issuer: string; authorizationEndpoint: string; tokenEndpoint: string; registrationEndpoint: string; scopesSupported: string[] }): Promise<string | undefined> {
async $getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata): Promise<string | undefined> {
const server = this._serverDefinitions.get(id);
if (!server) {
return undefined;
}

const issuer = URI.parse(metadata.issuer);
const scopesSupported = metadata.scopesSupported;
const providerId = await this._authenticationService.getOrActivateProviderIdForIssuer(issuer);
// Some better default?
const scopesSupported = metadata.scopes_supported || [];
let providerId = await this._authenticationService.getOrActivateProviderIdForIssuer(issuer);
if (!providerId) {
return undefined;
const provider = await this._authenticationService.createDynamicAuthenticationProvider(metadata);
if (!provider) {
return undefined;
}
providerId = provider.id;
}
const sessions = await this._authenticationService.getSessions(providerId, scopesSupported, undefined, true, issuer);
const accountNamePreference = this.authenticationMcpServersService.getAccountPreference(server.id, providerId);
Expand Down
5 changes: 3 additions & 2 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import * as typeConverters from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { ExtHostUriOpeners } from './extHostUriOpener.js';
import { IURITransformerService } from './extHostUriTransformerService.js';
import { ExtHostUrls } from './extHostUrls.js';
import { IExtHostUrlsService } from './extHostUrls.js';
import { ExtHostWebviews } from './extHostWebview.js';
import { ExtHostWebviewPanels } from './extHostWebviewPanels.js';
import { ExtHostWebviewViews } from './extHostWebviewView.js';
Expand Down Expand Up @@ -143,6 +143,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostTunnelService = accessor.get(IExtHostTunnelService);
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
const extHostWindow = accessor.get(IExtHostWindow);
const extHostUrls = accessor.get(IExtHostUrlsService);
const extHostSecretState = accessor.get(IExtHostSecretState);
const extHostEditorTabs = accessor.get(IExtHostEditorTabs);
const extHostManagedSockets = accessor.get(IExtHostManagedSockets);
Expand All @@ -159,6 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage);
rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService);
rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow);
rpcProtocol.set(ExtHostContext.ExtHostUrls, extHostUrls);
rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState);
rpcProtocol.set(ExtHostContext.ExtHostTelemetry, extHostTelemetry);
rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, extHostEditorTabs);
Expand All @@ -179,7 +181,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostLocalization = rpcProtocol.set(ExtHostContext.ExtHostLocalization, accessor.get(IExtHostLocalizationService));

// manually create and register addressable instances
const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol));
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService));
const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits)));
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.common.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ExtHostLanguageModels, IExtHostLanguageModels } from './extHostLanguage
import { IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration } from './extHostTerminalShellIntegration.js';
import { ExtHostTesting, IExtHostTesting } from './extHostTesting.js';
import { ExtHostMcpService, IExtHostMpcService } from './extHostMcp.js';
import { ExtHostUrls, IExtHostUrlsService } from './extHostUrls.js';

registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, InstantiationType.Delayed);
registerSingleton(ILoggerService, ExtHostLoggerService, InstantiationType.Delayed);
Expand All @@ -55,6 +56,7 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService, Instant
registerSingleton(IExtHostTerminalShellIntegration, ExtHostTerminalShellIntegration, InstantiationType.Eager);
registerSingleton(IExtHostTunnelService, ExtHostTunnelService, InstantiationType.Eager);
registerSingleton(IExtHostWindow, ExtHostWindow, InstantiationType.Eager);
registerSingleton(IExtHostUrlsService, ExtHostUrls, InstantiationType.Eager);
registerSingleton(IExtHostWorkspace, ExtHostWorkspace, InstantiationType.Eager);
registerSingleton(IExtHostSecretState, ExtHostSecretState, InstantiationType.Eager);
registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs, InstantiationType.Eager);
Expand Down
14 changes: 10 additions & 4 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IRelativePattern } from '../../../base/common/glob.js';
import { IMarkdownString } from '../../../base/common/htmlContent.js';
import { IJSONSchema } from '../../../base/common/jsonSchema.js';
import { IDisposable } from '../../../base/common/lifecycle.js';
import { IAuthorizationServerMetadata, IAuthorizationTokenResponse } from '../../../base/common/oauth.js';
import * as performance from '../../../base/common/performance.js';
import Severity from '../../../base/common/severity.js';
import { ThemeColor, ThemeIcon } from '../../../base/common/themables.js';
Expand Down Expand Up @@ -181,13 +182,16 @@ export interface AuthenticationGetSessionOptions {
}

export interface MainThreadAuthenticationShape extends IDisposable {
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers?: UriComponents[]): void;
$unregisterAuthenticationProvider(id: string): void;
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers?: UriComponents[]): Promise<void>;
$unregisterAuthenticationProvider(id: string): Promise<void>;
$ensureProvider(id: string): Promise<void>;
$sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void;
$sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void>;
$getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined>;
$getAccounts(providerId: string): Promise<ReadonlyArray<AuthenticationSessionAccount>>;
$removeSession(providerId: string, sessionId: string): Promise<void>;
$waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents>;
$registerDynamicAuthenticationProvider(id: string, label: string, issuer: UriComponents, clientId: string): Promise<void>;
$setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
}

export interface MainThreadSecretStateShape extends IDisposable {
Expand Down Expand Up @@ -1983,6 +1987,8 @@ export interface ExtHostAuthenticationShape {
$createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession>;
$removeSession(id: string, sessionId: string): Promise<void>;
$onDidChangeAuthenticationSessions(id: string, label: string, extensionIdFilter?: string[]): Promise<void>;
$registerDynamicAuthProvider(serverMetadata: IAuthorizationServerMetadata, clientId?: string, initialTokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
$onDidChangeDynamicAuthProviderTokens(authProviderId: string, clientId: string, tokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
}

export interface ExtHostAiRelatedInformationShape {
Expand Down Expand Up @@ -3010,7 +3016,7 @@ export interface MainThreadMcpShape {
$onDidReceiveMessage(id: number, message: string): void;
$upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void;
$deleteMcpCollection(collectionId: string): void;
$getTokenFromServerMetadata(id: number, metadata: { issuer: string; authorizationEndpoint: string; tokenEndpoint: string; registrationEndpoint: string; scopesSupported: string[] }): Promise<string | undefined>;
$getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata): Promise<string | undefined>;
}

export interface ExtHostLocalizationShape {
Expand Down
Loading