Path: blob/main/src/vs/workbench/api/browser/mainThreadAuthentication.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';6import * as nls from '../../../nls.js';7import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';8import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions, isAuthenticationWWWAuthenticateRequest, IAuthenticationConstraint } from '../../services/authentication/common/authentication.js';9import { AuthenticationWWWAuthenticateRequest, ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol.js';10import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';11import Severity from '../../../base/common/severity.js';12import { INotificationService } from '../../../platform/notification/common/notification.js';13import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';14import { Emitter, Event } from '../../../base/common/event.js';15import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js';16import { IAuthenticationUsageService } from '../../services/authentication/browser/authenticationUsageService.js';17import { URI, UriComponents } from '../../../base/common/uri.js';18import { IOpenerService } from '../../../platform/opener/common/opener.js';19import { CancellationError } from '../../../base/common/errors.js';20import { ILogService } from '../../../platform/log/common/log.js';21import { ExtensionHostKind } from '../../services/extensions/common/extensionHostKind.js';22import { IURLService } from '../../../platform/url/common/url.js';23import { DeferredPromise, raceTimeout } from '../../../base/common/async.js';24import { IAuthorizationTokenResponse } from '../../../base/common/oauth.js';25import { IDynamicAuthenticationProviderStorageService } from '../../services/authentication/common/dynamicAuthenticationProviderStorage.js';26import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';27import { IQuickInputService } from '../../../platform/quickinput/common/quickInput.js';2829export interface AuthenticationInteractiveOptions {30detail?: string;31learnMore?: UriComponents;32sessionToRecreate?: AuthenticationSession;33}3435export interface AuthenticationGetSessionOptions {36clearSessionPreference?: boolean;37createIfNone?: boolean | AuthenticationInteractiveOptions;38forceNewSession?: boolean | AuthenticationInteractiveOptions;39silent?: boolean;40account?: AuthenticationSessionAccount;41authorizationServer?: UriComponents;42}4344class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider {4546readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;4748constructor(49protected readonly _proxy: ExtHostAuthenticationShape,50public readonly id: string,51public readonly label: string,52public readonly supportsMultipleAccounts: boolean,53public readonly authorizationServers: ReadonlyArray<URI>,54onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,55) {56super();57this.onDidChangeSessions = onDidChangeSessionsEmitter.event;58}5960async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) {61return this._proxy.$getSessions(this.id, scopes, options);62}6364createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {65return this._proxy.$createSession(this.id, scopes, options);66}6768async removeSession(sessionId: string): Promise<void> {69await this._proxy.$removeSession(this.id, sessionId);70}71}7273class MainThreadAuthenticationProviderWithChallenges extends MainThreadAuthenticationProvider implements IAuthenticationProvider {7475constructor(76proxy: ExtHostAuthenticationShape,77id: string,78label: string,79supportsMultipleAccounts: boolean,80authorizationServers: ReadonlyArray<URI>,81onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,82) {83super(84proxy,85id,86label,87supportsMultipleAccounts,88authorizationServers,89onDidChangeSessionsEmitter90);91}9293getSessionsFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<readonly AuthenticationSession[]> {94return this._proxy.$getSessionsFromChallenges(this.id, constraint, options);95}9697createSessionFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {98return this._proxy.$createSessionFromChallenges(this.id, constraint, options);99}100}101102@extHostNamedCustomer(MainContext.MainThreadAuthentication)103export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape {104private readonly _proxy: ExtHostAuthenticationShape;105106private readonly _registrations = this._register(new DisposableMap<string>());107private _sentProviderUsageEvents = new Set<string>();108private _suppressUnregisterEvent = false;109110constructor(111extHostContext: IExtHostContext,112@IAuthenticationService private readonly authenticationService: IAuthenticationService,113@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,114@IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService,115@IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService,116@IDialogService private readonly dialogService: IDialogService,117@INotificationService private readonly notificationService: INotificationService,118@ITelemetryService private readonly telemetryService: ITelemetryService,119@IOpenerService private readonly openerService: IOpenerService,120@ILogService private readonly logService: ILogService,121@IURLService private readonly urlService: IURLService,122@IDynamicAuthenticationProviderStorageService private readonly dynamicAuthProviderStorageService: IDynamicAuthenticationProviderStorageService,123@IClipboardService private readonly clipboardService: IClipboardService,124@IQuickInputService private readonly quickInputService: IQuickInputService125) {126super();127this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);128129this._register(this.authenticationService.onDidChangeSessions(e => this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.label)));130this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {131if (!this._suppressUnregisterEvent) {132this._proxy.$onDidUnregisterAuthenticationProvider(e.id);133}134}));135this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(e => {136const providerInfo = this.authenticationService.getProvider(e.providerId);137this._proxy.$onDidChangeAuthenticationSessions(providerInfo.id, providerInfo.label, e.extensionIds);138}));139140// Listen for dynamic authentication provider token changes141this._register(this.dynamicAuthProviderStorageService.onDidChangeTokens(e => {142this._proxy.$onDidChangeDynamicAuthProviderTokens(e.authProviderId, e.clientId, e.tokens);143}));144145this._register(authenticationService.registerAuthenticationProviderHostDelegate({146// Prefer Node.js extension hosts when they're available. No CORS issues etc.147priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,148create: async (authorizationServer, serverMetadata, resource) => {149// Auth Provider Id is a combination of the authorization server and the resource, if provided.150const authProviderId = resource ? `${authorizationServer.toString(true)} ${resource.resource}` : authorizationServer.toString(true);151const clientDetails = await this.dynamicAuthProviderStorageService.getClientRegistration(authProviderId);152const clientId = clientDetails?.clientId;153const clientSecret = clientDetails?.clientSecret;154let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;155if (clientId) {156initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(authProviderId, clientId);157}158return await this._proxy.$registerDynamicAuthProvider(159authorizationServer,160serverMetadata,161resource,162clientId,163clientSecret,164initialTokens165);166}167}));168}169170async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedAuthorizationServer: UriComponents[] = [], supportsChallenges?: boolean): Promise<void> {171if (!this.authenticationService.declaredProviders.find(p => p.id === id)) {172// If telemetry shows that this is not happening much, we can instead throw an error here.173this.logService.warn(`Authentication provider ${id} was not declared in the Extension Manifest.`);174type AuthProviderNotDeclaredClassification = {175owner: 'TylerLeonhardt';176comment: 'An authentication provider was not declared in the Extension Manifest.';177id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };178};179this.telemetryService.publicLog2<{ id: string }, AuthProviderNotDeclaredClassification>('authentication.providerNotDeclared', { id });180}181const emitter = new Emitter<AuthenticationSessionsChangeEvent>();182this._registrations.set(id, emitter);183const supportedAuthorizationServerUris = supportedAuthorizationServer.map(i => URI.revive(i));184const provider =185supportsChallenges186? new MainThreadAuthenticationProviderWithChallenges(this._proxy, id, label, supportsMultipleAccounts, supportedAuthorizationServerUris, emitter)187: new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, supportedAuthorizationServerUris, emitter);188this.authenticationService.registerAuthenticationProvider(id, provider);189}190191async $unregisterAuthenticationProvider(id: string): Promise<void> {192this._registrations.deleteAndDispose(id);193// The ext host side already unregisters the provider, so we can suppress the event here.194this._suppressUnregisterEvent = true;195try {196this.authenticationService.unregisterAuthenticationProvider(id);197} finally {198this._suppressUnregisterEvent = false;199}200}201202async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void> {203const obj = this._registrations.get(providerId);204if (obj instanceof Emitter) {205obj.fire(event);206}207}208209$removeSession(providerId: string, sessionId: string): Promise<void> {210return this.authenticationService.removeSession(providerId, sessionId);211}212213async $waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents> {214const deferredPromise = new DeferredPromise<UriComponents>();215const disposable = this.urlService.registerHandler({216handleURL: async (uri: URI) => {217if (uri.scheme !== expectedUri.scheme || uri.authority !== expectedUri.authority || uri.path !== expectedUri.path) {218return false;219}220deferredPromise.complete(uri);221disposable.dispose();222return true;223}224});225const result = await raceTimeout(deferredPromise.p, 5 * 60 * 1000); // 5 minutes226if (!result) {227throw new Error('Timed out waiting for URI handler');228}229return await deferredPromise.p;230}231232$showContinueNotification(message: string): Promise<boolean> {233const yes = nls.localize('yes', "Yes");234const no = nls.localize('no', "No");235const deferredPromise = new DeferredPromise<boolean>();236let result = false;237const handle = this.notificationService.prompt(238Severity.Warning,239message,240[{241label: yes,242run: () => result = true243}, {244label: no,245run: () => result = false246}]);247const disposable = handle.onDidClose(() => {248deferredPromise.complete(result);249disposable.dispose();250});251return deferredPromise.p;252}253254async $registerDynamicAuthenticationProvider(id: string, label: string, authorizationServer: UriComponents, clientId: string, clientSecret?: string): Promise<void> {255await this.$registerAuthenticationProvider(id, label, true, [authorizationServer]);256await this.dynamicAuthProviderStorageService.storeClientRegistration(id, URI.revive(authorizationServer).toString(true), clientId, clientSecret, label);257}258259async $setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {260await this.dynamicAuthProviderStorageService.setSessionsForDynamicAuthProvider(authProviderId, clientId, sessions);261}262263async $sendDidChangeDynamicProviderInfo({ providerId, clientId, authorizationServer, label, clientSecret }: Partial<{ providerId: string; clientId: string; authorizationServer: UriComponents; label: string; clientSecret: string }>): Promise<void> {264this.logService.info(`Client ID for authentication provider ${providerId} changed to ${clientId}`);265const existing = this.dynamicAuthProviderStorageService.getInteractedProviders().find(p => p.providerId === providerId);266if (!existing) {267throw new Error(`Dynamic authentication provider ${providerId} not found. Has it been registered?`);268}269270// Store client credentials together271await this.dynamicAuthProviderStorageService.storeClientRegistration(272providerId || existing.providerId,273authorizationServer ? URI.revive(authorizationServer).toString(true) : existing.authorizationServer,274clientId || existing.clientId,275clientSecret,276label || existing.label277);278}279280private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationInteractiveOptions): Promise<boolean> {281let message: string;282283// Check if the provider has a custom confirmation message284const customMessage = provider.confirmation?.(extensionName, recreatingSession);285if (customMessage) {286message = customMessage;287} else {288message = recreatingSession289? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label)290: nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label);291}292293const buttons: IPromptButton<boolean | undefined>[] = [294{295label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),296run() {297return true;298},299}300];301if (options?.learnMore) {302buttons.push({303label: nls.localize('learnMore', "Learn more"),304run: async () => {305const result = this.loginPrompt(provider, extensionName, recreatingSession, options);306await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true });307return await result;308}309});310}311const { result } = await this.dialogService.prompt({312type: Severity.Info,313message,314buttons,315detail: options?.detail,316cancelButton: true,317});318319return result ?? false;320}321322private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise<boolean> {323const result = await this.dialogService.prompt({324message: nls.localize('incorrectAccount', "Incorrect account detected"),325detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel),326type: Severity.Warning,327cancelButton: true,328buttons: [329{330label: nls.localize('keep', 'Keep {0}', chosenAccountLabel),331run: () => chosenAccountLabel332},333{334label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel),335run: () => requestedAccountLabel336}337],338});339340if (!result.result) {341throw new CancellationError();342}343344return result.result === chosenAccountLabel;345}346347private async doGetSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | AuthenticationWWWAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {348const authorizationServer = URI.revive(options.authorizationServer);349const sessions = await this.authenticationService.getSessions(providerId, scopeListOrRequest, { account: options.account, authorizationServer }, true);350const provider = this.authenticationService.getProvider(providerId);351352// Error cases353if (options.forceNewSession && options.createIfNone) {354throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, createIfNone');355}356if (options.forceNewSession && options.silent) {357throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, silent');358}359if (options.createIfNone && options.silent) {360throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent');361}362363if (options.clearSessionPreference) {364// Clearing the session preference is usually paired with createIfNone, so just remove the preference and365// defer to the rest of the logic in this function to choose the session.366this.authenticationExtensionsService.removeAccountPreference(extensionId, providerId);367}368369const matchingAccountPreferenceSession =370// If an account was passed in, that takes precedence over the account preference371options.account372// We only support one session per account per set of scopes so grab the first one here373? sessions[0]374: this._getAccountPreference(extensionId, providerId, sessions);375376// Check if the sessions we have are valid377if (!options.forceNewSession && sessions.length) {378// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.379if (matchingAccountPreferenceSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingAccountPreferenceSession.account.label, extensionId)) {380return matchingAccountPreferenceSession;381}382// If we only have one account for a single auth provider, lets just check if it's allowed and return it if it is.383if (!provider.supportsMultipleAccounts && this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {384return sessions[0];385}386}387388// We may need to prompt because we don't have a valid session389// modal flows390if (options.createIfNone || options.forceNewSession) {391let uiOptions: AuthenticationInteractiveOptions | undefined;392if (typeof options.forceNewSession === 'object') {393uiOptions = options.forceNewSession;394} else if (typeof options.createIfNone === 'object') {395uiOptions = options.createIfNone;396}397398// We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions399// that we will be "forcing through".400const recreatingSession = !!(options.forceNewSession && sessions.length);401const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions);402if (!isAllowed) {403throw new Error('User did not consent to login.');404}405406let session: AuthenticationSession;407if (sessions?.length && !options.forceNewSession) {408session = provider.supportsMultipleAccounts && !options.account409? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopeListOrRequest, sessions)410: sessions[0];411} else {412const accountToCreate: AuthenticationSessionAccount | undefined = options.account ?? matchingAccountPreferenceSession?.account;413do {414session = await this.authenticationService.createSession(415providerId,416scopeListOrRequest,417{418activateImmediate: true,419account: accountToCreate,420authorizationServer421});422} while (423accountToCreate424&& accountToCreate.label !== session.account.label425&& !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label)426);427}428429this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]);430this.authenticationExtensionsService.updateNewSessionRequests(providerId, [session]);431this.authenticationExtensionsService.updateAccountPreference(extensionId, providerId, session.account);432return session;433}434435// For the silent flows, if we have a session but we don't have a session preference, we'll return the first one that is valid.436if (!matchingAccountPreferenceSession && !this.authenticationExtensionsService.getAccountPreference(extensionId, providerId)) {437const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId));438if (validSession) {439return validSession;440}441}442443// passive flows (silent or default)444if (!options.silent) {445// If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow,446// otherwise request a new one.447sessions.length448? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopeListOrRequest, sessions)449: await this.authenticationExtensionsService.requestNewSession(providerId, scopeListOrRequest, extensionId, extensionName);450}451return undefined;452}453454async $getSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | AuthenticationWWWAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {455const scopes = isAuthenticationWWWAuthenticateRequest(scopeListOrRequest) ? scopeListOrRequest.scopes : scopeListOrRequest;456if (scopes) {457this.sendClientIdUsageTelemetry(extensionId, providerId, scopes);458}459const session = await this.doGetSession(providerId, scopeListOrRequest, extensionId, extensionName, options);460461if (session) {462this.sendProviderUsageTelemetry(extensionId, providerId);463const scopes = isAuthenticationWWWAuthenticateRequest(scopeListOrRequest) ? scopeListOrRequest.scopes : scopeListOrRequest;464this.authenticationUsageService.addAccountUsage(providerId, session.account.label, scopes, extensionId, extensionName);465}466467return session;468}469470async $getAccounts(providerId: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {471const accounts = await this.authenticationService.getAccounts(providerId);472return accounts;473}474475// TODO@TylerLeonhardt this is a temporary addition to telemetry to understand what extensions are overriding the client id.476// We can use this telemetry to reach out to these extension authors and let them know that they many need configuration changes477// due to the adoption of the Microsoft broker.478// Remove this in a few iterations.479private _sentClientIdUsageEvents = new Set<string>();480private sendClientIdUsageTelemetry(extensionId: string, providerId: string, scopes: readonly string[]): void {481const containsVSCodeClientIdScope = scopes.some(scope => scope.startsWith('VSCODE_CLIENT_ID:'));482const key = `${extensionId}|${providerId}|${containsVSCodeClientIdScope}`;483if (this._sentClientIdUsageEvents.has(key)) {484return;485}486this._sentClientIdUsageEvents.add(key);487if (containsVSCodeClientIdScope) {488type ClientIdUsageClassification = {489owner: 'TylerLeonhardt';490comment: 'Used to see which extensions are using the VSCode client id override';491extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };492};493this.telemetryService.publicLog2<{ extensionId: string }, ClientIdUsageClassification>('authentication.clientIdUsage', { extensionId });494}495}496497private sendProviderUsageTelemetry(extensionId: string, providerId: string): void {498const key = `${extensionId}|${providerId}`;499if (this._sentProviderUsageEvents.has(key)) {500return;501}502this._sentProviderUsageEvents.add(key);503type AuthProviderUsageClassification = {504owner: 'TylerLeonhardt';505comment: 'Used to see which extensions are using which providers';506extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };507providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };508};509this.telemetryService.publicLog2<{ extensionId: string; providerId: string }, AuthProviderUsageClassification>('authentication.providerUsage', { providerId, extensionId });510}511512//#region Account Preferences513// TODO@TylerLeonhardt: Update this after a few iterations to no longer fallback to the session preference514515private _getAccountPreference(extensionId: string, providerId: string, sessions: ReadonlyArray<AuthenticationSession>): AuthenticationSession | undefined {516if (sessions.length === 0) {517return undefined;518}519const accountNamePreference = this.authenticationExtensionsService.getAccountPreference(extensionId, providerId);520if (accountNamePreference) {521const session = sessions.find(session => session.account.label === accountNamePreference);522return session;523}524return undefined;525}526//#endregion527528async $showDeviceCodeModal(userCode: string, verificationUri: string): Promise<boolean> {529const { result } = await this.dialogService.prompt({530type: Severity.Info,531message: nls.localize('deviceCodeTitle', "Device Code Authentication"),532detail: nls.localize('deviceCodeDetail', "Your code: {0}\n\nTo complete authentication, navigate to {1} and enter the code above.", userCode, verificationUri),533buttons: [534{535label: nls.localize('copyAndContinue', "Copy & Continue"),536run: () => true537}538],539cancelButton: true540});541542if (result) {543// Open verification URI544try {545await this.clipboardService.writeText(userCode);546return await this.openerService.open(URI.parse(verificationUri));547} catch (error) {548this.notificationService.error(nls.localize('failedToOpenUri', "Failed to open {0}", verificationUri));549}550}551return false;552}553554async $promptForClientRegistration(authorizationServerUrl: string): Promise<{ clientId: string; clientSecret?: string } | undefined> {555// Show modal dialog first to explain the situation and get user consent556const result = await this.dialogService.prompt({557type: Severity.Info,558message: nls.localize('dcrNotSupported', "Dynamic Client Registration not supported"),559detail: nls.localize('dcrNotSupportedDetail', "The authorization server '{0}' does not support automatic client registration. Do you want to proceed by manually providing a client registration (client ID)?\n\nNote: When registering your OAuth application, make sure to include these redirect URIs:\nhttp://127.0.0.1:33418\nhttps://vscode.dev/redirect", authorizationServerUrl),560buttons: [561{562label: nls.localize('provideClientDetails', "Proceed"),563run: () => true564}565],566cancelButton: {567label: nls.localize('cancel', "Cancel"),568run: () => false569}570});571572if (!result) {573return undefined;574}575576const sharedTitle = nls.localize('addClientRegistrationDetails', "Add Client Registration Details");577578const clientId = await this.quickInputService.input({579title: sharedTitle,580prompt: nls.localize('clientIdPrompt', "Enter an existing client ID that has been registered with the following redirect URIs: http://127.0.0.1:33418, https://vscode.dev/redirect"),581placeHolder: nls.localize('clientIdPlaceholder', "OAuth client ID (azye39d...)"),582ignoreFocusLost: true,583validateInput: async (value: string) => {584if (!value || value.trim().length === 0) {585return nls.localize('clientIdRequired', "Client ID is required");586}587return undefined;588}589});590591if (!clientId || clientId.trim().length === 0) {592return undefined;593}594595const clientSecret = await this.quickInputService.input({596title: sharedTitle,597prompt: nls.localize('clientSecretPrompt', "(optional) Enter an existing client secret associated with the client id '{0}' or leave this field blank", clientId),598placeHolder: nls.localize('clientSecretPlaceholder', "OAuth client secret (wer32o50f...) or leave it blank"),599password: true,600ignoreFocusLost: true601});602603return {604clientId: clientId.trim(),605clientSecret: clientSecret?.trim() || undefined606};607}608}609610611