Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/microsoft-authentication/src/node/publicClientCache.ts
3320 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 { AccountInfo } from '@azure/msal-node';
7
import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Event } from 'vscode';
8
import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache';
9
import { CachedPublicClientApplication } from './cachedPublicClientApplication';
10
import { IAccountAccess, ScopedAccountAccess } from '../common/accountAccess';
11
import { MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter';
12
import { Environment } from '@azure/ms-rest-azure-env';
13
import { Config } from '../common/config';
14
import { DEFAULT_REDIRECT_URI } from '../common/env';
15
16
export interface IPublicClientApplicationInfo {
17
clientId: string;
18
authority: string;
19
}
20
21
export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager {
22
// The key is the clientId
23
private readonly _pcas = new Map<string, ICachedPublicClientApplication>();
24
private readonly _pcaDisposables = new Map<string, Disposable>();
25
26
private _disposable: Disposable;
27
28
private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>();
29
readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event;
30
31
private constructor(
32
private readonly _env: Environment,
33
private readonly _pcasSecretStorage: IPublicClientApplicationSecretStorage,
34
private readonly _accountAccess: IAccountAccess,
35
private readonly _secretStorage: SecretStorage,
36
private readonly _logger: LogOutputChannel,
37
private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter,
38
disposables: Disposable[]
39
) {
40
this._disposable = Disposable.from(
41
...disposables,
42
this._registerSecretStorageHandler(),
43
this._onDidAccountsChangeEmitter
44
);
45
}
46
47
static async create(
48
secretStorage: SecretStorage,
49
logger: LogOutputChannel,
50
telemetryReporter: MicrosoftAuthenticationTelemetryReporter,
51
env: Environment
52
): Promise<CachedPublicClientApplicationManager> {
53
const pcasSecretStorage = await PublicClientApplicationsSecretStorage.create(secretStorage, env.name);
54
// TODO: Remove the migrations in a version
55
const migrations = await pcasSecretStorage.getOldValue();
56
const accountAccess = await ScopedAccountAccess.create(secretStorage, env.name, logger, migrations);
57
const manager = new CachedPublicClientApplicationManager(env, pcasSecretStorage, accountAccess, secretStorage, logger, telemetryReporter, [pcasSecretStorage, accountAccess]);
58
await manager.initialize();
59
return manager;
60
}
61
62
private _registerSecretStorageHandler() {
63
return this._pcasSecretStorage.onDidChange(() => this._handleSecretStorageChange());
64
}
65
66
private async initialize() {
67
this._logger.debug('[initialize] Initializing PublicClientApplicationManager');
68
let clientIds: string[] | undefined;
69
try {
70
clientIds = await this._pcasSecretStorage.get();
71
} catch (e) {
72
// data is corrupted
73
this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e);
74
await this._pcasSecretStorage.delete();
75
}
76
if (!clientIds) {
77
return;
78
}
79
80
const promises = new Array<Promise<ICachedPublicClientApplication>>();
81
for (const clientId of clientIds) {
82
try {
83
// Load the PCA in memory
84
promises.push(this._doCreatePublicClientApplication(clientId));
85
} catch (e) {
86
this._logger.error('[initialize] Error intitializing PCA:', clientId);
87
}
88
}
89
90
const results = await Promise.allSettled(promises);
91
let pcasChanged = false;
92
for (const result of results) {
93
if (result.status === 'rejected') {
94
this._logger.error('[initialize] Error getting PCA:', result.reason);
95
} else {
96
if (!result.value.accounts.length) {
97
pcasChanged = true;
98
const clientId = result.value.clientId;
99
this._pcaDisposables.get(clientId)?.dispose();
100
this._pcaDisposables.delete(clientId);
101
this._pcas.delete(clientId);
102
this._logger.debug(`[initialize] [${clientId}] PCA disposed because it's empty.`);
103
}
104
}
105
}
106
if (pcasChanged) {
107
await this._storePublicClientApplications();
108
}
109
this._logger.debug('[initialize] PublicClientApplicationManager initialized');
110
}
111
112
dispose() {
113
this._disposable.dispose();
114
Disposable.from(...this._pcaDisposables.values()).dispose();
115
}
116
117
async getOrCreate(clientId: string, migrate?: { refreshTokensToMigrate?: string[]; tenant: string }): Promise<ICachedPublicClientApplication> {
118
let pca = this._pcas.get(clientId);
119
if (pca) {
120
this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache hit`);
121
} else {
122
this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache miss, creating new PCA...`);
123
pca = await this._doCreatePublicClientApplication(clientId);
124
await this._storePublicClientApplications();
125
this._logger.debug(`[getOrCreate] [${clientId}] PCA created.`);
126
}
127
128
// TODO: MSAL Migration. Remove this when we remove the old flow.
129
if (migrate?.refreshTokensToMigrate?.length) {
130
this._logger.debug(`[getOrCreate] [${clientId}] Migrating refresh tokens to PCA...`);
131
const authority = new URL(migrate.tenant, this._env.activeDirectoryEndpointUrl).toString();
132
let redirectUri = DEFAULT_REDIRECT_URI;
133
if (pca.isBrokerAvailable && process.platform === 'darwin') {
134
redirectUri = Config.macOSBrokerRedirectUri;
135
}
136
for (const refreshToken of migrate.refreshTokensToMigrate) {
137
try {
138
// Use the refresh token to acquire a result. This will cache the refresh token for future operations.
139
// The scopes don't matter here since we can create any token from the refresh token.
140
const result = await pca.acquireTokenByRefreshToken({
141
refreshToken,
142
forceCache: true,
143
scopes: [],
144
authority,
145
redirectUri
146
});
147
if (result?.account) {
148
this._logger.debug(`[getOrCreate] [${clientId}] Refresh token migrated to PCA.`);
149
}
150
} catch (e) {
151
this._logger.error(`[getOrCreate] [${clientId}] Error migrating refresh token:`, e);
152
}
153
}
154
}
155
return pca;
156
}
157
158
private async _doCreatePublicClientApplication(clientId: string): Promise<ICachedPublicClientApplication> {
159
const pca = await CachedPublicClientApplication.create(clientId, this._secretStorage, this._accountAccess, this._logger, this._telemetryReporter);
160
this._pcas.set(clientId, pca);
161
const disposable = Disposable.from(
162
pca,
163
pca.onDidAccountsChange(e => this._onDidAccountsChangeEmitter.fire(e)),
164
pca.onDidRemoveLastAccount(() => {
165
// The PCA has no more accounts, so we can dispose it so we're not keeping it
166
// around forever.
167
disposable.dispose();
168
this._pcaDisposables.delete(clientId);
169
this._pcas.delete(clientId);
170
this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] PCA disposed. Firing off storing of PCAs...`);
171
void this._storePublicClientApplications();
172
})
173
);
174
this._pcaDisposables.set(clientId, disposable);
175
// Fire for the initial state and only if accounts exist
176
if (pca.accounts.length > 0) {
177
this._onDidAccountsChangeEmitter.fire({ added: pca.accounts, changed: [], deleted: [] });
178
}
179
return pca;
180
}
181
182
getAll(): ICachedPublicClientApplication[] {
183
return Array.from(this._pcas.values());
184
}
185
186
private async _handleSecretStorageChange() {
187
this._logger.debug(`[_handleSecretStorageChange] Handling PCAs secret storage change...`);
188
let result: string[] | undefined;
189
try {
190
result = await this._pcasSecretStorage.get();
191
} catch (_e) {
192
// The data in secret storage has been corrupted somehow so
193
// we store what we have in this window
194
await this._storePublicClientApplications();
195
return;
196
}
197
if (!result) {
198
this._logger.debug(`[_handleSecretStorageChange] PCAs deleted in secret storage. Disposing all...`);
199
Disposable.from(...this._pcaDisposables.values()).dispose();
200
this._pcas.clear();
201
this._pcaDisposables.clear();
202
this._logger.debug(`[_handleSecretStorageChange] Finished PCAs secret storage change.`);
203
return;
204
}
205
206
const pcaKeysFromStorage = new Set(result);
207
// Handle the deleted ones
208
for (const pcaKey of this._pcas.keys()) {
209
if (!pcaKeysFromStorage.delete(pcaKey)) {
210
this._logger.debug(`[_handleSecretStorageChange] PCA was deleted in another window: ${pcaKey}`);
211
}
212
}
213
214
// Handle the new ones
215
for (const clientId of pcaKeysFromStorage) {
216
try {
217
this._logger.debug(`[_handleSecretStorageChange] [${clientId}] Creating new PCA that was created in another window...`);
218
await this._doCreatePublicClientApplication(clientId);
219
this._logger.debug(`[_handleSecretStorageChange] [${clientId}] PCA created.`);
220
} catch (_e) {
221
// This really shouldn't happen, but should we do something about this?
222
this._logger.error(`Failed to create new PublicClientApplication: ${clientId}`);
223
continue;
224
}
225
}
226
227
this._logger.debug('[_handleSecretStorageChange] Finished handling PCAs secret storage change.');
228
}
229
230
private _storePublicClientApplications() {
231
return this._pcasSecretStorage.store(Array.from(this._pcas.keys()));
232
}
233
}
234
235
interface IPublicClientApplicationSecretStorage {
236
get(): Promise<string[] | undefined>;
237
getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined>;
238
store(value: string[]): Thenable<void>;
239
delete(): Thenable<void>;
240
onDidChange: Event<void>;
241
}
242
243
class PublicClientApplicationsSecretStorage implements IPublicClientApplicationSecretStorage, Disposable {
244
private _disposable: Disposable;
245
246
private readonly _onDidChangeEmitter = new EventEmitter<void>;
247
readonly onDidChange: Event<void> = this._onDidChangeEmitter.event;
248
249
private readonly _oldKey: string;
250
private readonly _key: string;
251
252
private constructor(
253
private readonly _secretStorage: SecretStorage,
254
private readonly _cloudName: string
255
) {
256
this._oldKey = `publicClientApplications-${this._cloudName}`;
257
this._key = `publicClients-${this._cloudName}`;
258
259
this._disposable = Disposable.from(
260
this._onDidChangeEmitter,
261
this._secretStorage.onDidChange(e => {
262
if (e.key === this._key) {
263
this._onDidChangeEmitter.fire();
264
}
265
})
266
);
267
}
268
269
static async create(secretStorage: SecretStorage, cloudName: string): Promise<PublicClientApplicationsSecretStorage> {
270
const storage = new PublicClientApplicationsSecretStorage(secretStorage, cloudName);
271
await storage.initialize();
272
return storage;
273
}
274
275
/**
276
* Runs the migration.
277
* TODO: Remove this after a version.
278
*/
279
private async initialize() {
280
const oldValue = await this.getOldValue();
281
if (!oldValue) {
282
return;
283
}
284
const newValue = await this.get() ?? [];
285
for (const { clientId } of oldValue) {
286
if (!newValue.includes(clientId)) {
287
newValue.push(clientId);
288
}
289
}
290
await this.store(newValue);
291
}
292
293
async get(): Promise<string[] | undefined> {
294
const value = await this._secretStorage.get(this._key);
295
if (!value) {
296
return undefined;
297
}
298
return JSON.parse(value);
299
}
300
301
/**
302
* Old representation of data that included the authority. This should be removed in a version or 2.
303
* @returns An array of objects with clientId and authority
304
*/
305
async getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined> {
306
const value = await this._secretStorage.get(this._oldKey);
307
if (!value) {
308
return undefined;
309
}
310
const result: { clientId: string; authority: string }[] = [];
311
for (const stringifiedObj of JSON.parse(value)) {
312
const obj = JSON.parse(stringifiedObj);
313
if (obj.clientId && obj.authority) {
314
result.push(obj);
315
}
316
}
317
return result;
318
}
319
320
store(value: string[]): Thenable<void> {
321
return this._secretStorage.store(this._key, JSON.stringify(value));
322
}
323
324
delete(): Thenable<void> {
325
return this._secretStorage.delete(this._key);
326
}
327
328
dispose() {
329
this._disposable.dispose();
330
}
331
}
332
333