Path: blob/main/src/vs/workbench/api/browser/mainThreadAuthentication.ts
5237 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, IAuthenticationWwwAuthenticateRequest } from '../../services/authentication/common/authentication.js';9import { ExtHostAuthenticationShape, ExtHostContext, IRegisterAuthenticationProviderDetails, IRegisterDynamicAuthenticationProviderDetails, 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 { ActivationKind, IExtensionService } from '../../services/extensions/common/extensions.js';14import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';15import { Emitter, Event } from '../../../base/common/event.js';16import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js';17import { IAuthenticationUsageService } from '../../services/authentication/browser/authenticationUsageService.js';18import { getAuthenticationProviderActivationEvent } from '../../services/authentication/browser/authenticationService.js';19import { URI, UriComponents } from '../../../base/common/uri.js';20import { IOpenerService } from '../../../platform/opener/common/opener.js';21import { CancellationError } from '../../../base/common/errors.js';22import { ILogService } from '../../../platform/log/common/log.js';23import { ExtensionHostKind } from '../../services/extensions/common/extensionHostKind.js';24import { IURLService } from '../../../platform/url/common/url.js';25import { DeferredPromise, raceTimeout } from '../../../base/common/async.js';26import { IAuthorizationTokenResponse } from '../../../base/common/oauth.js';27import { IDynamicAuthenticationProviderStorageService } from '../../services/authentication/common/dynamicAuthenticationProviderStorage.js';28import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';29import { IQuickInputService } from '../../../platform/quickinput/common/quickInput.js';30import { IProductService } from '../../../platform/product/common/productService.js';3132export interface AuthenticationInteractiveOptions {33detail?: string;34learnMore?: UriComponents;35sessionToRecreate?: AuthenticationSession;36}3738export interface AuthenticationGetSessionOptions {39clearSessionPreference?: boolean;40createIfNone?: boolean | AuthenticationInteractiveOptions;41forceNewSession?: boolean | AuthenticationInteractiveOptions;42silent?: boolean;43account?: AuthenticationSessionAccount;44authorizationServer?: UriComponents;45}4647class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider {4849readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;5051constructor(52protected readonly _proxy: ExtHostAuthenticationShape,53public readonly id: string,54public readonly label: string,55public readonly supportsMultipleAccounts: boolean,56public readonly authorizationServers: ReadonlyArray<URI>,57public readonly resourceServer: URI | undefined,58onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,59) {60super();61this.onDidChangeSessions = onDidChangeSessionsEmitter.event;62}6364async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) {65return this._proxy.$getSessions(this.id, scopes, options);66}6768createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {69return this._proxy.$createSession(this.id, scopes, options);70}7172async removeSession(sessionId: string): Promise<void> {73await this._proxy.$removeSession(this.id, sessionId);74}75}7677class MainThreadAuthenticationProviderWithChallenges extends MainThreadAuthenticationProvider implements IAuthenticationProvider {7879constructor(80proxy: ExtHostAuthenticationShape,81id: string,82label: string,83supportsMultipleAccounts: boolean,84authorizationServers: ReadonlyArray<URI>,85resourceServer: URI | undefined,86onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,87) {88super(89proxy,90id,91label,92supportsMultipleAccounts,93authorizationServers,94resourceServer,95onDidChangeSessionsEmitter96);97}9899getSessionsFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<readonly AuthenticationSession[]> {100return this._proxy.$getSessionsFromChallenges(this.id, constraint, options);101}102103createSessionFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {104return this._proxy.$createSessionFromChallenges(this.id, constraint, options);105}106}107108@extHostNamedCustomer(MainContext.MainThreadAuthentication)109export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape {110private readonly _proxy: ExtHostAuthenticationShape;111112private readonly _registrations = this._register(new DisposableMap<string>());113private _sentProviderUsageEvents = new Set<string>();114private _suppressUnregisterEvent = false;115116constructor(117extHostContext: IExtHostContext,118@IProductService private readonly productService: IProductService,119@IAuthenticationService private readonly authenticationService: IAuthenticationService,120@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,121@IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService,122@IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService,123@IDialogService private readonly dialogService: IDialogService,124@INotificationService private readonly notificationService: INotificationService,125@IExtensionService private readonly extensionService: IExtensionService,126@ITelemetryService private readonly telemetryService: ITelemetryService,127@IOpenerService private readonly openerService: IOpenerService,128@ILogService private readonly logService: ILogService,129@IURLService private readonly urlService: IURLService,130@IDynamicAuthenticationProviderStorageService private readonly dynamicAuthProviderStorageService: IDynamicAuthenticationProviderStorageService,131@IClipboardService private readonly clipboardService: IClipboardService,132@IQuickInputService private readonly quickInputService: IQuickInputService133) {134super();135this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);136137this._register(this.authenticationService.onDidChangeSessions(e => this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.label)));138this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {139if (!this._suppressUnregisterEvent) {140this._proxy.$onDidUnregisterAuthenticationProvider(e.id);141}142}));143this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(e => {144const providerInfo = this.authenticationService.getProvider(e.providerId);145this._proxy.$onDidChangeAuthenticationSessions(providerInfo.id, providerInfo.label, e.extensionIds);146}));147148// Listen for dynamic authentication provider token changes149this._register(this.dynamicAuthProviderStorageService.onDidChangeTokens(e => {150this._proxy.$onDidChangeDynamicAuthProviderTokens(e.authProviderId, e.clientId, e.tokens);151}));152153this._register(authenticationService.registerAuthenticationProviderHostDelegate({154// Prefer Node.js extension hosts when they're available. No CORS issues etc.155priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,156create: async (authorizationServer, serverMetadata, resource) => {157// Auth Provider Id is a combination of the authorization server and the resource, if provided.158const authProviderId = resource ? `${authorizationServer.toString(true)} ${resource.resource}` : authorizationServer.toString(true);159const clientDetails = await this.dynamicAuthProviderStorageService.getClientRegistration(authProviderId);160let clientId = clientDetails?.clientId;161const clientSecret = clientDetails?.clientSecret;162let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;163if (clientId) {164initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(authProviderId, clientId);165// If we don't already have a client id, check if the server supports the Client Id Metadata flow (see docs on the property)166// and add the "client id" if so.167} else if (serverMetadata.client_id_metadata_document_supported) {168clientId = this.productService.authClientIdMetadataUrl;169}170return await this._proxy.$registerDynamicAuthProvider(171authorizationServer,172serverMetadata,173resource,174clientId,175clientSecret,176initialTokens177);178}179}));180}181182async $registerAuthenticationProvider({ id, label, supportsMultipleAccounts, resourceServer, supportedAuthorizationServers, supportsChallenges }: IRegisterAuthenticationProviderDetails): Promise<void> {183if (!this.authenticationService.declaredProviders.find(p => p.id === id)) {184// If telemetry shows that this is not happening much, we can instead throw an error here.185this.logService.warn(`Authentication provider ${id} was not declared in the Extension Manifest.`);186type AuthProviderNotDeclaredClassification = {187owner: 'TylerLeonhardt';188comment: 'An authentication provider was not declared in the Extension Manifest.';189id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };190};191this.telemetryService.publicLog2<{ id: string }, AuthProviderNotDeclaredClassification>('authentication.providerNotDeclared', { id });192}193const emitter = new Emitter<AuthenticationSessionsChangeEvent>();194this._registrations.set(id, emitter);195const supportedAuthorizationServerUris = (supportedAuthorizationServers ?? []).map(i => URI.revive(i));196const provider =197supportsChallenges198? new MainThreadAuthenticationProviderWithChallenges(199this._proxy,200id,201label,202supportsMultipleAccounts,203supportedAuthorizationServerUris,204resourceServer ? URI.revive(resourceServer) : undefined,205emitter206)207: new MainThreadAuthenticationProvider(208this._proxy,209id,210label,211supportsMultipleAccounts,212supportedAuthorizationServerUris,213resourceServer ? URI.revive(resourceServer) : undefined,214emitter215);216this.authenticationService.registerAuthenticationProvider(id, provider);217}218219async $unregisterAuthenticationProvider(id: string): Promise<void> {220this._registrations.deleteAndDispose(id);221// The ext host side already unregisters the provider, so we can suppress the event here.222this._suppressUnregisterEvent = true;223try {224this.authenticationService.unregisterAuthenticationProvider(id);225} finally {226this._suppressUnregisterEvent = false;227}228}229230async $ensureProvider(id: string): Promise<void> {231if (!this.authenticationService.isAuthenticationProviderRegistered(id)) {232return await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate);233}234}235236async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void> {237const obj = this._registrations.get(providerId);238if (obj instanceof Emitter) {239obj.fire(event);240}241}242243$removeSession(providerId: string, sessionId: string): Promise<void> {244return this.authenticationService.removeSession(providerId, sessionId);245}246247async $waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents> {248const deferredPromise = new DeferredPromise<UriComponents>();249const disposable = this.urlService.registerHandler({250handleURL: async (uri: URI) => {251if (uri.scheme !== expectedUri.scheme || uri.authority !== expectedUri.authority || uri.path !== expectedUri.path) {252return false;253}254deferredPromise.complete(uri);255disposable.dispose();256return true;257}258});259const result = await raceTimeout(deferredPromise.p, 5 * 60 * 1000); // 5 minutes260if (!result) {261throw new Error('Timed out waiting for URI handler');262}263return await deferredPromise.p;264}265266$showContinueNotification(message: string): Promise<boolean> {267const yes = nls.localize('yes', "Yes");268const no = nls.localize('no', "No");269const deferredPromise = new DeferredPromise<boolean>();270let result = false;271const handle = this.notificationService.prompt(272Severity.Warning,273message,274[{275label: yes,276run: () => result = true277}, {278label: no,279run: () => result = false280}]);281const disposable = handle.onDidClose(() => {282deferredPromise.complete(result);283disposable.dispose();284});285return deferredPromise.p;286}287288async $registerDynamicAuthenticationProvider(details: IRegisterDynamicAuthenticationProviderDetails): Promise<void> {289await this.$registerAuthenticationProvider({290id: details.id,291label: details.label,292supportsMultipleAccounts: true,293supportedAuthorizationServers: [details.authorizationServer],294resourceServer: details.resourceServer,295});296await this.dynamicAuthProviderStorageService.storeClientRegistration(details.id, URI.revive(details.authorizationServer).toString(true), details.clientId, details.clientSecret, details.label);297}298299async $setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {300await this.dynamicAuthProviderStorageService.setSessionsForDynamicAuthProvider(authProviderId, clientId, sessions);301}302303async $sendDidChangeDynamicProviderInfo({ providerId, clientId, authorizationServer, label, clientSecret }: Partial<{ providerId: string; clientId: string; authorizationServer: UriComponents; label: string; clientSecret: string }>): Promise<void> {304this.logService.info(`Client ID for authentication provider ${providerId} changed to ${clientId}`);305const existing = this.dynamicAuthProviderStorageService.getInteractedProviders().find(p => p.providerId === providerId);306if (!existing) {307throw new Error(`Dynamic authentication provider ${providerId} not found. Has it been registered?`);308}309310// Store client credentials together311await this.dynamicAuthProviderStorageService.storeClientRegistration(312providerId || existing.providerId,313authorizationServer ? URI.revive(authorizationServer).toString(true) : existing.authorizationServer,314clientId || existing.clientId,315clientSecret,316label || existing.label317);318}319320private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationInteractiveOptions): Promise<boolean> {321let message: string;322323// Check if the provider has a custom confirmation message324const customMessage = provider.confirmation?.(extensionName, recreatingSession);325if (customMessage) {326message = customMessage;327} else {328message = recreatingSession329? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label)330: nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label);331}332333const buttons: IPromptButton<boolean | undefined>[] = [334{335label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),336run() {337return true;338},339}340];341if (options?.learnMore) {342buttons.push({343label: nls.localize('learnMore', "Learn more"),344run: async () => {345const result = this.loginPrompt(provider, extensionName, recreatingSession, options);346await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true });347return await result;348}349});350}351const { result } = await this.dialogService.prompt({352type: Severity.Info,353message,354buttons,355detail: options?.detail,356cancelButton: true,357});358359return result ?? false;360}361362private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise<boolean> {363const result = await this.dialogService.prompt({364message: nls.localize('incorrectAccount', "Incorrect account detected"),365detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel),366type: Severity.Warning,367cancelButton: true,368buttons: [369{370label: nls.localize('keep', 'Keep {0}', chosenAccountLabel),371run: () => chosenAccountLabel372},373{374label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel),375run: () => requestedAccountLabel376}377],378});379380if (!result.result) {381throw new CancellationError();382}383384return result.result === chosenAccountLabel;385}386387private async doGetSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {388const authorizationServer = URI.revive(options.authorizationServer);389const sessions = await this.authenticationService.getSessions(providerId, scopeListOrRequest, { account: options.account, authorizationServer }, true);390const provider = this.authenticationService.getProvider(providerId);391392// Error cases393if (options.forceNewSession && options.createIfNone) {394throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, createIfNone');395}396if (options.forceNewSession && options.silent) {397throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, silent');398}399if (options.createIfNone && options.silent) {400throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent');401}402403if (options.clearSessionPreference) {404// Clearing the session preference is usually paired with createIfNone, so just remove the preference and405// defer to the rest of the logic in this function to choose the session.406this.authenticationExtensionsService.removeAccountPreference(extensionId, providerId);407}408409const matchingAccountPreferenceSession =410// If an account was passed in, that takes precedence over the account preference411options.account412// We only support one session per account per set of scopes so grab the first one here413? sessions[0]414: this._getAccountPreference(extensionId, providerId, sessions);415416// Check if the sessions we have are valid417if (!options.forceNewSession && sessions.length) {418// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.419if (matchingAccountPreferenceSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingAccountPreferenceSession.account.label, extensionId)) {420return matchingAccountPreferenceSession;421}422// If we only have one account for a single auth provider, lets just check if it's allowed and return it if it is.423if (!provider.supportsMultipleAccounts && this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {424return sessions[0];425}426}427428// We may need to prompt because we don't have a valid session429// modal flows430if (options.createIfNone || options.forceNewSession) {431let uiOptions: AuthenticationInteractiveOptions | undefined;432if (typeof options.forceNewSession === 'object') {433uiOptions = options.forceNewSession;434} else if (typeof options.createIfNone === 'object') {435uiOptions = options.createIfNone;436}437438// We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions439// that we will be "forcing through".440const recreatingSession = !!(options.forceNewSession && sessions.length);441const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions);442if (!isAllowed) {443throw new Error('User did not consent to login.');444}445446let session: AuthenticationSession;447if (sessions?.length && !options.forceNewSession) {448session = provider.supportsMultipleAccounts && !options.account449? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopeListOrRequest, sessions)450: sessions[0];451} else {452const accountToCreate: AuthenticationSessionAccount | undefined = options.account ?? matchingAccountPreferenceSession?.account;453do {454session = await this.authenticationService.createSession(455providerId,456scopeListOrRequest,457{458activateImmediate: true,459account: accountToCreate,460authorizationServer461});462} while (463accountToCreate464&& accountToCreate.label !== session.account.label465&& !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label)466);467}468469this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]);470this.authenticationExtensionsService.updateNewSessionRequests(providerId, [session]);471this.authenticationExtensionsService.updateAccountPreference(extensionId, providerId, session.account);472return session;473}474475// For the silent flows, if we don't have a session that matches the account preference, we can return any valid session if there is only one to choose from.476if (!matchingAccountPreferenceSession) {477const validSessions = sessions.filter(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId));478if (validSessions.length === 1) {479return validSessions[0];480}481}482483// passive flows (silent or default)484if (!options.silent) {485// If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow,486// otherwise request a new one.487sessions.length488? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopeListOrRequest, sessions)489: await this.authenticationExtensionsService.requestNewSession(providerId, scopeListOrRequest, extensionId, extensionName);490}491return undefined;492}493494async $getSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {495const scopes = isAuthenticationWwwAuthenticateRequest(scopeListOrRequest) ? scopeListOrRequest.fallbackScopes : scopeListOrRequest;496if (scopes) {497this.sendClientIdUsageTelemetry(extensionId, providerId, scopes);498}499const session = await this.doGetSession(providerId, scopeListOrRequest, extensionId, extensionName, options);500501if (session) {502this.sendProviderUsageTelemetry(extensionId, providerId);503this.authenticationUsageService.addAccountUsage(providerId, session.account.label, session.scopes, extensionId, extensionName);504}505506return session;507}508509async $getAccounts(providerId: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {510const accounts = await this.authenticationService.getAccounts(providerId);511return accounts;512}513514// TODO@TylerLeonhardt this is a temporary addition to telemetry to understand what extensions are overriding the client id.515// We can use this telemetry to reach out to these extension authors and let them know that they many need configuration changes516// due to the adoption of the Microsoft broker.517// Remove this in a few iterations.518private _sentClientIdUsageEvents = new Set<string>();519private sendClientIdUsageTelemetry(extensionId: string, providerId: string, scopes: readonly string[]): void {520const containsVSCodeClientIdScope = scopes.some(scope => scope.startsWith('VSCODE_CLIENT_ID:'));521const key = `${extensionId}|${providerId}|${containsVSCodeClientIdScope}`;522if (this._sentClientIdUsageEvents.has(key)) {523return;524}525this._sentClientIdUsageEvents.add(key);526if (containsVSCodeClientIdScope) {527type ClientIdUsageClassification = {528owner: 'TylerLeonhardt';529comment: 'Used to see which extensions are using the VSCode client id override';530extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };531};532this.telemetryService.publicLog2<{ extensionId: string }, ClientIdUsageClassification>('authentication.clientIdUsage', { extensionId });533}534}535536private sendProviderUsageTelemetry(extensionId: string, providerId: string): void {537const key = `${extensionId}|${providerId}`;538if (this._sentProviderUsageEvents.has(key)) {539return;540}541this._sentProviderUsageEvents.add(key);542type AuthProviderUsageClassification = {543owner: 'TylerLeonhardt';544comment: 'Used to see which extensions are using which providers';545extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };546providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };547};548this.telemetryService.publicLog2<{ extensionId: string; providerId: string }, AuthProviderUsageClassification>('authentication.providerUsage', { providerId, extensionId });549}550551//#region Account Preferences552// TODO@TylerLeonhardt: Update this after a few iterations to no longer fallback to the session preference553554private _getAccountPreference(extensionId: string, providerId: string, sessions: ReadonlyArray<AuthenticationSession>): AuthenticationSession | undefined {555if (sessions.length === 0) {556return undefined;557}558const accountNamePreference = this.authenticationExtensionsService.getAccountPreference(extensionId, providerId);559if (accountNamePreference) {560const session = sessions.find(session => session.account.label === accountNamePreference);561return session;562}563return undefined;564}565//#endregion566567async $showDeviceCodeModal(userCode: string, verificationUri: string): Promise<boolean> {568const { result } = await this.dialogService.prompt({569type: Severity.Info,570message: nls.localize('deviceCodeTitle', "Device Code Authentication"),571detail: nls.localize('deviceCodeDetail', "Your code: {0}\n\nTo complete authentication, navigate to {1} and enter the code above.", userCode, verificationUri),572buttons: [573{574label: nls.localize('copyAndContinue', "Copy & Continue"),575run: () => true576}577],578cancelButton: true579});580581if (result) {582// Open verification URI583try {584await this.clipboardService.writeText(userCode);585return await this.openerService.open(URI.parse(verificationUri));586} catch (error) {587this.notificationService.error(nls.localize('failedToOpenUri', "Failed to open {0}", verificationUri));588}589}590return false;591}592593async $promptForClientRegistration(authorizationServerUrl: string): Promise<{ clientId: string; clientSecret?: string } | undefined> {594const redirectUrls = 'http://127.0.0.1:33418\nhttps://vscode.dev/redirect';595596// Show modal dialog first to explain the situation and get user consent597const result = await this.dialogService.prompt({598type: Severity.Info,599message: nls.localize('dcrNotSupported', "Dynamic Client Registration not supported"),600detail: 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:\n{1}", authorizationServerUrl, redirectUrls),601buttons: [602{603label: nls.localize('dcrCopyUrlsAndProceed', "Copy URIs & Proceed"),604run: async () => {605try {606await this.clipboardService.writeText(redirectUrls);607} catch (error) {608this.notificationService.error(nls.localize('dcrFailedToCopy', "Failed to copy redirect URIs to clipboard."));609}610return true;611}612},613],614cancelButton: {615label: nls.localize('cancel', "Cancel"),616run: () => false617}618});619620if (!result) {621return undefined;622}623624const sharedTitle = nls.localize('addClientRegistrationDetails', "Add Client Registration Details");625626const clientId = await this.quickInputService.input({627title: sharedTitle,628prompt: 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"),629placeHolder: nls.localize('clientIdPlaceholder', "OAuth client ID (azye39d...)"),630ignoreFocusLost: true,631validateInput: async (value: string) => {632if (!value || value.trim().length === 0) {633return nls.localize('clientIdRequired', "Client ID is required");634}635return undefined;636}637});638639if (!clientId || clientId.trim().length === 0) {640return undefined;641}642643const clientSecret = await this.quickInputService.input({644title: sharedTitle,645prompt: nls.localize('clientSecretPrompt', "(optional) Enter an existing client secret associated with the client id '{0}' or leave this field blank", clientId),646placeHolder: nls.localize('clientSecretPlaceholder', "OAuth client secret (wer32o50f...) or leave it blank"),647password: true,648ignoreFocusLost: true649});650651return {652clientId: clientId.trim(),653clientSecret: clientSecret?.trim() || undefined654};655}656}657658659