Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/authentication/browser/authenticationService.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 { Emitter, Event } from '../../../../base/common/event.js';
7
import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';
9
import { isString } from '../../../../base/common/types.js';
10
import { localize } from '../../../../nls.js';
11
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
12
import { IProductService } from '../../../../platform/product/common/productService.js';
13
import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
14
import { IAuthenticationAccessService } from './authenticationAccessService.js';
15
import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationGetSessionsOptions, IAuthenticationProvider, IAuthenticationProviderHostDelegate, IAuthenticationService, IAuthenticationWWWAuthenticateRequest, isAuthenticationWWWAuthenticateRequest } from '../common/authentication.js';
16
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
17
import { ActivationKind, IExtensionService } from '../../extensions/common/extensions.js';
18
import { ILogService } from '../../../../platform/log/common/log.js';
19
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
20
import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';
21
import { match } from '../../../../base/common/glob.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, parseWWWAuthenticateHeader } from '../../../../base/common/oauth.js';
24
import { raceCancellation, raceTimeout } from '../../../../base/common/async.js';
25
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
26
27
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
28
29
// TODO: pull this out into its own service
30
export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean };
31
export async function getCurrentAuthenticationSessionInfo(
32
secretStorageService: ISecretStorageService,
33
productService: IProductService
34
): Promise<AuthenticationSessionInfo | undefined> {
35
const authenticationSessionValue = await secretStorageService.get(`${productService.urlProtocol}.loginAccount`);
36
if (authenticationSessionValue) {
37
try {
38
const authenticationSessionInfo: AuthenticationSessionInfo = JSON.parse(authenticationSessionValue);
39
if (authenticationSessionInfo
40
&& isString(authenticationSessionInfo.id)
41
&& isString(authenticationSessionInfo.accessToken)
42
&& isString(authenticationSessionInfo.providerId)
43
) {
44
return authenticationSessionInfo;
45
}
46
} catch (e) {
47
// This is a best effort operation.
48
console.error(`Failed parsing current auth session value: ${e}`);
49
}
50
}
51
return undefined;
52
}
53
54
const authenticationDefinitionSchema: IJSONSchema = {
55
type: 'object',
56
additionalProperties: false,
57
properties: {
58
id: {
59
type: 'string',
60
description: localize('authentication.id', 'The id of the authentication provider.')
61
},
62
label: {
63
type: 'string',
64
description: localize('authentication.label', 'The human readable name of the authentication provider.'),
65
},
66
authorizationServerGlobs: {
67
type: 'array',
68
items: {
69
type: 'string',
70
description: localize('authentication.authorizationServerGlobs', 'A list of globs that match the authorization servers that this provider supports.'),
71
},
72
description: localize('authentication.authorizationServerGlobsDescription', 'A list of globs that match the authorization servers that this provider supports.')
73
}
74
}
75
};
76
77
const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint<AuthenticationProviderInformation[]>({
78
extensionPoint: 'authentication',
79
jsonSchema: {
80
description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'),
81
type: 'array',
82
items: authenticationDefinitionSchema
83
},
84
activationEventsGenerator: (authenticationProviders, result) => {
85
for (const authenticationProvider of authenticationProviders) {
86
if (authenticationProvider.id) {
87
result.push(`onAuthenticationRequest:${authenticationProvider.id}`);
88
}
89
}
90
}
91
});
92
93
export class AuthenticationService extends Disposable implements IAuthenticationService {
94
declare readonly _serviceBrand: undefined;
95
96
private _onDidRegisterAuthenticationProvider: Emitter<AuthenticationProviderInformation> = this._register(new Emitter<AuthenticationProviderInformation>());
97
readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this._onDidRegisterAuthenticationProvider.event;
98
99
private _onDidUnregisterAuthenticationProvider: Emitter<AuthenticationProviderInformation> = this._register(new Emitter<AuthenticationProviderInformation>());
100
readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this._onDidUnregisterAuthenticationProvider.event;
101
102
private _onDidChangeSessions: Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>());
103
readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;
104
105
private _onDidChangeDeclaredProviders: Emitter<void> = this._register(new Emitter<void>());
106
readonly onDidChangeDeclaredProviders: Event<void> = this._onDidChangeDeclaredProviders.event;
107
108
private _authenticationProviders: Map<string, IAuthenticationProvider> = new Map<string, IAuthenticationProvider>();
109
private _authenticationProviderDisposables: DisposableMap<string, IDisposable> = this._register(new DisposableMap<string, IDisposable>());
110
private _dynamicAuthenticationProviderIds = new Set<string>();
111
112
private readonly _delegates: IAuthenticationProviderHostDelegate[] = [];
113
114
private _disposedSource = new CancellationTokenSource();
115
116
constructor(
117
@IExtensionService private readonly _extensionService: IExtensionService,
118
@IAuthenticationAccessService authenticationAccessService: IAuthenticationAccessService,
119
@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService,
120
@ILogService private readonly _logService: ILogService
121
) {
122
super();
123
this._register(toDisposable(() => this._disposedSource.dispose(true)));
124
this._register(authenticationAccessService.onDidChangeExtensionSessionAccess(e => {
125
// The access has changed, not the actual session itself but extensions depend on this event firing
126
// when they have gained access to an account so this fires that event.
127
this._onDidChangeSessions.fire({
128
providerId: e.providerId,
129
label: e.accountName,
130
event: {
131
added: [],
132
changed: [],
133
removed: []
134
}
135
});
136
}));
137
138
this._registerEnvContributedAuthenticationProviders();
139
this._registerAuthenticationExtentionPointHandler();
140
}
141
142
private _declaredProviders: AuthenticationProviderInformation[] = [];
143
get declaredProviders(): AuthenticationProviderInformation[] {
144
return this._declaredProviders;
145
}
146
147
private _registerEnvContributedAuthenticationProviders(): void {
148
if (!this._environmentService.options?.authenticationProviders?.length) {
149
return;
150
}
151
for (const provider of this._environmentService.options.authenticationProviders) {
152
this.registerDeclaredAuthenticationProvider(provider);
153
this.registerAuthenticationProvider(provider.id, provider);
154
}
155
}
156
157
private _registerAuthenticationExtentionPointHandler(): void {
158
this._register(authenticationExtPoint.setHandler((_extensions, { added, removed }) => {
159
this._logService.debug(`Found authentication providers. added: ${added.length}, removed: ${removed.length}`);
160
added.forEach(point => {
161
for (const provider of point.value) {
162
if (isFalsyOrWhitespace(provider.id)) {
163
point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.'));
164
continue;
165
}
166
167
if (isFalsyOrWhitespace(provider.label)) {
168
point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.'));
169
continue;
170
}
171
172
if (!this.declaredProviders.some(p => p.id === provider.id)) {
173
this.registerDeclaredAuthenticationProvider(provider);
174
this._logService.debug(`Declared authentication provider: ${provider.id}`);
175
} else {
176
point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id));
177
}
178
}
179
});
180
181
const removedExtPoints = removed.flatMap(r => r.value);
182
removedExtPoints.forEach(point => {
183
const provider = this.declaredProviders.find(provider => provider.id === point.id);
184
if (provider) {
185
this.unregisterDeclaredAuthenticationProvider(provider.id);
186
this._logService.debug(`Undeclared authentication provider: ${provider.id}`);
187
}
188
});
189
}));
190
}
191
192
registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void {
193
if (isFalsyOrWhitespace(provider.id)) {
194
throw new Error(localize('authentication.missingId', 'An authentication contribution must specify an id.'));
195
}
196
if (isFalsyOrWhitespace(provider.label)) {
197
throw new Error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.'));
198
}
199
if (this.declaredProviders.some(p => p.id === provider.id)) {
200
throw new Error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id));
201
}
202
this._declaredProviders.push(provider);
203
this._onDidChangeDeclaredProviders.fire();
204
}
205
206
unregisterDeclaredAuthenticationProvider(id: string): void {
207
const index = this.declaredProviders.findIndex(provider => provider.id === id);
208
if (index > -1) {
209
this.declaredProviders.splice(index, 1);
210
}
211
this._onDidChangeDeclaredProviders.fire();
212
}
213
214
isAuthenticationProviderRegistered(id: string): boolean {
215
return this._authenticationProviders.has(id);
216
}
217
218
isDynamicAuthenticationProvider(id: string): boolean {
219
return this._dynamicAuthenticationProviderIds.has(id);
220
}
221
222
registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void {
223
this._authenticationProviders.set(id, authenticationProvider);
224
const disposableStore = new DisposableStore();
225
disposableStore.add(authenticationProvider.onDidChangeSessions(e => this._onDidChangeSessions.fire({
226
providerId: id,
227
label: authenticationProvider.label,
228
event: e
229
})));
230
if (isDisposable(authenticationProvider)) {
231
disposableStore.add(authenticationProvider);
232
}
233
this._authenticationProviderDisposables.set(id, disposableStore);
234
this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label });
235
}
236
237
unregisterAuthenticationProvider(id: string): void {
238
const provider = this._authenticationProviders.get(id);
239
if (provider) {
240
this._authenticationProviders.delete(id);
241
// If this is a dynamic provider, remove it from the set of dynamic providers
242
if (this._dynamicAuthenticationProviderIds.has(id)) {
243
this._dynamicAuthenticationProviderIds.delete(id);
244
}
245
this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label });
246
}
247
this._authenticationProviderDisposables.deleteAndDispose(id);
248
}
249
250
getProviderIds(): string[] {
251
const providerIds: string[] = [];
252
this._authenticationProviders.forEach(provider => {
253
providerIds.push(provider.id);
254
});
255
return providerIds;
256
}
257
258
getProvider(id: string): IAuthenticationProvider {
259
if (this._authenticationProviders.has(id)) {
260
return this._authenticationProviders.get(id)!;
261
}
262
throw new Error(`No authentication provider '${id}' is currently registered.`);
263
}
264
265
async getAccounts(id: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {
266
// TODO: Cache this
267
const sessions = await this.getSessions(id);
268
const accounts = new Array<AuthenticationSessionAccount>();
269
const seenAccounts = new Set<string>();
270
for (const session of sessions) {
271
if (!seenAccounts.has(session.account.label)) {
272
seenAccounts.add(session.account.label);
273
accounts.push(session.account);
274
}
275
}
276
return accounts;
277
}
278
279
async getSessions(id: string, scopeListOrRequest?: ReadonlyArray<string> | IAuthenticationWWWAuthenticateRequest, options?: IAuthenticationGetSessionsOptions, activateImmediate: boolean = false): Promise<ReadonlyArray<AuthenticationSession>> {
280
if (this._disposedSource.token.isCancellationRequested) {
281
return [];
282
}
283
284
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate);
285
if (authProvider) {
286
// Check if the authorization server is in the list of supported authorization servers
287
if (options?.authorizationServer) {
288
const authServerStr = options.authorizationServer.toString(true);
289
// TODO: something is off here...
290
if (!authProvider.authorizationServers?.some(i => i.toString(true) === authServerStr || match(i.toString(true), authServerStr))) {
291
throw new Error(`The authorization server '${authServerStr}' is not supported by the authentication provider '${id}'.`);
292
}
293
}
294
if (isAuthenticationWWWAuthenticateRequest(scopeListOrRequest)) {
295
if (!authProvider.getSessionsFromChallenges) {
296
throw new Error(`The authentication provider '${id}' does not support getting sessions from challenges.`);
297
}
298
return await authProvider.getSessionsFromChallenges(
299
{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), scopes: scopeListOrRequest.scopes },
300
{ ...options }
301
);
302
}
303
return await authProvider.getSessions(scopeListOrRequest ? [...scopeListOrRequest] : undefined, { ...options });
304
} else {
305
throw new Error(`No authentication provider '${id}' is currently registered.`);
306
}
307
}
308
309
async createSession(id: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWWWAuthenticateRequest, options?: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession> {
310
if (this._disposedSource.token.isCancellationRequested) {
311
throw new Error('Authentication service is disposed.');
312
}
313
314
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate);
315
if (authProvider) {
316
if (isAuthenticationWWWAuthenticateRequest(scopeListOrRequest)) {
317
if (!authProvider.createSessionFromChallenges) {
318
throw new Error(`The authentication provider '${id}' does not support creating sessions from challenges.`);
319
}
320
return await authProvider.createSessionFromChallenges(
321
{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), scopes: scopeListOrRequest.scopes },
322
{ ...options }
323
);
324
}
325
return await authProvider.createSession([...scopeListOrRequest], { ...options });
326
} else {
327
throw new Error(`No authentication provider '${id}' is currently registered.`);
328
}
329
}
330
331
async removeSession(id: string, sessionId: string): Promise<void> {
332
if (this._disposedSource.token.isCancellationRequested) {
333
throw new Error('Authentication service is disposed.');
334
}
335
336
const authProvider = this._authenticationProviders.get(id);
337
if (authProvider) {
338
return authProvider.removeSession(sessionId);
339
} else {
340
throw new Error(`No authentication provider '${id}' is currently registered.`);
341
}
342
}
343
344
async getOrActivateProviderIdForServer(authorizationServer: URI): Promise<string | undefined> {
345
for (const provider of this._authenticationProviders.values()) {
346
if (provider.authorizationServers?.some(i => i.toString(true) === authorizationServer.toString(true) || match(i.toString(true), authorizationServer.toString(true)))) {
347
return provider.id;
348
}
349
}
350
351
const authServerStr = authorizationServer.toString(true);
352
const providers = this._declaredProviders
353
// Only consider providers that are not already registered since we already checked them
354
.filter(p => !this._authenticationProviders.has(p.id))
355
.filter(p => !!p.authorizationServerGlobs?.some(i => match(i, authServerStr)));
356
// TODO:@TylerLeonhardt fan out?
357
for (const provider of providers) {
358
const activeProvider = await this.tryActivateProvider(provider.id, true);
359
// Check the resolved authorization servers
360
if (activeProvider.authorizationServers?.some(i => match(i.toString(true), authServerStr))) {
361
return activeProvider.id;
362
}
363
}
364
return undefined;
365
}
366
367
async createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined> {
368
const delegate = this._delegates[0];
369
if (!delegate) {
370
this._logService.error('No authentication provider host delegate found');
371
return undefined;
372
}
373
const providerId = await delegate.create(authorizationServer, serverMetadata, resource);
374
const provider = this._authenticationProviders.get(providerId);
375
if (provider) {
376
this._logService.debug(`Created dynamic authentication provider: ${providerId}`);
377
this._dynamicAuthenticationProviderIds.add(providerId);
378
return provider;
379
}
380
this._logService.error(`Failed to create dynamic authentication provider: ${providerId}`);
381
return undefined;
382
}
383
384
registerAuthenticationProviderHostDelegate(delegate: IAuthenticationProviderHostDelegate): IDisposable {
385
this._delegates.push(delegate);
386
this._delegates.sort((a, b) => b.priority - a.priority);
387
388
return {
389
dispose: () => {
390
const index = this._delegates.indexOf(delegate);
391
if (index !== -1) {
392
this._delegates.splice(index, 1);
393
}
394
}
395
};
396
}
397
398
private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise<IAuthenticationProvider> {
399
try {
400
await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal);
401
} catch (e) {
402
this._logService.error(`Extension Service failed to activate authentication provider '${providerId}':`, e);
403
throw e;
404
}
405
let provider = this._authenticationProviders.get(providerId);
406
if (provider) {
407
return provider;
408
}
409
if (this._disposedSource.token.isCancellationRequested) {
410
throw new Error('Authentication service is disposed.');
411
}
412
413
const store = new DisposableStore();
414
try {
415
const result = await raceTimeout(
416
raceCancellation(
417
Event.toPromise(
418
Event.filter(
419
this.onDidRegisterAuthenticationProvider,
420
e => e.id === providerId,
421
store
422
),
423
store
424
),
425
this._disposedSource.token
426
),
427
5000
428
);
429
if (!result) {
430
throw new Error(`Timed out waiting for authentication provider '${providerId}' to register.`);
431
}
432
provider = this._authenticationProviders.get(result.id);
433
if (provider) {
434
return provider;
435
}
436
throw new Error(`No authentication provider '${providerId}' is currently registered.`);
437
} finally {
438
store.dispose();
439
}
440
}
441
}
442
443
registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed);
444
445