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
5240 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 { equalsIgnoreCase, 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: function* (authenticationProviders) {
85
for (const authenticationProvider of authenticationProviders) {
86
if (authenticationProvider.id) {
87
yield `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._registerAuthenticationExtensionPointHandler();
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 _registerAuthenticationExtensionPointHandler(): 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
this._dynamicAuthenticationProviderIds.delete(id);
243
this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label });
244
}
245
this._authenticationProviderDisposables.deleteAndDispose(id);
246
}
247
248
getProviderIds(): string[] {
249
const providerIds: string[] = [];
250
this._authenticationProviders.forEach(provider => {
251
providerIds.push(provider.id);
252
});
253
return providerIds;
254
}
255
256
getProvider(id: string): IAuthenticationProvider {
257
if (this._authenticationProviders.has(id)) {
258
return this._authenticationProviders.get(id)!;
259
}
260
throw new Error(`No authentication provider '${id}' is currently registered.`);
261
}
262
263
async getAccounts(id: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {
264
// TODO: Cache this
265
const sessions = await this.getSessions(id);
266
const accounts = new Array<AuthenticationSessionAccount>();
267
const seenAccounts = new Set<string>();
268
for (const session of sessions) {
269
if (!seenAccounts.has(session.account.label)) {
270
seenAccounts.add(session.account.label);
271
accounts.push(session.account);
272
}
273
}
274
return accounts;
275
}
276
277
async getSessions(id: string, scopeListOrRequest?: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, options?: IAuthenticationGetSessionsOptions, activateImmediate: boolean = false): Promise<ReadonlyArray<AuthenticationSession>> {
278
if (this._disposedSource.token.isCancellationRequested) {
279
return [];
280
}
281
282
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate);
283
if (authProvider) {
284
// Check if the authorization server is in the list of supported authorization servers
285
const server = options?.authorizationServer;
286
if (server) {
287
// Skip the resource server check since the auth provider id contains a specific resource server
288
// TODO@TylerLeonhardt: this can change when we have providers that support multiple resource servers
289
if (!this.matchesProvider(authProvider, server)) {
290
throw new Error(`The authentication provider '${id}' does not support the authorization server '${server.toString(true)}'.`);
291
}
292
}
293
if (isAuthenticationWwwAuthenticateRequest(scopeListOrRequest)) {
294
if (!authProvider.getSessionsFromChallenges) {
295
throw new Error(`The authentication provider '${id}' does not support getting sessions from challenges.`);
296
}
297
return await authProvider.getSessionsFromChallenges(
298
{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), fallbackScopes: scopeListOrRequest.fallbackScopes },
299
{ ...options }
300
);
301
}
302
return await authProvider.getSessions(scopeListOrRequest ? [...scopeListOrRequest] : undefined, { ...options });
303
} else {
304
throw new Error(`No authentication provider '${id}' is currently registered.`);
305
}
306
}
307
308
async createSession(id: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWwwAuthenticateRequest, options?: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession> {
309
if (this._disposedSource.token.isCancellationRequested) {
310
throw new Error('Authentication service is disposed.');
311
}
312
313
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate);
314
if (authProvider) {
315
if (isAuthenticationWwwAuthenticateRequest(scopeListOrRequest)) {
316
if (!authProvider.createSessionFromChallenges) {
317
throw new Error(`The authentication provider '${id}' does not support creating sessions from challenges.`);
318
}
319
return await authProvider.createSessionFromChallenges(
320
{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), fallbackScopes: scopeListOrRequest.fallbackScopes },
321
{ ...options }
322
);
323
}
324
return await authProvider.createSession([...scopeListOrRequest], { ...options });
325
} else {
326
throw new Error(`No authentication provider '${id}' is currently registered.`);
327
}
328
}
329
330
async removeSession(id: string, sessionId: string): Promise<void> {
331
if (this._disposedSource.token.isCancellationRequested) {
332
throw new Error('Authentication service is disposed.');
333
}
334
335
const authProvider = this._authenticationProviders.get(id);
336
if (authProvider) {
337
return authProvider.removeSession(sessionId);
338
} else {
339
throw new Error(`No authentication provider '${id}' is currently registered.`);
340
}
341
}
342
343
async getOrActivateProviderIdForServer(authorizationServer: URI, resourceServer?: URI): Promise<string | undefined> {
344
for (const provider of this._authenticationProviders.values()) {
345
if (this.matchesProvider(provider, authorizationServer, resourceServer)) {
346
return provider.id;
347
}
348
}
349
350
const authServerStr = authorizationServer.toString(true);
351
const providers = this._declaredProviders
352
// Only consider providers that are not already registered since we already checked them
353
.filter(p => !this._authenticationProviders.has(p.id))
354
.filter(p => !!p.authorizationServerGlobs?.some(i => match(i, authServerStr, { ignoreCase: true })));
355
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 (this.matchesProvider(activeProvider, authorizationServer, resourceServer)) {
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 matchesProvider(provider: IAuthenticationProvider, authorizationServer: URI, resourceServer?: URI): boolean {
399
// If a resourceServer is provided and the provider has a resourceServer defined, they must match
400
if (resourceServer && provider.resourceServer) {
401
const resourceServerStr = resourceServer.toString(true);
402
const providerResourceServerStr = provider.resourceServer.toString(true);
403
if (!equalsIgnoreCase(providerResourceServerStr, resourceServerStr)) {
404
return false;
405
}
406
}
407
408
if (provider.authorizationServers) {
409
const authServerStr = authorizationServer.toString(true);
410
for (const server of provider.authorizationServers) {
411
const str = server.toString(true);
412
if (equalsIgnoreCase(str, authServerStr) || match(str, authServerStr, { ignoreCase: true })) {
413
return true;
414
}
415
}
416
}
417
return false;
418
}
419
420
private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise<IAuthenticationProvider> {
421
await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal);
422
let provider = this._authenticationProviders.get(providerId);
423
if (provider) {
424
return provider;
425
}
426
if (this._disposedSource.token.isCancellationRequested) {
427
throw new Error('Authentication service is disposed.');
428
}
429
430
const store = new DisposableStore();
431
try {
432
// TODO: Remove this timeout and figure out a better way to ensure auth providers
433
// are registered _during_ extension activation.
434
const result = await raceTimeout(
435
raceCancellation(
436
Event.toPromise(
437
Event.filter(
438
this.onDidRegisterAuthenticationProvider,
439
e => e.id === providerId,
440
store
441
),
442
store
443
),
444
this._disposedSource.token
445
),
446
5000
447
);
448
provider = this._authenticationProviders.get(providerId);
449
if (provider) {
450
return provider;
451
}
452
if (!result) {
453
throw new Error(`Timed out waiting for authentication provider '${providerId}' to register.`);
454
}
455
throw new Error(`No authentication provider '${providerId}' is currently registered.`);
456
} finally {
457
store.dispose();
458
}
459
}
460
}
461
462
registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed);
463
464