Path: blob/main/src/vs/workbench/services/authentication/browser/authenticationService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';8import { isString } from '../../../../base/common/types.js';9import { localize } from '../../../../nls.js';10import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';11import { IProductService } from '../../../../platform/product/common/productService.js';12import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';13import { IAuthenticationAccessService } from './authenticationAccessService.js';14import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationGetSessionsOptions, IAuthenticationProvider, IAuthenticationProviderHostDelegate, IAuthenticationService, IAuthenticationWWWAuthenticateRequest, isAuthenticationWWWAuthenticateRequest } from '../common/authentication.js';15import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';16import { ActivationKind, IExtensionService } from '../../extensions/common/extensions.js';17import { ILogService } from '../../../../platform/log/common/log.js';18import { IJSONSchema } from '../../../../base/common/jsonSchema.js';19import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';20import { match } from '../../../../base/common/glob.js';21import { URI } from '../../../../base/common/uri.js';22import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, parseWWWAuthenticateHeader } from '../../../../base/common/oauth.js';23import { raceCancellation, raceTimeout } from '../../../../base/common/async.js';24import { CancellationTokenSource } from '../../../../base/common/cancellation.js';2526export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }2728// TODO: pull this out into its own service29export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean };30export async function getCurrentAuthenticationSessionInfo(31secretStorageService: ISecretStorageService,32productService: IProductService33): Promise<AuthenticationSessionInfo | undefined> {34const authenticationSessionValue = await secretStorageService.get(`${productService.urlProtocol}.loginAccount`);35if (authenticationSessionValue) {36try {37const authenticationSessionInfo: AuthenticationSessionInfo = JSON.parse(authenticationSessionValue);38if (authenticationSessionInfo39&& isString(authenticationSessionInfo.id)40&& isString(authenticationSessionInfo.accessToken)41&& isString(authenticationSessionInfo.providerId)42) {43return authenticationSessionInfo;44}45} catch (e) {46// This is a best effort operation.47console.error(`Failed parsing current auth session value: ${e}`);48}49}50return undefined;51}5253const authenticationDefinitionSchema: IJSONSchema = {54type: 'object',55additionalProperties: false,56properties: {57id: {58type: 'string',59description: localize('authentication.id', 'The id of the authentication provider.')60},61label: {62type: 'string',63description: localize('authentication.label', 'The human readable name of the authentication provider.'),64},65authorizationServerGlobs: {66type: 'array',67items: {68type: 'string',69description: localize('authentication.authorizationServerGlobs', 'A list of globs that match the authorization servers that this provider supports.'),70},71description: localize('authentication.authorizationServerGlobsDescription', 'A list of globs that match the authorization servers that this provider supports.')72}73}74};7576const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint<AuthenticationProviderInformation[]>({77extensionPoint: 'authentication',78jsonSchema: {79description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'),80type: 'array',81items: authenticationDefinitionSchema82},83activationEventsGenerator: (authenticationProviders, result) => {84for (const authenticationProvider of authenticationProviders) {85if (authenticationProvider.id) {86result.push(`onAuthenticationRequest:${authenticationProvider.id}`);87}88}89}90});9192export class AuthenticationService extends Disposable implements IAuthenticationService {93declare readonly _serviceBrand: undefined;9495private _onDidRegisterAuthenticationProvider: Emitter<AuthenticationProviderInformation> = this._register(new Emitter<AuthenticationProviderInformation>());96readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this._onDidRegisterAuthenticationProvider.event;9798private _onDidUnregisterAuthenticationProvider: Emitter<AuthenticationProviderInformation> = this._register(new Emitter<AuthenticationProviderInformation>());99readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this._onDidUnregisterAuthenticationProvider.event;100101private _onDidChangeSessions: Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>());102readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;103104private _onDidChangeDeclaredProviders: Emitter<void> = this._register(new Emitter<void>());105readonly onDidChangeDeclaredProviders: Event<void> = this._onDidChangeDeclaredProviders.event;106107private _authenticationProviders: Map<string, IAuthenticationProvider> = new Map<string, IAuthenticationProvider>();108private _authenticationProviderDisposables: DisposableMap<string, IDisposable> = this._register(new DisposableMap<string, IDisposable>());109private _dynamicAuthenticationProviderIds = new Set<string>();110111private readonly _delegates: IAuthenticationProviderHostDelegate[] = [];112113private _disposedSource = new CancellationTokenSource();114115constructor(116@IExtensionService private readonly _extensionService: IExtensionService,117@IAuthenticationAccessService authenticationAccessService: IAuthenticationAccessService,118@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService,119@ILogService private readonly _logService: ILogService120) {121super();122this._register(toDisposable(() => this._disposedSource.dispose(true)));123this._register(authenticationAccessService.onDidChangeExtensionSessionAccess(e => {124// The access has changed, not the actual session itself but extensions depend on this event firing125// when they have gained access to an account so this fires that event.126this._onDidChangeSessions.fire({127providerId: e.providerId,128label: e.accountName,129event: {130added: [],131changed: [],132removed: []133}134});135}));136137this._registerEnvContributedAuthenticationProviders();138this._registerAuthenticationExtentionPointHandler();139}140141private _declaredProviders: AuthenticationProviderInformation[] = [];142get declaredProviders(): AuthenticationProviderInformation[] {143return this._declaredProviders;144}145146private _registerEnvContributedAuthenticationProviders(): void {147if (!this._environmentService.options?.authenticationProviders?.length) {148return;149}150for (const provider of this._environmentService.options.authenticationProviders) {151this.registerDeclaredAuthenticationProvider(provider);152this.registerAuthenticationProvider(provider.id, provider);153}154}155156private _registerAuthenticationExtentionPointHandler(): void {157this._register(authenticationExtPoint.setHandler((_extensions, { added, removed }) => {158this._logService.debug(`Found authentication providers. added: ${added.length}, removed: ${removed.length}`);159added.forEach(point => {160for (const provider of point.value) {161if (isFalsyOrWhitespace(provider.id)) {162point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.'));163continue;164}165166if (isFalsyOrWhitespace(provider.label)) {167point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.'));168continue;169}170171if (!this.declaredProviders.some(p => p.id === provider.id)) {172this.registerDeclaredAuthenticationProvider(provider);173this._logService.debug(`Declared authentication provider: ${provider.id}`);174} else {175point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id));176}177}178});179180const removedExtPoints = removed.flatMap(r => r.value);181removedExtPoints.forEach(point => {182const provider = this.declaredProviders.find(provider => provider.id === point.id);183if (provider) {184this.unregisterDeclaredAuthenticationProvider(provider.id);185this._logService.debug(`Undeclared authentication provider: ${provider.id}`);186}187});188}));189}190191registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void {192if (isFalsyOrWhitespace(provider.id)) {193throw new Error(localize('authentication.missingId', 'An authentication contribution must specify an id.'));194}195if (isFalsyOrWhitespace(provider.label)) {196throw new Error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.'));197}198if (this.declaredProviders.some(p => p.id === provider.id)) {199throw new Error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id));200}201this._declaredProviders.push(provider);202this._onDidChangeDeclaredProviders.fire();203}204205unregisterDeclaredAuthenticationProvider(id: string): void {206const index = this.declaredProviders.findIndex(provider => provider.id === id);207if (index > -1) {208this.declaredProviders.splice(index, 1);209}210this._onDidChangeDeclaredProviders.fire();211}212213isAuthenticationProviderRegistered(id: string): boolean {214return this._authenticationProviders.has(id);215}216217isDynamicAuthenticationProvider(id: string): boolean {218return this._dynamicAuthenticationProviderIds.has(id);219}220221registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void {222this._authenticationProviders.set(id, authenticationProvider);223const disposableStore = new DisposableStore();224disposableStore.add(authenticationProvider.onDidChangeSessions(e => this._onDidChangeSessions.fire({225providerId: id,226label: authenticationProvider.label,227event: e228})));229if (isDisposable(authenticationProvider)) {230disposableStore.add(authenticationProvider);231}232this._authenticationProviderDisposables.set(id, disposableStore);233this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label });234}235236unregisterAuthenticationProvider(id: string): void {237const provider = this._authenticationProviders.get(id);238if (provider) {239this._authenticationProviders.delete(id);240// If this is a dynamic provider, remove it from the set of dynamic providers241if (this._dynamicAuthenticationProviderIds.has(id)) {242this._dynamicAuthenticationProviderIds.delete(id);243}244this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label });245}246this._authenticationProviderDisposables.deleteAndDispose(id);247}248249getProviderIds(): string[] {250const providerIds: string[] = [];251this._authenticationProviders.forEach(provider => {252providerIds.push(provider.id);253});254return providerIds;255}256257getProvider(id: string): IAuthenticationProvider {258if (this._authenticationProviders.has(id)) {259return this._authenticationProviders.get(id)!;260}261throw new Error(`No authentication provider '${id}' is currently registered.`);262}263264async getAccounts(id: string): Promise<ReadonlyArray<AuthenticationSessionAccount>> {265// TODO: Cache this266const sessions = await this.getSessions(id);267const accounts = new Array<AuthenticationSessionAccount>();268const seenAccounts = new Set<string>();269for (const session of sessions) {270if (!seenAccounts.has(session.account.label)) {271seenAccounts.add(session.account.label);272accounts.push(session.account);273}274}275return accounts;276}277278async getSessions(id: string, scopeListOrRequest?: ReadonlyArray<string> | IAuthenticationWWWAuthenticateRequest, options?: IAuthenticationGetSessionsOptions, activateImmediate: boolean = false): Promise<ReadonlyArray<AuthenticationSession>> {279if (this._disposedSource.token.isCancellationRequested) {280return [];281}282283const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate);284if (authProvider) {285// Check if the authorization server is in the list of supported authorization servers286if (options?.authorizationServer) {287const authServerStr = options.authorizationServer.toString(true);288// TODO: something is off here...289if (!authProvider.authorizationServers?.some(i => i.toString(true) === authServerStr || match(i.toString(true), authServerStr))) {290throw new Error(`The authorization server '${authServerStr}' is not supported by the authentication provider '${id}'.`);291}292}293if (isAuthenticationWWWAuthenticateRequest(scopeListOrRequest)) {294if (!authProvider.getSessionsFromChallenges) {295throw new Error(`The authentication provider '${id}' does not support getting sessions from challenges.`);296}297return await authProvider.getSessionsFromChallenges(298{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), scopes: scopeListOrRequest.scopes },299{ ...options }300);301}302return await authProvider.getSessions(scopeListOrRequest ? [...scopeListOrRequest] : undefined, { ...options });303} else {304throw new Error(`No authentication provider '${id}' is currently registered.`);305}306}307308async createSession(id: string, scopeListOrRequest: ReadonlyArray<string> | IAuthenticationWWWAuthenticateRequest, options?: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession> {309if (this._disposedSource.token.isCancellationRequested) {310throw new Error('Authentication service is disposed.');311}312313const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate);314if (authProvider) {315if (isAuthenticationWWWAuthenticateRequest(scopeListOrRequest)) {316if (!authProvider.createSessionFromChallenges) {317throw new Error(`The authentication provider '${id}' does not support creating sessions from challenges.`);318}319return await authProvider.createSessionFromChallenges(320{ challenges: parseWWWAuthenticateHeader(scopeListOrRequest.wwwAuthenticate), scopes: scopeListOrRequest.scopes },321{ ...options }322);323}324return await authProvider.createSession([...scopeListOrRequest], { ...options });325} else {326throw new Error(`No authentication provider '${id}' is currently registered.`);327}328}329330async removeSession(id: string, sessionId: string): Promise<void> {331if (this._disposedSource.token.isCancellationRequested) {332throw new Error('Authentication service is disposed.');333}334335const authProvider = this._authenticationProviders.get(id);336if (authProvider) {337return authProvider.removeSession(sessionId);338} else {339throw new Error(`No authentication provider '${id}' is currently registered.`);340}341}342343async getOrActivateProviderIdForServer(authorizationServer: URI): Promise<string | undefined> {344for (const provider of this._authenticationProviders.values()) {345if (provider.authorizationServers?.some(i => i.toString(true) === authorizationServer.toString(true) || match(i.toString(true), authorizationServer.toString(true)))) {346return provider.id;347}348}349350const authServerStr = authorizationServer.toString(true);351const providers = this._declaredProviders352// Only consider providers that are not already registered since we already checked them353.filter(p => !this._authenticationProviders.has(p.id))354.filter(p => !!p.authorizationServerGlobs?.some(i => match(i, authServerStr)));355// TODO:@TylerLeonhardt fan out?356for (const provider of providers) {357const activeProvider = await this.tryActivateProvider(provider.id, true);358// Check the resolved authorization servers359if (activeProvider.authorizationServers?.some(i => match(i.toString(true), authServerStr))) {360return activeProvider.id;361}362}363return undefined;364}365366async createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined> {367const delegate = this._delegates[0];368if (!delegate) {369this._logService.error('No authentication provider host delegate found');370return undefined;371}372const providerId = await delegate.create(authorizationServer, serverMetadata, resource);373const provider = this._authenticationProviders.get(providerId);374if (provider) {375this._logService.debug(`Created dynamic authentication provider: ${providerId}`);376this._dynamicAuthenticationProviderIds.add(providerId);377return provider;378}379this._logService.error(`Failed to create dynamic authentication provider: ${providerId}`);380return undefined;381}382383registerAuthenticationProviderHostDelegate(delegate: IAuthenticationProviderHostDelegate): IDisposable {384this._delegates.push(delegate);385this._delegates.sort((a, b) => b.priority - a.priority);386387return {388dispose: () => {389const index = this._delegates.indexOf(delegate);390if (index !== -1) {391this._delegates.splice(index, 1);392}393}394};395}396397private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise<IAuthenticationProvider> {398try {399await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal);400} catch (e) {401this._logService.error(`Extension Service failed to activate authentication provider '${providerId}':`, e);402throw e;403}404let provider = this._authenticationProviders.get(providerId);405if (provider) {406return provider;407}408if (this._disposedSource.token.isCancellationRequested) {409throw new Error('Authentication service is disposed.');410}411412const store = new DisposableStore();413try {414const result = await raceTimeout(415raceCancellation(416Event.toPromise(417Event.filter(418this.onDidRegisterAuthenticationProvider,419e => e.id === providerId,420store421),422store423),424this._disposedSource.token425),4265000427);428if (!result) {429throw new Error(`Timed out waiting for authentication provider '${providerId}' to register.`);430}431provider = this._authenticationProviders.get(result.id);432if (provider) {433return provider;434}435throw new Error(`No authentication provider '${providerId}' is currently registered.`);436} finally {437store.dispose();438}439}440}441442registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed);443444445