Path: blob/main/src/vs/workbench/services/authentication/browser/dynamicAuthenticationProviderStorageService.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 { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';6import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';7import { IDynamicAuthenticationProviderStorageService, DynamicAuthenticationProviderInfo, DynamicAuthenticationProviderTokensChangeEvent } from '../common/dynamicAuthenticationProviderStorage.js';8import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';9import { IAuthorizationTokenResponse, isAuthorizationTokenResponse } from '../../../../base/common/oauth.js';10import { ILogService } from '../../../../platform/log/common/log.js';11import { Emitter, Event } from '../../../../base/common/event.js';12import { Disposable } from '../../../../base/common/lifecycle.js';13import { Queue } from '../../../../base/common/async.js';1415export class DynamicAuthenticationProviderStorageService extends Disposable implements IDynamicAuthenticationProviderStorageService {16declare readonly _serviceBrand: undefined;1718private static readonly PROVIDERS_STORAGE_KEY = 'dynamicAuthProviders';1920private readonly _onDidChangeTokens = this._register(new Emitter<DynamicAuthenticationProviderTokensChangeEvent>());21readonly onDidChangeTokens: Event<DynamicAuthenticationProviderTokensChangeEvent> = this._onDidChangeTokens.event;2223constructor(24@IStorageService private readonly storageService: IStorageService,25@ISecretStorageService private readonly secretStorageService: ISecretStorageService,26@ILogService private readonly logService: ILogService27) {28super();2930// Listen for secret storage changes and emit events for dynamic auth provider token changes31const queue = new Queue<void>();32this._register(this.secretStorageService.onDidChangeSecret(async (key: string) => {33let payload: { isDynamicAuthProvider: boolean; authProviderId: string; clientId: string } | undefined;34try {35payload = JSON.parse(key);36} catch (error) {37// Ignore errors... must not be a dynamic auth provider38}39if (payload?.isDynamicAuthProvider) {40void queue.queue(async () => {41const tokens = await this.getSessionsForDynamicAuthProvider(payload.authProviderId, payload.clientId);42this._onDidChangeTokens.fire({43authProviderId: payload.authProviderId,44clientId: payload.clientId,45tokens46});47});48}49}));50}5152async getClientRegistration(providerId: string): Promise<{ clientId?: string; clientSecret?: string } | undefined> {53// First try new combined SecretStorage format54const key = `dynamicAuthProvider:clientRegistration:${providerId}`;55const credentialsValue = await this.secretStorageService.get(key);56if (credentialsValue) {57try {58const credentials = JSON.parse(credentialsValue);59if (credentials && (credentials.clientId || credentials.clientSecret)) {60return credentials;61}62} catch {63await this.secretStorageService.delete(key);64}65}6667// Just grab the client id from the provider68const providers = this._getStoredProviders();69const provider = providers.find(p => p.providerId === providerId);70return provider?.clientId ? { clientId: provider.clientId } : undefined;71}7273getClientId(providerId: string): string | undefined {74// For backward compatibility, try old storage format first75const providers = this._getStoredProviders();76const provider = providers.find(p => p.providerId === providerId);77return provider?.clientId;78}7980async storeClientRegistration(providerId: string, authorizationServer: string, clientId: string, clientSecret?: string, label?: string): Promise<void> {81// Store provider information for backward compatibility and UI display82this._trackProvider(providerId, authorizationServer, clientId, label);8384// Store both client ID and secret together in SecretStorage85const key = `dynamicAuthProvider:clientRegistration:${providerId}`;86const credentials = { clientId, clientSecret };87await this.secretStorageService.set(key, JSON.stringify(credentials));88}8990private _trackProvider(providerId: string, authorizationServer: string, clientId: string, label?: string): void {91const providers = this._getStoredProviders();9293// Check if provider already exists94const existingProviderIndex = providers.findIndex(p => p.providerId === providerId);95if (existingProviderIndex === -1) {96// Add new provider with provided or default info97const newProvider: DynamicAuthenticationProviderInfo = {98providerId,99label: label || providerId, // Use provided label or providerId as default100authorizationServer,101clientId102};103providers.push(newProvider);104this._storeProviders(providers);105} else {106const existingProvider = providers[existingProviderIndex];107// Create new provider object with updated info108const updatedProvider: DynamicAuthenticationProviderInfo = {109providerId,110label: label || existingProvider.label,111authorizationServer,112clientId113};114providers[existingProviderIndex] = updatedProvider;115this._storeProviders(providers);116}117}118119private _getStoredProviders(): DynamicAuthenticationProviderInfo[] {120const stored = this.storageService.get(DynamicAuthenticationProviderStorageService.PROVIDERS_STORAGE_KEY, StorageScope.APPLICATION, '[]');121try {122const providerInfos = JSON.parse(stored);123// MIGRATION: remove after an iteration or 2124for (const providerInfo of providerInfos) {125if (!providerInfo.authorizationServer) {126providerInfo.authorizationServer = providerInfo.issuer;127}128}129return providerInfos;130} catch {131return [];132}133}134135private _storeProviders(providers: DynamicAuthenticationProviderInfo[]): void {136this.storageService.store(137DynamicAuthenticationProviderStorageService.PROVIDERS_STORAGE_KEY,138JSON.stringify(providers),139StorageScope.APPLICATION,140StorageTarget.MACHINE141);142}143144getInteractedProviders(): ReadonlyArray<DynamicAuthenticationProviderInfo> {145return this._getStoredProviders();146}147148async removeDynamicProvider(providerId: string): Promise<void> {149// Get provider info before removal for secret cleanup150const providers = this._getStoredProviders();151const providerInfo = providers.find(p => p.providerId === providerId);152153// Remove from stored providers154const filteredProviders = providers.filter(p => p.providerId !== providerId);155this._storeProviders(filteredProviders);156157// Remove sessions from secret storage if we have the provider info158if (providerInfo) {159const secretKey = JSON.stringify({ isDynamicAuthProvider: true, authProviderId: providerId, clientId: providerInfo.clientId });160await this.secretStorageService.delete(secretKey);161}162163// Remove client credentials from new SecretStorage format164const credentialsKey = `dynamicAuthProvider:clientRegistration:${providerId}`;165await this.secretStorageService.delete(credentialsKey);166}167168async getSessionsForDynamicAuthProvider(authProviderId: string, clientId: string): Promise<(IAuthorizationTokenResponse & { created_at: number })[] | undefined> {169const key = JSON.stringify({ isDynamicAuthProvider: true, authProviderId, clientId });170const value = await this.secretStorageService.get(key);171if (value) {172const parsed = JSON.parse(value);173if (!Array.isArray(parsed) || !parsed.every((t) => typeof t.created_at === 'number' && isAuthorizationTokenResponse(t))) {174this.logService.error(`Invalid session data for ${authProviderId} (${clientId}) in secret storage:`, parsed);175await this.secretStorageService.delete(key);176return undefined;177}178return parsed;179}180return undefined;181}182183async setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {184const key = JSON.stringify({ isDynamicAuthProvider: true, authProviderId, clientId });185const value = JSON.stringify(sessions);186await this.secretStorageService.set(key, value);187this.logService.trace(`Set session data for ${authProviderId} (${clientId}) in secret storage:`, sessions);188}189}190191registerSingleton(IDynamicAuthenticationProviderStorageService, DynamicAuthenticationProviderStorageService, InstantiationType.Delayed);192193194