diff --git a/packages/core/src/browser/authentication-service.ts b/packages/core/src/browser/authentication-service.ts index 403598899f497..ba58c8c86b908 100644 --- a/packages/core/src/browser/authentication-service.ts +++ b/packages/core/src/browser/authentication-service.ts @@ -216,7 +216,8 @@ export class AuthenticationServiceImpl implements AuthenticationService { this.authenticationProviders.delete(id); this.onDidUnregisterAuthenticationProviderEmitter.fire({ id, label: provider.label }); this.updateAccountsMenuItem(); - console.log(`An authentication provider with id '${id}' was unregistered.`); + } else { + console.error(`Failed to unregister an authentication provider. A provider with id '${id}' was not found.`); } } @@ -230,6 +231,8 @@ export class AuthenticationServiceImpl implements AuthenticationService { if (event.added) { await this.updateNewSessionRequests(provider); } + } else { + console.error(`Failed to update an authentication session. An authentication provider with id '${id}' was not found.`); } } @@ -346,7 +349,7 @@ export class AuthenticationServiceImpl implements AuthenticationService { async getSessions(id: string): Promise> { const authProvider = this.authenticationProviders.get(id); if (authProvider) { - return await authProvider.getSessions(); + return authProvider.getSessions(); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index e46808f9d7a92..ad6a62156cf52 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1514,13 +1514,8 @@ export interface AuthenticationMain { $unregisterAuthenticationProvider(id: string): void; $getProviderIds(): Promise; $updateSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; - $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, - potentialSessions: AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; - $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; - $loginPrompt(providerName: string, extensionName: string): Promise; - $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise; - $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - + $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, + options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; $logout(providerId: string, sessionId: string): Promise; } diff --git a/packages/plugin-ext/src/main/browser/authentication-main.ts b/packages/plugin-ext/src/main/browser/authentication-main.ts index f9051bcb7565e..c82e00f88b0ae 100644 --- a/packages/plugin-ext/src/main/browser/authentication-main.ts +++ b/packages/plugin-ext/src/main/browser/authentication-main.ts @@ -80,12 +80,49 @@ export class AuthenticationMainImpl implements AuthenticationMain { return this.authenticationService.logout(providerId, sessionId); } - async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { + protected async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); } - async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, - potentialSessions: AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { + async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, + options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise { + const orderedScopes = scopes.sort().join(' '); + const sessions = (await this.authenticationService.getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); + const label = this.authenticationService.getLabel(providerId); + + if (sessions.length) { + if (!this.authenticationService.supportsMultipleAccounts(providerId)) { + const session = sessions[0]; + const allowed = await this.getSessionsPrompt(providerId, session.account.label, label, extensionId, extensionName); + if (allowed) { + return session; + } else { + throw new Error('User did not consent to login.'); + } + } + + // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid + const selected = await this.selectSession(providerId, label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + return sessions.find(session => session.id === selected.id); + } else { + if (options.createIfNone) { + const isAllowed = await this.loginPrompt(label, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + const session = await this.authenticationService.login(providerId, scopes); + await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); + return session; + } else { + await this.requestNewSession(providerId, scopes, extensionId, extensionName); + return undefined; + } + } + } + + protected async selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, + potentialSessions: AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { if (!potentialSessions.length) { throw new Error('No potential sessions found'); } @@ -97,7 +134,7 @@ export class AuthenticationMainImpl implements AuthenticationMain { if (existingSessionPreference) { const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); if (matchingSession) { - const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.label, providerName, extensionId, extensionName); + const allowed = await this.getSessionsPrompt(providerId, matchingSession.account.label, providerName, extensionId, extensionName); if (allowed) { return matchingSession; } @@ -139,7 +176,7 @@ export class AuthenticationMainImpl implements AuthenticationMain { }); } - async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { + protected async getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { const allowList = await readAllowedExtensions(this.storageService, providerId, accountName); const extensionData = allowList.find(extension => extension.id === extensionId); if (extensionData) { @@ -158,12 +195,12 @@ export class AuthenticationMainImpl implements AuthenticationMain { return allow; } - async $loginPrompt(providerName: string, extensionName: string): Promise { + protected async loginPrompt(providerName: string, extensionName: string): Promise { const choice = await this.messageService.info(`The extension '${extensionName}' wants to sign in using ${providerName}.`, 'Allow', 'Cancel'); return choice === 'Allow'; } - async $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise { + protected async setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise { const allowList = await readAllowedExtensions(this.storageService, providerId, accountName); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); diff --git a/packages/plugin-ext/src/plugin/authentication-ext.ts b/packages/plugin-ext/src/plugin/authentication-ext.ts index 0082a90f93fc6..82c07b50349e6 100644 --- a/packages/plugin-ext/src/plugin/authentication-ext.ts +++ b/packages/plugin-ext/src/plugin/authentication-ext.ts @@ -29,7 +29,7 @@ import { import { RPCProtocol } from '../common/rpc-protocol'; import { Emitter, Event } from '@theia/core/lib/common/event'; import * as theia from '@theia/plugin'; -import { AuthenticationSessionsChangeEvent } from '../common/plugin-api-rpc-model'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent } from '../common/plugin-api-rpc-model'; export class AuthenticationExtImpl implements AuthenticationExt { private proxy: AuthenticationMain; @@ -65,55 +65,14 @@ export class AuthenticationExtImpl implements AuthenticationExt { options: theia.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; async getSession(requestingExtension: InternalPlugin, providerId: string, scopes: string[], options: theia.AuthenticationGetSessionOptions = {}): Promise { - const provider = this.authenticationProviders.get(providerId); const extensionName = requestingExtension.model.displayName || requestingExtension.model.name; const extensionId = requestingExtension.model.id.toLowerCase(); - if (!provider) { - throw new Error(`An authentication provider with id '${providerId}' was not found.`); - } - - const orderedScopes = scopes.sort().join(' '); - const sessions = (await provider.getSessions()).filter(s => s.scopes.slice().sort().join(' ') === orderedScopes); - - if (sessions.length > 0) { - if (!provider.supportsMultipleAccounts) { - const session = sessions[0]; - const allowed = await this.proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName); - if (allowed) { - return session; - } else { - throw new Error('User did not consent to login.'); - } - } - - // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this.proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); - return sessions.find(session => session.id === selected.id); - } else { - if (options.createIfNone) { - const isAllowed = await this.proxy.$loginPrompt(provider.label, extensionName); - if (!isAllowed) { - throw new Error('User did not consent to login.'); - } - - const session = await provider.login(scopes); - await this.proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); - return session; - } else { - await this.proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); - return undefined; - } - } + return this.proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } async logout(providerId: string, sessionId: string): Promise { - const provider = this.authenticationProviders.get(providerId); - if (!provider) { - return this.proxy.$logout(providerId, sessionId); - } - - return provider.logout(sessionId); + return this.proxy.$logout(providerId, sessionId); } registerAuthenticationProvider(provider: theia.AuthenticationProvider): theia.Disposable { @@ -156,7 +115,7 @@ export class AuthenticationExtImpl implements AuthenticationExt { }); } - $login(providerId: string, scopes: string[]): Promise { + $login(providerId: string, scopes: string[]): Promise { const authProvider = this.authenticationProviders.get(providerId); if (authProvider) { return Promise.resolve(authProvider.login(scopes)); @@ -174,10 +133,26 @@ export class AuthenticationExtImpl implements AuthenticationExt { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - $getSessions(providerId: string): Promise> { + async $getSessions(providerId: string): Promise> { const authProvider = this.authenticationProviders.get(providerId); if (authProvider) { - return Promise.resolve(authProvider.getSessions()); + const sessions = await authProvider.getSessions(); + + /* Wrap the session object received from the plugin to prevent serialization mismatches + e.g. if the plugin object is constructed with the help of getters they won't be serialized: + class SessionImpl implements AuthenticationSession { + private _id; + get id() { + return _id; + } + ... + } will translate to JSON as { _id: '' } not { id: '' } */ + return sessions.map(session => ({ + id: session.id, + accessToken: session.accessToken, + account: { id: session.account.id, label: session.account.label }, + scopes: session.scopes + })); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 5becf86755f16..a613d7610f753 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9821,8 +9821,6 @@ declare module '@theia/plugin' { * the extension. If there are multiple sessions with the same scopes, the user will be shown a * quickpick to select which account they would like to use. * - * Currently, there are only two authentication providers that are contributed from built in extensions - * to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider * @param options The [getSessionOptions](#GetSessionOptions) to use