Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/microsoft-authentication/src/node/authProvider.ts
5220 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
import { AccountInfo, AuthenticationResult, AuthError, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node';
6
import { AuthenticationChallenge, AuthenticationConstraint, AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode';
7
import { Environment } from '@azure/ms-rest-azure-env';
8
import { CachedPublicClientApplicationManager } from './publicClientCache';
9
import { UriEventHandler } from '../UriEventHandler';
10
import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache';
11
import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter';
12
import { ScopeData } from '../common/scopeData';
13
import { EventBufferer } from '../common/event';
14
import { BetterTokenStorage } from '../betterSecretStorage';
15
import { ExtensionHost, getMsalFlows } from './flows';
16
import { base64Decode } from './buffer';
17
import { Config } from '../common/config';
18
import { isSupportedClient } from '../common/env';
19
20
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
21
const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a';
22
23
/**
24
* Interface for sessions stored from the old authentication flow.
25
* Used for migration purposes when upgrading to MSAL.
26
* TODO: Remove this after one or two releases.
27
*/
28
export interface IStoredSession {
29
id: string;
30
refreshToken: string;
31
scope: string; // Scopes are alphabetized and joined with a space
32
account: {
33
label: string;
34
id: string;
35
};
36
endpoint: string | undefined;
37
}
38
39
export class MsalAuthProvider implements AuthenticationProvider {
40
41
private readonly _disposables: { dispose(): void }[];
42
private readonly _eventBufferer = new EventBufferer();
43
44
/**
45
* Event to signal a change in authentication sessions for this provider.
46
*/
47
private readonly _onDidChangeSessionsEmitter = new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
48
49
/**
50
* Event to signal a change in authentication sessions for this provider.
51
*
52
* NOTE: This event is handled differently in the Microsoft auth provider than "typical" auth providers. Normally,
53
* this event would fire when the provider's sessions change... which are tied to a specific list of scopes. However,
54
* since Microsoft identity doesn't care too much about scopes (you can mint a new token from an existing token),
55
* we just fire this event whenever the account list changes... so essentially there is one session per account.
56
*
57
* This is not quite how the API should be used... but this event really is just for signaling that the account list
58
* has changed.
59
*/
60
onDidChangeSessions = this._onDidChangeSessionsEmitter.event;
61
62
private constructor(
63
private readonly _context: ExtensionContext,
64
private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter,
65
private readonly _logger: LogOutputChannel,
66
private readonly _uriHandler: UriEventHandler,
67
private readonly _publicClientManager: ICachedPublicClientApplicationManager,
68
private readonly _env: Environment = Environment.AzureCloud
69
) {
70
this._disposables = _context.subscriptions;
71
const accountChangeEvent = this._eventBufferer.wrapEvent(
72
this._publicClientManager.onDidAccountsChange,
73
(last, newEvent) => {
74
if (!last) {
75
return newEvent;
76
}
77
const mergedEvent = {
78
added: [...(last.added ?? []), ...(newEvent.added ?? [])],
79
deleted: [...(last.deleted ?? []), ...(newEvent.deleted ?? [])],
80
changed: [...(last.changed ?? []), ...(newEvent.changed ?? [])]
81
};
82
83
const dedupedEvent = {
84
added: Array.from(new Map(mergedEvent.added.map(item => [item.username, item])).values()),
85
deleted: Array.from(new Map(mergedEvent.deleted.map(item => [item.username, item])).values()),
86
changed: Array.from(new Map(mergedEvent.changed.map(item => [item.username, item])).values())
87
};
88
89
return dedupedEvent;
90
},
91
{ added: new Array<AccountInfo>(), deleted: new Array<AccountInfo>(), changed: new Array<AccountInfo>() }
92
)(e => this._handleAccountChange(e));
93
this._disposables.push(
94
this._onDidChangeSessionsEmitter,
95
accountChangeEvent
96
);
97
}
98
99
static async create(
100
context: ExtensionContext,
101
telemetryReporter: MicrosoftAuthenticationTelemetryReporter,
102
logger: LogOutputChannel,
103
uriHandler: UriEventHandler,
104
env: Environment = Environment.AzureCloud
105
): Promise<MsalAuthProvider> {
106
const publicClientManager = await CachedPublicClientApplicationManager.create(context.secrets, logger, telemetryReporter, env);
107
context.subscriptions.push(publicClientManager);
108
const authProvider = new MsalAuthProvider(context, telemetryReporter, logger, uriHandler, publicClientManager, env);
109
await authProvider.initialize();
110
return authProvider;
111
}
112
113
/**
114
* Migrate sessions from the old secret storage to MSAL.
115
* TODO: MSAL Migration. Remove this when we remove the old flow.
116
*/
117
private async _migrateSessions() {
118
const betterSecretStorage = new BetterTokenStorage<IStoredSession>('microsoft.login.keylist', this._context);
119
const sessions = await betterSecretStorage.getAll(item => {
120
item.endpoint ||= Environment.AzureCloud.activeDirectoryEndpointUrl;
121
return item.endpoint === this._env.activeDirectoryEndpointUrl;
122
});
123
this._context.globalState.update('msalMigration', true);
124
125
const clientTenantMap = new Map<string, { clientId: string; tenant: string; refreshTokens: string[] }>();
126
127
for (const session of sessions) {
128
const scopeData = new ScopeData(session.scope.split(' '));
129
const key = `${scopeData.clientId}:${scopeData.tenant}`;
130
if (!clientTenantMap.has(key)) {
131
clientTenantMap.set(key, { clientId: scopeData.clientId, tenant: scopeData.tenant, refreshTokens: [] });
132
}
133
clientTenantMap.get(key)!.refreshTokens.push(session.refreshToken);
134
}
135
136
for (const { clientId, tenant, refreshTokens } of clientTenantMap.values()) {
137
await this._publicClientManager.getOrCreate(clientId, { refreshTokensToMigrate: refreshTokens, tenant });
138
}
139
}
140
141
private async initialize(): Promise<void> {
142
if (!this._context.globalState.get('msalMigration', false)) {
143
await this._migrateSessions();
144
}
145
146
// Send telemetry for existing accounts
147
for (const cachedPca of this._publicClientManager.getAll()) {
148
for (const account of cachedPca.accounts) {
149
const tid = account.tenantId;
150
const type = tid === MSA_TID || tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD;
151
this._telemetryReporter.sendAccountEvent([], type);
152
}
153
}
154
}
155
156
/**
157
* See {@link onDidChangeSessions} for more information on how this is used.
158
* @param param0 Event that contains the added and removed accounts
159
*/
160
private _handleAccountChange({ added, changed, deleted }: { added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }) {
161
this._logger.debug(`[_handleAccountChange] added: ${added.length}, changed: ${changed.length}, deleted: ${deleted.length}`);
162
this._onDidChangeSessionsEmitter.fire({
163
added: added.map(this.sessionFromAccountInfo),
164
changed: changed.map(this.sessionFromAccountInfo),
165
removed: deleted.map(this.sessionFromAccountInfo)
166
});
167
}
168
169
//#region AuthenticationProvider methods
170
171
async getSessions(scopes: string[] | undefined, options: AuthenticationGetSessionOptions = {}): Promise<AuthenticationSession[]> {
172
const askingForAll = scopes === undefined;
173
const scopeData = new ScopeData(scopes, undefined, options?.authorizationServer);
174
// Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead.
175
this._logger.info('[getSessions]', askingForAll ? '[all]' : `[${scopeData.scopeStr}]`, 'starting');
176
177
// This branch only gets called by Core for sign out purposes and initial population of the account menu. Since we are
178
// living in a world where a "session" from Core's perspective is an account, we return 1 session per account.
179
// See the large comment on `onDidChangeSessions` for more information.
180
if (askingForAll) {
181
const allSessionsForAccounts = new Map<string, AuthenticationSession>();
182
for (const cachedPca of this._publicClientManager.getAll()) {
183
for (const account of cachedPca.accounts) {
184
if (allSessionsForAccounts.has(account.homeAccountId)) {
185
continue;
186
}
187
allSessionsForAccounts.set(account.homeAccountId, this.sessionFromAccountInfo(account));
188
}
189
}
190
const allSessions = Array.from(allSessionsForAccounts.values());
191
this._logger.info('[getSessions] [all]', `returned ${allSessions.length} session(s)`);
192
return allSessions;
193
}
194
195
const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId);
196
const sessions = await this.getAllSessionsForPca(cachedPca, scopeData, options?.account);
197
this._logger.info(`[getSessions] [${scopeData.scopeStr}] returned ${sessions.length} session(s)`);
198
return sessions;
199
200
}
201
202
async createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
203
const scopeData = new ScopeData(scopes, undefined, options.authorizationServer);
204
// Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead.
205
206
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting');
207
const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId);
208
209
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
210
let userCancelled: boolean | undefined;
211
const yes = l10n.t('Yes');
212
const no = l10n.t('No');
213
const promptToContinue = async (mode: string) => {
214
if (userCancelled === undefined) {
215
// We haven't had a failure yet so wait to prompt
216
return;
217
}
218
const message = userCancelled
219
? l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
220
: l10n.t('You have not yet finished authorizing this extension to use your Microsoft Account. Would you like to try a different way? ({0})', mode);
221
const result = await window.showWarningMessage(message, yes, no);
222
if (result !== yes) {
223
throw new CancellationError();
224
}
225
};
226
227
const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));
228
const flows = getMsalFlows({
229
extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote,
230
supportedClient: isSupportedClient(callbackUri),
231
isBrokerSupported: cachedPca.isBrokerAvailable,
232
isPortableMode: env.isAppPortable
233
});
234
235
const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString();
236
let lastError: Error | undefined;
237
for (const flow of flows) {
238
if (flow !== flows[0]) {
239
try {
240
await promptToContinue(flow.label);
241
} finally {
242
this._telemetryReporter.sendLoginFailedEvent();
243
}
244
}
245
try {
246
const result = await flow.trigger({
247
cachedPca,
248
authority,
249
scopes: scopeData.scopesToSend,
250
loginHint: options.account?.label,
251
windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined,
252
logger: this._logger,
253
uriHandler: this._uriHandler,
254
callbackUri
255
});
256
257
const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes);
258
this._telemetryReporter.sendLoginEvent(session.scopes);
259
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session');
260
return session;
261
} catch (e) {
262
lastError = e;
263
if (e instanceof ServerError || (e as ClientAuthError)?.errorCode === ClientAuthErrorCodes.userCanceled) {
264
this._telemetryReporter.sendLoginFailedEvent();
265
throw e;
266
}
267
// Continue to next flow
268
if (e instanceof CancellationError) {
269
userCancelled = true;
270
}
271
}
272
}
273
274
this._telemetryReporter.sendLoginFailedEvent();
275
throw lastError ?? new Error('No auth flow succeeded');
276
}
277
278
async removeSession(sessionId: string): Promise<void> {
279
this._logger.info('[removeSession]', sessionId, 'starting');
280
const promises = new Array<Promise<void>>();
281
for (const cachedPca of this._publicClientManager.getAll()) {
282
const accounts = cachedPca.accounts;
283
for (const account of accounts) {
284
if (account.homeAccountId === sessionId) {
285
this._telemetryReporter.sendLogoutEvent();
286
promises.push(cachedPca.removeAccount(account));
287
this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] removing session...`);
288
}
289
}
290
}
291
if (!promises.length) {
292
this._logger.info('[removeSession]', sessionId, 'session not found');
293
return;
294
}
295
const results = await Promise.allSettled(promises);
296
for (const result of results) {
297
if (result.status === 'rejected') {
298
this._telemetryReporter.sendLogoutFailedEvent();
299
this._logger.error('[removeSession]', sessionId, 'error removing session', result.reason);
300
}
301
}
302
303
this._logger.info('[removeSession]', sessionId, `attempted to remove ${promises.length} sessions`);
304
}
305
306
async getSessionsFromChallenges(constraint: AuthenticationConstraint, options: AuthenticationProviderSessionOptions): Promise<readonly AuthenticationSession[]> {
307
this._logger.info('[getSessionsFromChallenges]', 'starting with', constraint.challenges.length, 'challenges');
308
309
// Use scopes from challenges if provided, otherwise use fallback scopes
310
const scopes = this.extractScopesFromChallenges(constraint.challenges) ?? constraint.fallbackScopes;
311
if (!scopes || scopes.length === 0) {
312
throw new Error('No scopes found in authentication challenges or fallback scopes');
313
}
314
const claims = this.extractClaimsFromChallenges(constraint.challenges);
315
if (!claims) {
316
throw new Error('No claims found in authentication challenges');
317
}
318
const scopeData = new ScopeData(scopes, claims, options?.authorizationServer);
319
this._logger.info('[getSessionsFromChallenges]', `[${scopeData.scopeStr}]`, 'with claims:', scopeData.claims);
320
321
const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId);
322
const sessions = await this.getAllSessionsForPca(cachedPca, scopeData, options?.account);
323
324
this._logger.info('[getSessionsFromChallenges]', 'returning', sessions.length, 'sessions');
325
return sessions;
326
}
327
328
async createSessionFromChallenges(constraint: AuthenticationConstraint, options: AuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
329
this._logger.info('[createSessionFromChallenges]', 'starting with', constraint.challenges.length, 'challenges');
330
331
// Use scopes from challenges if provided, otherwise use fallback scopes
332
const scopes = this.extractScopesFromChallenges(constraint.challenges) ?? constraint.fallbackScopes;
333
if (!scopes || scopes.length === 0) {
334
throw new Error('No scopes found in authentication challenges or fallback scopes');
335
}
336
const claims = this.extractClaimsFromChallenges(constraint.challenges);
337
338
// Use scopes if available, otherwise fall back to default scopes
339
const effectiveScopes = scopes.length > 0 ? scopes : ['https://graph.microsoft.com/User.Read'];
340
341
const scopeData = new ScopeData(effectiveScopes, claims, options.authorizationServer);
342
this._logger.info('[createSessionFromChallenges]', `[${scopeData.scopeStr}]`, 'starting with claims:', claims);
343
344
const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId);
345
346
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
347
let userCancelled: boolean | undefined;
348
const yes = l10n.t('Yes');
349
const no = l10n.t('No');
350
const promptToContinue = async (mode: string) => {
351
if (userCancelled === undefined) {
352
// We haven't had a failure yet so wait to prompt
353
return;
354
}
355
const message = userCancelled
356
? l10n.t('Having trouble logging in? Would you like to try a different way? ({0})', mode)
357
: l10n.t('You have not yet finished authorizing this extension to use your Microsoft Account. Would you like to try a different way? ({0})', mode);
358
const result = await window.showWarningMessage(message, yes, no);
359
if (result !== yes) {
360
throw new CancellationError();
361
}
362
};
363
364
const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));
365
const flows = getMsalFlows({
366
extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote,
367
isBrokerSupported: cachedPca.isBrokerAvailable,
368
supportedClient: isSupportedClient(callbackUri),
369
isPortableMode: env.isAppPortable
370
});
371
372
const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString();
373
let lastError: Error | undefined;
374
for (const flow of flows) {
375
if (flow !== flows[0]) {
376
try {
377
await promptToContinue(flow.label);
378
} finally {
379
this._telemetryReporter.sendLoginFailedEvent();
380
}
381
}
382
try {
383
// Create the authentication request with claims if provided
384
const authRequest = {
385
cachedPca,
386
authority,
387
scopes: scopeData.scopesToSend,
388
loginHint: options.account?.label,
389
windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined,
390
logger: this._logger,
391
uriHandler: this._uriHandler,
392
claims: scopeData.claims,
393
callbackUri
394
};
395
396
const result = await flow.trigger(authRequest);
397
398
const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes);
399
this._telemetryReporter.sendLoginEvent(session.scopes);
400
this._logger.info('[createSessionFromChallenges]', `[${scopeData.scopeStr}]`, 'returned session');
401
return session;
402
} catch (e) {
403
lastError = e as Error;
404
if (e instanceof ClientAuthError && e.errorCode === ClientAuthErrorCodes.userCanceled) {
405
this._logger.info('[createSessionFromChallenges]', `[${scopeData.scopeStr}]`, 'user cancelled');
406
userCancelled = true;
407
continue;
408
}
409
this._logger.error('[createSessionFromChallenges]', `[${scopeData.scopeStr}]`, 'error', e);
410
throw e;
411
}
412
}
413
414
this._telemetryReporter.sendLoginFailedEvent();
415
throw lastError ?? new Error('No auth flow succeeded');
416
}
417
418
private extractScopesFromChallenges(challenges: readonly AuthenticationChallenge[]): string[] | undefined {
419
for (const challenge of challenges) {
420
if (challenge.scheme.toLowerCase() === 'bearer' && challenge.params.scope) {
421
return challenge.params.scope.split(' ');
422
}
423
}
424
return undefined;
425
}
426
427
private extractClaimsFromChallenges(challenges: readonly AuthenticationChallenge[]): string | undefined {
428
for (const challenge of challenges) {
429
if (challenge.scheme.toLowerCase() === 'bearer' && challenge.params.claims) {
430
try {
431
return base64Decode(challenge.params.claims);
432
} catch (e) {
433
this._logger.warn('[extractClaimsFromChallenges]', 'failed to decode claims... checking if it is already JSON', e);
434
try {
435
JSON.parse(challenge.params.claims);
436
return challenge.params.claims;
437
} catch (e) {
438
this._logger.error('[extractClaimsFromChallenges]', 'failed to parse claims as JSON... returning undefined', e);
439
}
440
}
441
}
442
}
443
return undefined;
444
}
445
446
//#endregion
447
448
private async getAllSessionsForPca(
449
cachedPca: ICachedPublicClientApplication,
450
scopeData: ScopeData,
451
accountFilter?: AuthenticationSessionAccountInformation
452
): Promise<AuthenticationSession[]> {
453
let filteredAccounts = accountFilter
454
? cachedPca.accounts.filter(a => a.homeAccountId === accountFilter.id)
455
: cachedPca.accounts;
456
457
// Group accounts by homeAccountId
458
const accountGroups = new Map<string, AccountInfo[]>();
459
for (const account of filteredAccounts) {
460
const existing = accountGroups.get(account.homeAccountId) || [];
461
existing.push(account);
462
accountGroups.set(account.homeAccountId, existing);
463
}
464
465
// Filter to one account per homeAccountId
466
filteredAccounts = Array.from(accountGroups.values()).map(accounts => {
467
if (accounts.length === 1) {
468
return accounts[0];
469
}
470
471
// If we have a specific tenant to target, prefer that one
472
if (scopeData.tenantId) {
473
const matchingTenant = accounts.find(a => a.tenantId === scopeData.tenantId);
474
if (matchingTenant) {
475
return matchingTenant;
476
}
477
}
478
479
// Otherwise prefer the home tenant
480
return accounts.find(a => a.tenantId === a.idTokenClaims?.tid) || accounts[0];
481
});
482
483
const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString();
484
const sessions: AuthenticationSession[] = [];
485
return this._eventBufferer.bufferEventsAsync(async () => {
486
for (const account of filteredAccounts) {
487
try {
488
let forceRefresh: true | undefined;
489
if (scopeData.tenantId) {
490
// If the tenants do not match, then we need to skip the cache
491
// to get a new token for the new tenant
492
if (account.tenantId !== scopeData.tenantId) {
493
forceRefresh = true;
494
}
495
} else {
496
// If we are requesting the home tenant and we don't yet have
497
// a token for the home tenant, we need to skip the cache
498
// to get a new token for the home tenant
499
if (account.tenantId !== account.idTokenClaims?.tid) {
500
forceRefresh = true;
501
}
502
}
503
// When claims are present, force refresh to ensure we get a token that satisfies the claims
504
let claims: string | undefined;
505
if (scopeData.claims) {
506
forceRefresh = true;
507
claims = scopeData.claims;
508
}
509
let redirectUri: string | undefined;
510
// If we have the broker available and are on macOS, we HAVE to include the redirect URI or MSAL will throw an error.
511
// HOWEVER, if we are _not_ using the broker, we MUST NOT include the redirect URI or MSAL will throw an error.
512
if (cachedPca.isBrokerAvailable && process.platform === 'darwin') {
513
redirectUri = Config.macOSBrokerRedirectUri;
514
}
515
this._logger.trace(`[getAllSessionsForPca] [${scopeData.scopeStr}] [${account.environment}] [${account.username}] acquiring token silently with${forceRefresh ? ' ' : 'out '}force refresh${claims ? ' and claims' : ''}...`);
516
const result = await cachedPca.acquireTokenSilent({
517
account,
518
authority,
519
scopes: scopeData.scopesToSend,
520
claims,
521
redirectUri,
522
forceRefresh
523
});
524
sessions.push(this.sessionFromAuthenticationResult(result, scopeData.originalScopes));
525
} catch (e) {
526
// If we can't get a token silently, the account is probably in a bad state so we should skip it
527
// MSAL will log this already, so we don't need to log it again
528
if (e instanceof AuthError) {
529
this._telemetryReporter.sendTelemetryClientAuthErrorEvent(e);
530
} else {
531
this._telemetryReporter.sendTelemetryErrorEvent(e);
532
}
533
this._logger.info(`[getAllSessionsForPca] [${scopeData.scopeStr}] [${account.username}] failed to acquire token silently, skipping account`, JSON.stringify(e));
534
continue;
535
}
536
}
537
return sessions;
538
});
539
}
540
541
private sessionFromAuthenticationResult(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } {
542
return {
543
accessToken: result.accessToken,
544
idToken: result.idToken,
545
id: result.account?.homeAccountId ?? result.uniqueId,
546
account: {
547
id: result.account?.homeAccountId ?? result.uniqueId,
548
label: result.account?.username.toLowerCase() ?? 'Unknown',
549
},
550
scopes
551
};
552
}
553
554
private sessionFromAccountInfo(account: AccountInfo): AuthenticationSession {
555
return {
556
accessToken: '1234',
557
id: account.homeAccountId,
558
scopes: [],
559
account: {
560
id: account.homeAccountId,
561
label: account.username.toLowerCase(),
562
},
563
idToken: account.idToken,
564
};
565
}
566
}
567
568