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