Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadAuthentication.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
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 { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
7
import * as nls from '../../../nls.js';
8
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
9
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions, isAuthenticationWwwAuthenticateRequest, IAuthenticationConstraint, IAuthenticationWwwAuthenticateRequest } from '../../services/authentication/common/authentication.js';
10
import { ExtHostAuthenticationShape, ExtHostContext, IRegisterAuthenticationProviderDetails, IRegisterDynamicAuthenticationProviderDetails, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol.js';
11
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
12
import Severity from '../../../base/common/severity.js';
13
import { INotificationService } from '../../../platform/notification/common/notification.js';
14
import { ActivationKind, IExtensionService } from '../../services/extensions/common/extensions.js';
15
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';
16
import { Emitter, Event } from '../../../base/common/event.js';
17
import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js';
18
import { IAuthenticationUsageService } from '../../services/authentication/browser/authenticationUsageService.js';
19
import { getAuthenticationProviderActivationEvent } from '../../services/authentication/browser/authenticationService.js';
20
import { URI, UriComponents } from '../../../base/common/uri.js';
21
import { IOpenerService } from '../../../platform/opener/common/opener.js';
22
import { CancellationError } from '../../../base/common/errors.js';
23
import { ILogService } from '../../../platform/log/common/log.js';
24
import { ExtensionHostKind } from '../../services/extensions/common/extensionHostKind.js';
25
import { IURLService } from '../../../platform/url/common/url.js';
26
import { DeferredPromise, raceTimeout } from '../../../base/common/async.js';
27
import { IAuthorizationTokenResponse } from '../../../base/common/oauth.js';
28
import { IDynamicAuthenticationProviderStorageService } from '../../services/authentication/common/dynamicAuthenticationProviderStorage.js';
29
import { IClipboardService } from '../../../platform/clipboard/common/clipboardService.js';
30
import { IQuickInputService } from '../../../platform/quickinput/common/quickInput.js';
31
import { IProductService } from '../../../platform/product/common/productService.js';
32
33
export interface AuthenticationInteractiveOptions {
34
detail?: string;
35
learnMore?: UriComponents;
36
sessionToRecreate?: AuthenticationSession;
37
}
38
39
export interface AuthenticationGetSessionOptions {
40
clearSessionPreference?: boolean;
41
createIfNone?: boolean | AuthenticationInteractiveOptions;
42
forceNewSession?: boolean | AuthenticationInteractiveOptions;
43
silent?: boolean;
44
account?: AuthenticationSessionAccount;
45
authorizationServer?: UriComponents;
46
}
47
48
class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider {
49
50
readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;
51
52
constructor(
53
protected readonly _proxy: ExtHostAuthenticationShape,
54
public readonly id: string,
55
public readonly label: string,
56
public readonly supportsMultipleAccounts: boolean,
57
public readonly authorizationServers: ReadonlyArray<URI>,
58
public readonly resourceServer: URI | undefined,
59
onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,
60
) {
61
super();
62
this.onDidChangeSessions = onDidChangeSessionsEmitter.event;
63
}
64
65
async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) {
66
return this._proxy.$getSessions(this.id, scopes, options);
67
}
68
69
createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
70
return this._proxy.$createSession(this.id, scopes, options);
71
}
72
73
async removeSession(sessionId: string): Promise<void> {
74
await this._proxy.$removeSession(this.id, sessionId);
75
}
76
}
77
78
class MainThreadAuthenticationProviderWithChallenges extends MainThreadAuthenticationProvider implements IAuthenticationProvider {
79
80
constructor(
81
proxy: ExtHostAuthenticationShape,
82
id: string,
83
label: string,
84
supportsMultipleAccounts: boolean,
85
authorizationServers: ReadonlyArray<URI>,
86
resourceServer: URI | undefined,
87
onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,
88
) {
89
super(
90
proxy,
91
id,
92
label,
93
supportsMultipleAccounts,
94
authorizationServers,
95
resourceServer,
96
onDidChangeSessionsEmitter
97
);
98
}
99
100
getSessionsFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<readonly AuthenticationSession[]> {
101
return this._proxy.$getSessionsFromChallenges(this.id, constraint, options);
102
}
103
104
createSessionFromChallenges(constraint: IAuthenticationConstraint, options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
105
return this._proxy.$createSessionFromChallenges(this.id, constraint, options);
106
}
107
}
108
109
@extHostNamedCustomer(MainContext.MainThreadAuthentication)
110
export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape {
111
private readonly _proxy: ExtHostAuthenticationShape;
112
113
private readonly _registrations = this._register(new DisposableMap<string>());
114
private _sentProviderUsageEvents = new Set<string>();
115
private _suppressUnregisterEvent = false;
116
117
constructor(
118
extHostContext: IExtHostContext,
119
@IProductService private readonly productService: IProductService,
120
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
121
@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,
122
@IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService,
123
@IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService,
124
@IDialogService private readonly dialogService: IDialogService,
125
@INotificationService private readonly notificationService: INotificationService,
126
@IExtensionService private readonly extensionService: IExtensionService,
127
@ITelemetryService private readonly telemetryService: ITelemetryService,
128
@IOpenerService private readonly openerService: IOpenerService,
129
@ILogService private readonly logService: ILogService,
130
@IURLService private readonly urlService: IURLService,
131
@IDynamicAuthenticationProviderStorageService private readonly dynamicAuthProviderStorageService: IDynamicAuthenticationProviderStorageService,
132
@IClipboardService private readonly clipboardService: IClipboardService,
133
@IQuickInputService private readonly quickInputService: IQuickInputService
134
) {
135
super();
136
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
137
138
this._register(this.authenticationService.onDidChangeSessions(e => this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.label)));
139
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {
140
if (!this._suppressUnregisterEvent) {
141
this._proxy.$onDidUnregisterAuthenticationProvider(e.id);
142
}
143
}));
144
this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(e => {
145
const providerInfo = this.authenticationService.getProvider(e.providerId);
146
this._proxy.$onDidChangeAuthenticationSessions(providerInfo.id, providerInfo.label, e.extensionIds);
147
}));
148
149
// Listen for dynamic authentication provider token changes
150
this._register(this.dynamicAuthProviderStorageService.onDidChangeTokens(e => {
151
this._proxy.$onDidChangeDynamicAuthProviderTokens(e.authProviderId, e.clientId, e.tokens);
152
}));
153
154
this._register(authenticationService.registerAuthenticationProviderHostDelegate({
155
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
156
priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
157
create: async (authorizationServer, serverMetadata, resource) => {
158
// Auth Provider Id is a combination of the authorization server and the resource, if provided.
159
const authProviderId = resource ? `${authorizationServer.toString(true)} ${resource.resource}` : authorizationServer.toString(true);
160
const clientDetails = await this.dynamicAuthProviderStorageService.getClientRegistration(authProviderId);
161
let clientId = clientDetails?.clientId;
162
const clientSecret = clientDetails?.clientSecret;
163
let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;
164
if (clientId) {
165
initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(authProviderId, clientId);
166
// If we don't already have a client id, check if the server supports the Client Id Metadata flow (see docs on the property)
167
// and add the "client id" if so.
168
} else if (serverMetadata.client_id_metadata_document_supported) {
169
clientId = this.productService.authClientIdMetadataUrl;
170
}
171
return await this._proxy.$registerDynamicAuthProvider(
172
authorizationServer,
173
serverMetadata,
174
resource,
175
clientId,
176
clientSecret,
177
initialTokens
178
);
179
}
180
}));
181
}
182
183
async $registerAuthenticationProvider({ id, label, supportsMultipleAccounts, resourceServer, supportedAuthorizationServers, supportsChallenges }: IRegisterAuthenticationProviderDetails): Promise<void> {
184
if (!this.authenticationService.declaredProviders.find(p => p.id === id)) {
185
// If telemetry shows that this is not happening much, we can instead throw an error here.
186
this.logService.warn(`Authentication provider ${id} was not declared in the Extension Manifest.`);
187
type AuthProviderNotDeclaredClassification = {
188
owner: 'TylerLeonhardt';
189
comment: 'An authentication provider was not declared in the Extension Manifest.';
190
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };
191
};
192
this.telemetryService.publicLog2<{ id: string }, AuthProviderNotDeclaredClassification>('authentication.providerNotDeclared', { id });
193
}
194
const emitter = new Emitter<AuthenticationSessionsChangeEvent>();
195
this._registrations.set(id, emitter);
196
const supportedAuthorizationServerUris = (supportedAuthorizationServers ?? []).map(i => URI.revive(i));
197
const provider =
198
supportsChallenges
199
? new MainThreadAuthenticationProviderWithChallenges(
200
this._proxy,
201
id,
202
label,
203
supportsMultipleAccounts,
204
supportedAuthorizationServerUris,
205
resourceServer ? URI.revive(resourceServer) : undefined,
206
emitter
207
)
208
: new MainThreadAuthenticationProvider(
209
this._proxy,
210
id,
211
label,
212
supportsMultipleAccounts,
213
supportedAuthorizationServerUris,
214
resourceServer ? URI.revive(resourceServer) : undefined,
215
emitter
216
);
217
this.authenticationService.registerAuthenticationProvider(id, provider);
218
}
219
220
async $unregisterAuthenticationProvider(id: string): Promise<void> {
221
this._registrations.deleteAndDispose(id);
222
// The ext host side already unregisters the provider, so we can suppress the event here.
223
this._suppressUnregisterEvent = true;
224
try {
225
this.authenticationService.unregisterAuthenticationProvider(id);
226
} finally {
227
this._suppressUnregisterEvent = false;
228
}
229
}
230
231
async $ensureProvider(id: string): Promise<void> {
232
if (!this.authenticationService.isAuthenticationProviderRegistered(id)) {
233
return await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate);
234
}
235
}
236
237
async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void> {
238
const obj = this._registrations.get(providerId);
239
if (obj instanceof Emitter) {
240
obj.fire(event);
241
}
242
}
243
244
$removeSession(providerId: string, sessionId: string): Promise<void> {
245
return this.authenticationService.removeSession(providerId, sessionId);
246
}
247
248
async $waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents> {
249
const deferredPromise = new DeferredPromise<UriComponents>();
250
const disposable = this.urlService.registerHandler({
251
handleURL: async (uri: URI) => {
252
if (uri.scheme !== expectedUri.scheme || uri.authority !== expectedUri.authority || uri.path !== expectedUri.path) {
253
return false;
254
}
255
deferredPromise.complete(uri);
256
disposable.dispose();
257
return true;
258
}
259
});
260
const result = await raceTimeout(deferredPromise.p, 5 * 60 * 1000); // 5 minutes
261
if (!result) {
262
throw new Error('Timed out waiting for URI handler');
263
}
264
return await deferredPromise.p;
265
}
266
267
$showContinueNotification(message: string): Promise<boolean> {
268
const yes = nls.localize('yes', "Yes");
269
const no = nls.localize('no', "No");
270
const deferredPromise = new DeferredPromise<boolean>();
271
let result = false;
272
const handle = this.notificationService.prompt(
273
Severity.Warning,
274
message,
275
[{
276
label: yes,
277
run: () => result = true
278
}, {
279
label: no,
280
run: () => result = false
281
}]);
282
const disposable = handle.onDidClose(() => {
283
deferredPromise.complete(result);
284
disposable.dispose();
285
});
286
return deferredPromise.p;
287
}
288
289
async $registerDynamicAuthenticationProvider(details: IRegisterDynamicAuthenticationProviderDetails): Promise<void> {
290
await this.$registerAuthenticationProvider({
291
id: details.id,
292
label: details.label,
293
supportsMultipleAccounts: true,
294
supportedAuthorizationServers: [details.authorizationServer],
295
resourceServer: details.resourceServer,
296
});
297
await this.dynamicAuthProviderStorageService.storeClientRegistration(details.id, URI.revive(details.authorizationServer).toString(true), details.clientId, details.clientSecret, details.label);
298
}
299
300
async $setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {
301
await this.dynamicAuthProviderStorageService.setSessionsForDynamicAuthProvider(authProviderId, clientId, sessions);
302
}
303
304
async $sendDidChangeDynamicProviderInfo({ providerId, clientId, authorizationServer, label, clientSecret }: Partial<{ providerId: string; clientId: string; authorizationServer: UriComponents; label: string; clientSecret: string }>): Promise<void> {
305
this.logService.info(`Client ID for authentication provider ${providerId} changed to ${clientId}`);
306
const existing = this.dynamicAuthProviderStorageService.getInteractedProviders().find(p => p.providerId === providerId);
307
if (!existing) {
308
throw new Error(`Dynamic authentication provider ${providerId} not found. Has it been registered?`);
309
}
310
311
// Store client credentials together
312
await this.dynamicAuthProviderStorageService.storeClientRegistration(
313
providerId || existing.providerId,
314
authorizationServer ? URI.revive(authorizationServer).toString(true) : existing.authorizationServer,
315
clientId || existing.clientId,
316
clientSecret,
317
label || existing.label
318
);
319
}
320
321
private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationInteractiveOptions): Promise<boolean> {
322
let message: string;
323
324
// Check if the provider has a custom confirmation message
325
const customMessage = provider.confirmation?.(extensionName, recreatingSession);
326
if (customMessage) {
327
message = customMessage;
328
} else {
329
message = recreatingSession
330
? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label)
331
: nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label);
332
}
333
334
const buttons: IPromptButton<boolean | undefined>[] = [
335
{
336
label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"),
337
run() {
338
return true;
339
},
340
}
341
];
342
if (options?.learnMore) {
343
buttons.push({
344
label: nls.localize('learnMore', "Learn more"),
345
run: async () => {
346
const result = this.loginPrompt(provider, extensionName, recreatingSession, options);
347
await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true });
348
return await result;
349
}
350
});
351
}
352
const { result } = await this.dialogService.prompt({
353
type: Severity.Info,
354
message,
355
buttons,
356
detail: options?.detail,
357
cancelButton: true,
358
});
359
360
return result ?? false;
361
}
362
363
private async continueWithIncorrectAccountPrompt(chosenAccountLabel: string, requestedAccountLabel: string): Promise<boolean> {
364
const result = await this.dialogService.prompt({
365
message: nls.localize('incorrectAccount', "Incorrect account detected"),
366
detail: nls.localize('incorrectAccountDetail', "The chosen account, {0}, does not match the requested account, {1}.", chosenAccountLabel, requestedAccountLabel),
367
type: Severity.Warning,
368
cancelButton: true,
369
buttons: [
370
{
371
label: nls.localize('keep', 'Keep {0}', chosenAccountLabel),
372
run: () => chosenAccountLabel
373
},
374
{
375
label: nls.localize('loginWith', 'Login with {0}', requestedAccountLabel),
376
run: () => requestedAccountLabel
377
}
378
],
379
});
380
381
if (!result.result) {
382
throw new CancellationError();
383
}
384
385
return result.result === chosenAccountLabel;
386
}
387
388
private async doGetSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
389
const authorizationServer = URI.revive(options.authorizationServer);
390
const sessions = await this.authenticationService.getSessions(providerId, scopeListOrRequest, { account: options.account, authorizationServer }, true);
391
const provider = this.authenticationService.getProvider(providerId);
392
393
// Error cases
394
if (options.forceNewSession && options.createIfNone) {
395
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, createIfNone');
396
}
397
if (options.forceNewSession && options.silent) {
398
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, silent');
399
}
400
if (options.createIfNone && options.silent) {
401
throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent');
402
}
403
404
if (options.clearSessionPreference) {
405
// Clearing the session preference is usually paired with createIfNone, so just remove the preference and
406
// defer to the rest of the logic in this function to choose the session.
407
this.authenticationExtensionsService.removeAccountPreference(extensionId, providerId);
408
}
409
410
const matchingAccountPreferenceSession =
411
// If an account was passed in, that takes precedence over the account preference
412
options.account
413
// We only support one session per account per set of scopes so grab the first one here
414
? sessions[0]
415
: this._getAccountPreference(extensionId, providerId, sessions);
416
417
// Check if the sessions we have are valid
418
if (!options.forceNewSession && sessions.length) {
419
// If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function.
420
if (matchingAccountPreferenceSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingAccountPreferenceSession.account.label, extensionId)) {
421
return matchingAccountPreferenceSession;
422
}
423
// If we only have one account for a single auth provider, lets just check if it's allowed and return it if it is.
424
if (!provider.supportsMultipleAccounts && this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {
425
return sessions[0];
426
}
427
}
428
429
// We may need to prompt because we don't have a valid session
430
// modal flows
431
if (options.createIfNone || options.forceNewSession) {
432
let uiOptions: AuthenticationInteractiveOptions | undefined;
433
if (typeof options.forceNewSession === 'object') {
434
uiOptions = options.forceNewSession;
435
} else if (typeof options.createIfNone === 'object') {
436
uiOptions = options.createIfNone;
437
}
438
439
// We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions
440
// that we will be "forcing through".
441
const recreatingSession = !!(options.forceNewSession && sessions.length);
442
const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions);
443
if (!isAllowed) {
444
throw new Error('User did not consent to login.');
445
}
446
447
let session: AuthenticationSession;
448
if (sessions?.length && !options.forceNewSession) {
449
session = provider.supportsMultipleAccounts && !options.account
450
? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopeListOrRequest, sessions)
451
: sessions[0];
452
} else {
453
const accountToCreate: AuthenticationSessionAccount | undefined = options.account ?? matchingAccountPreferenceSession?.account;
454
do {
455
session = await this.authenticationService.createSession(
456
providerId,
457
scopeListOrRequest,
458
{
459
activateImmediate: true,
460
account: accountToCreate,
461
authorizationServer
462
});
463
} while (
464
accountToCreate
465
&& accountToCreate.label !== session.account.label
466
&& !await this.continueWithIncorrectAccountPrompt(session.account.label, accountToCreate.label)
467
);
468
}
469
470
this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]);
471
this.authenticationExtensionsService.updateNewSessionRequests(providerId, [session]);
472
this.authenticationExtensionsService.updateAccountPreference(extensionId, providerId, session.account);
473
return session;
474
}
475
476
// 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.
477
if (!matchingAccountPreferenceSession) {
478
const validSessions = sessions.filter(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId));
479
if (validSessions.length === 1) {
480
return validSessions[0];
481
}
482
}
483
484
// passive flows (silent or default)
485
if (!options.silent) {
486
// If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow,
487
// otherwise request a new one.
488
sessions.length
489
? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopeListOrRequest, sessions)
490
: await this.authenticationExtensionsService.requestNewSession(providerId, scopeListOrRequest, extensionId, extensionName);
491
}
492
return undefined;
493
}
494
495
async $getSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
496
const scopes = isAuthenticationWwwAuthenticateRequest(scopeListOrRequest) ? scopeListOrRequest.fallbackScopes : scopeListOrRequest;
497
if (scopes) {
498
this.sendClientIdUsageTelemetry(extensionId, providerId, scopes);
499
}
500
const session = await this.doGetSession(providerId, scopeListOrRequest, extensionId, extensionName, options);
501
502
if (session) {
503
this.sendProviderUsageTelemetry(extensionId, providerId);
504
this.authenticationUsageService.addAccountUsage(providerId, session.account.label, session.scopes, extensionId, extensionName);
505
}
506
507
return session;
508
}
509
510
async $getAccounts(providerId: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {
511
const accounts = await this.authenticationService.getAccounts(providerId);
512
return accounts;
513
}
514
515
// TODO@TylerLeonhardt this is a temporary addition to telemetry to understand what extensions are overriding the client id.
516
// We can use this telemetry to reach out to these extension authors and let them know that they many need configuration changes
517
// due to the adoption of the Microsoft broker.
518
// Remove this in a few iterations.
519
private _sentClientIdUsageEvents = new Set<string>();
520
private sendClientIdUsageTelemetry(extensionId: string, providerId: string, scopes: readonly string[]): void {
521
const containsVSCodeClientIdScope = scopes.some(scope => scope.startsWith('VSCODE_CLIENT_ID:'));
522
const key = `${extensionId}|${providerId}|${containsVSCodeClientIdScope}`;
523
if (this._sentClientIdUsageEvents.has(key)) {
524
return;
525
}
526
this._sentClientIdUsageEvents.add(key);
527
if (containsVSCodeClientIdScope) {
528
type ClientIdUsageClassification = {
529
owner: 'TylerLeonhardt';
530
comment: 'Used to see which extensions are using the VSCode client id override';
531
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };
532
};
533
this.telemetryService.publicLog2<{ extensionId: string }, ClientIdUsageClassification>('authentication.clientIdUsage', { extensionId });
534
}
535
}
536
537
private sendProviderUsageTelemetry(extensionId: string, providerId: string): void {
538
const key = `${extensionId}|${providerId}`;
539
if (this._sentProviderUsageEvents.has(key)) {
540
return;
541
}
542
this._sentProviderUsageEvents.add(key);
543
type AuthProviderUsageClassification = {
544
owner: 'TylerLeonhardt';
545
comment: 'Used to see which extensions are using which providers';
546
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension id.' };
547
providerId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider id.' };
548
};
549
this.telemetryService.publicLog2<{ extensionId: string; providerId: string }, AuthProviderUsageClassification>('authentication.providerUsage', { providerId, extensionId });
550
}
551
552
//#region Account Preferences
553
// TODO@TylerLeonhardt: Update this after a few iterations to no longer fallback to the session preference
554
555
private _getAccountPreference(extensionId: string, providerId: string, sessions: ReadonlyArray<AuthenticationSession>): AuthenticationSession | undefined {
556
if (sessions.length === 0) {
557
return undefined;
558
}
559
const accountNamePreference = this.authenticationExtensionsService.getAccountPreference(extensionId, providerId);
560
if (accountNamePreference) {
561
const session = sessions.find(session => session.account.label === accountNamePreference);
562
return session;
563
}
564
return undefined;
565
}
566
//#endregion
567
568
async $showDeviceCodeModal(userCode: string, verificationUri: string): Promise<boolean> {
569
const { result } = await this.dialogService.prompt({
570
type: Severity.Info,
571
message: nls.localize('deviceCodeTitle', "Device Code Authentication"),
572
detail: nls.localize('deviceCodeDetail', "Your code: {0}\n\nTo complete authentication, navigate to {1} and enter the code above.", userCode, verificationUri),
573
buttons: [
574
{
575
label: nls.localize('copyAndContinue', "Copy & Continue"),
576
run: () => true
577
}
578
],
579
cancelButton: true
580
});
581
582
if (result) {
583
// Open verification URI
584
try {
585
await this.clipboardService.writeText(userCode);
586
return await this.openerService.open(URI.parse(verificationUri));
587
} catch (error) {
588
this.notificationService.error(nls.localize('failedToOpenUri', "Failed to open {0}", verificationUri));
589
}
590
}
591
return false;
592
}
593
594
async $promptForClientRegistration(authorizationServerUrl: string): Promise<{ clientId: string; clientSecret?: string } | undefined> {
595
const redirectUrls = 'http://127.0.0.1:33418\nhttps://vscode.dev/redirect';
596
597
// Show modal dialog first to explain the situation and get user consent
598
const result = await this.dialogService.prompt({
599
type: Severity.Info,
600
message: nls.localize('dcrNotSupported', "Dynamic Client Registration not supported"),
601
detail: 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),
602
buttons: [
603
{
604
label: nls.localize('dcrCopyUrlsAndProceed', "Copy URIs & Proceed"),
605
run: async () => {
606
try {
607
await this.clipboardService.writeText(redirectUrls);
608
} catch (error) {
609
this.notificationService.error(nls.localize('dcrFailedToCopy', "Failed to copy redirect URIs to clipboard."));
610
}
611
return true;
612
}
613
},
614
],
615
cancelButton: {
616
label: nls.localize('cancel', "Cancel"),
617
run: () => false
618
}
619
});
620
621
if (!result) {
622
return undefined;
623
}
624
625
const sharedTitle = nls.localize('addClientRegistrationDetails', "Add Client Registration Details");
626
627
const clientId = await this.quickInputService.input({
628
title: sharedTitle,
629
prompt: 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"),
630
placeHolder: nls.localize('clientIdPlaceholder', "OAuth client ID (azye39d...)"),
631
ignoreFocusLost: true,
632
validateInput: async (value: string) => {
633
if (!value || value.trim().length === 0) {
634
return nls.localize('clientIdRequired', "Client ID is required");
635
}
636
return undefined;
637
}
638
});
639
640
if (!clientId || clientId.trim().length === 0) {
641
return undefined;
642
}
643
644
const clientSecret = await this.quickInputService.input({
645
title: sharedTitle,
646
prompt: nls.localize('clientSecretPrompt', "(optional) Enter an existing client secret associated with the client id '{0}' or leave this field blank", clientId),
647
placeHolder: nls.localize('clientSecretPlaceholder', "OAuth client secret (wer32o50f...) or leave it blank"),
648
password: true,
649
ignoreFocusLost: true
650
});
651
652
return {
653
clientId: clientId.trim(),
654
clientSecret: clientSecret?.trim() || undefined
655
};
656
}
657
}
658
659