diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 3d73a7621fd..fd3ba077028 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -14,7 +14,8 @@ ], "activationEvents": [], "enabledApiProposals": [ - "idToken" + "idToken", + "authGetSessions" ], "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index df36686dc9a..bc4d71e56d6 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -203,11 +203,13 @@ export class AzureActiveDirectoryService { return this._sessionChangeEmitter.event; } - public getSessions(scopes?: string[]): Promise { + public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { if (!scopes) { this._logger.info('Getting sessions for all scopes...'); - const sessions = this._tokens.map(token => this.convertToSessionSync(token)); - this._logger.info(`Got ${sessions.length} sessions for all scopes...`); + const sessions = this._tokens + .filter(token => !account?.label || token.account.label === account.label) + .map(token => this.convertToSessionSync(token)); + this._logger.info(`Got ${sessions.length} sessions for all scopes${account ? ` for account '${account.label}'` : ''}...`); return Promise.resolve(sessions); } @@ -238,23 +240,43 @@ export class AzureActiveDirectoryService { tenant: this.getTenantId(scopes), }; - this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions`); - return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData)); + this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); + return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData, account)); } - private async doGetSessions(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Getting sessions`); + private async doGetSessions(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Getting sessions` + account ? ` for ${account?.label}` : ''); - const matchingTokens = this._tokens.filter(token => token.scope === scopeData.scopeStr); + const matchingTokens = this._tokens + .filter(token => token.scope === scopeData.scopeStr) + .filter(token => !account?.label || token.account.label === account.label); // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { - // Get a token with the correct client id. - const token = scopeData.clientId === DEFAULT_CLIENT_ID - ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) - : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)); + // Get a token with the correct client id and account. + let token: IToken | undefined; + for (const t of this._tokens) { + // No refresh token, so we can't make a new token from this session + if (!t.refreshToken) { + continue; + } + // Need to make sure the account matches if we were provided one + if (account?.label && t.account.label !== account.label) { + continue; + } + // If the client id is the default client id, then check for the absence of the VSCODE_CLIENT_ID scope + if (scopeData.clientId === DEFAULT_CLIENT_ID && !t.scope.includes('VSCODE_CLIENT_ID')) { + token = t; + break; + } + // If the client id is not the default client id, then check for the matching VSCODE_CLIENT_ID scope + if (scopeData.clientId !== DEFAULT_CLIENT_ID && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)) { + token = t; + break; + } + } if (token) { this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); @@ -275,7 +297,7 @@ export class AzureActiveDirectoryService { .map(result => (result as PromiseFulfilledResult).value); } - public createSession(scopes: string[]): Promise { + public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise { let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); @@ -301,11 +323,11 @@ export class AzureActiveDirectoryService { }; this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData)); + return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData, account)); } - private async doCreateSession(scopeData: IScopeData): Promise { - this._logger.info(`[${scopeData.scopeStr}] Creating session`); + private async doCreateSession(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { + this._logger.info(`[${scopeData.scopeStr}] Creating session` + account ? ` for ${account?.label}` : ''); const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -316,17 +338,17 @@ export class AzureActiveDirectoryService { return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Signing in to your account...'), cancellable: true }, async (_progress, token) => { if (runsRemote || runsServerless) { - return await this.createSessionWithoutLocalServer(scopeData, token); + return await this.createSessionWithoutLocalServer(scopeData, account?.label, token); } try { - return await this.createSessionWithLocalServer(scopeData, token); + return await this.createSessionWithLocalServer(scopeData, account?.label, token); } catch (e) { this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); // If the error was about starting the server, try directly hitting the login endpoint instead if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - return this.createSessionWithoutLocalServer(scopeData, token); + return this.createSessionWithoutLocalServer(scopeData, account?.label, token); } throw e; @@ -334,7 +356,7 @@ export class AzureActiveDirectoryService { }); } - private async createSessionWithLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); @@ -344,11 +366,15 @@ export class AzureActiveDirectoryService { client_id: scopeData.clientId, redirect_uri: redirectUrl, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); - const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString(); + }); + if (loginHint) { + qs.set('login_hint', loginHint); + } else { + qs.set('prompt', 'select_account'); + } + const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs.toString()}`, this._env.activeDirectoryEndpointUrl).toString(); const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); @@ -370,7 +396,7 @@ export class AzureActiveDirectoryService { return session; } - private async createSessionWithoutLocalServer(scopeData: IScopeData, token: vscode.CancellationToken): Promise { + private async createSessionWithoutLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = generateCodeVerifier(); @@ -383,17 +409,22 @@ export class AzureActiveDirectoryService { const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); - signInUrl.search = new URLSearchParams({ + const qs = new URLSearchParams({ response_type: 'code', client_id: encodeURIComponent(scopeData.clientId), response_mode: 'query', redirect_uri: redirectUrl, state, scope: scopeData.scopesToSend, - prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }).toString(); + }); + if (loginHint) { + qs.append('login_hint', loginHint); + } else { + qs.append('prompt', 'select_account'); + } + signInUrl.search = qs.toString(); const uri = vscode.Uri.parse(signInUrl.toString()); vscode.env.openExternal(uri); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 02cfb4643f4..87dc94e4c25 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -123,8 +123,8 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[]) => loginService.getSessions(scopes), - createSession: async (scopes: string[]) => { + getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account), + createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { try { /* __GDPR__ "login" : { @@ -138,7 +138,7 @@ export async function activate(context: vscode.ExtensionContext) { scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), }); - return await loginService.createSession(scopes); + return await loginService.createSession(scopes, options?.account); } catch (e) { /* __GDPR__ "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 4b9d06d1847..cad76d078bd 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -22,6 +22,7 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.idToken.d.ts" + "../../src/vscode-dts/vscode.proposed.idToken.d.ts", + "../../src/vscode-dts/vscode.proposed.authGetSessions.d.ts" ] } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3719825841a..c60acfa9739 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -31,6 +31,7 @@ interface AuthenticationGetSessionOptions { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; silent?: boolean; + account?: AuthenticationSessionAccount; } export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -49,8 +50,8 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut this.onDidChangeSessions = onDidChangeSessionsEmitter.event; } - async getSessions(scopes?: string[]) { - return this._proxy.$getSessions(this.id, scopes); + async getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions) { + return this._proxy.$getSessions(this.id, scopes, options); } createSession(scopes: string[], options: IAuthenticationCreateSessionOptions): Promise { @@ -159,7 +160,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { - const sessions = await this.authenticationService.getSessions(providerId, scopes, true); + const sessions = await this.authenticationService.getSessions(providerId, scopes, options.account, true); const provider = this.authenticationService.getProvider(providerId); // Error cases @@ -213,18 +214,16 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu let session; if (sessions?.length && !options.forceNewSession) { - session = provider.supportsMultipleAccounts + session = provider.supportsMultipleAccounts && !options.account ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { - let sessionToRecreate: AuthenticationSession | undefined; - if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { - sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; - } else { + let account: AuthenticationSessionAccount | undefined = options.account; + if (!account) { const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; + account = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate)?.account : undefined; } - session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); + session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, account }); } this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); @@ -262,7 +261,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { - const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); + const sessions = await this.authenticationService.getSessions(providerId, [...scopes], undefined, true); const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); @@ -273,6 +272,19 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return accessibleSessions; } + async $getAccounts(providerId: string): Promise> { + const sessions = await this.authenticationService.getSessions(providerId); + const accounts = new Array(); + const seenAccounts = new Set(); + for (const session of sessions) { + if (!seenAccounts.has(session.account.label)) { + seenAccounts.add(session.account.label); + accounts.push(session.account); + } + } + return accounts; + } + private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { type AuthProviderUsageClassification = { owner: 'TylerLeonhardt'; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index 9b14928a514..0418e5676bc 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -14,7 +14,7 @@ import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLang import { ILanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector } from 'vs/workbench/contrib/chat/common/languageModels'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -161,9 +161,9 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider { if (this._session) { return [this._session]; } - return [await this.createSession(scopes || [], {})]; + return [await this.createSession(scopes || [])]; } - async createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise { + async createSession(scopes: string[]): Promise { this._session = this._createFakeSession(scopes); this._onDidChangeSessions.fire({ added: [this._session], changed: [], removed: [] }); return this._session; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 68c6b305eab..f4c448d9e47 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -287,12 +287,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { checkProposedApiEnabled(extension, 'authLearnMore'); } + if (options?.account) { + checkProposedApiEnabled(extension, 'authGetSessions'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: readonly string[]) { checkProposedApiEnabled(extension, 'authGetSessions'); return extHostAuthentication.getSessions(extension, providerId, scopes); }, + getAccounts(providerId: string) { + checkProposedApiEnabled(extension, 'authGetSessions'); + return extHostAuthentication.getAccounts(providerId); + }, // TODO: remove this after GHPR and Codespaces move off of it async hasSession(providerId: string, scopes: readonly string[]) { checkProposedApiEnabled(extension, 'authSession'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index fdbc0cb4b1f..cc5a244204f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -68,7 +68,7 @@ import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFil import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { RelatedInformationResult, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation'; -import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; @@ -162,6 +162,7 @@ export interface MainThreadAuthenticationShape extends IDisposable { $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean; forceNewSession?: boolean | AuthenticationForceNewSessionOptions; clearSessionPreference?: boolean }): Promise; $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise; + $getAccounts(providerId: string): Promise>; $removeSession(providerId: string, sessionId: string): Promise; } @@ -1806,7 +1807,7 @@ export interface ExtHostLabelServiceShape { } export interface ExtHostAuthenticationShape { - $getSessions(id: string, scopes?: string[]): Promise>; + $getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise>; $createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise; $removeSession(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 16b1d4a405b..5c75a1318ca 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -64,6 +64,11 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } + async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); + return await this._proxy.$getAccounts(providerId); + } + async removeSession(providerId: string, sessionId: string): Promise { const providerData = this._authenticationProviders.get(providerId); if (!providerData) { @@ -89,7 +94,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { }); } - async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderCreateSessionOptions): Promise { + async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise { const providerData = this._authenticationProviders.get(providerId); if (providerData) { return await providerData.provider.createSession(scopes, options); @@ -107,10 +112,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - async $getSessions(providerId: string, scopes?: string[]): Promise> { + async $getSessions(providerId: string, scopes: ReadonlyArray | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise> { const providerData = this._authenticationProviders.get(providerId); if (providerData) { - return await providerData.provider.getSessions(scopes); + return await providerData.provider.getSessions(scopes, options); } throw new Error(`Unable to find authentication provider with handle: ${providerId}`); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 84cc7730f4b..d154337b9a5 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -12,7 +12,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IProductService } from 'vs/platform/product/common/productService'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; -import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -164,10 +164,10 @@ export class AuthenticationService extends Disposable implements IAuthentication throw new Error(`No authentication provider '${id}' is currently registered.`); } - async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { + async getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate: boolean = false): Promise> { const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); if (authProvider) { - return await authProvider.getSessions(scopes); + return await authProvider.getSessions(scopes, { account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } @@ -177,7 +177,7 @@ export class AuthenticationService extends Disposable implements IAuthentication const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { return await authProvider.createSession(scopes, { - sessionToRecreate: options?.sessionToRecreate + account: options?.account }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index 6da6e530237..1d99ffc059a 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -35,8 +35,12 @@ export interface AuthenticationProviderInformation { } export interface IAuthenticationCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; activateImmediate?: boolean; + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } export interface AllowedExtension { @@ -131,7 +135,7 @@ export interface IAuthenticationService { * @param scopes The scopes for the session * @param activateImmediate If true, the provider should activate immediately if it is not already */ - getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; + getSessions(id: string, scopes?: string[], account?: AuthenticationSessionAccount, activateImmediate?: boolean): Promise>; /** * Creates an AuthenticationSession with the given provider and scopes @@ -162,8 +166,12 @@ export interface IAuthenticationExtensionsService { requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; } -export interface IAuthenticationProviderCreateSessionOptions { - sessionToRecreate?: AuthenticationSession; +export interface IAuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccount; } /** @@ -194,9 +202,10 @@ export interface IAuthenticationProvider { /** * Retrieves a list of authentication sessions. * @param scopes - An optional list of scopes. If provided, the sessions returned should match these permissions, otherwise all sessions should be returned. + * @param options - Additional options for getting sessions. * @returns A promise that resolves to an array of authentication sessions. */ - getSessions(scopes?: string[]): Promise; + getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise; /** * Prompts the user to log in. @@ -207,7 +216,7 @@ export interface IAuthenticationProvider { * @param options - Additional options for creating the session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: string[], options: IAuthenticationProviderCreateSessionOptions): Promise; + createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise; /** * Removes the session corresponding to the specified session ID. diff --git a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts index 20a692c0180..45049789f50 100644 --- a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts +++ b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts @@ -7,42 +7,50 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/152399 - export interface AuthenticationForceNewSessionOptions { - /** - * The session that you are asking to be recreated. The Auth Provider can use this to - * help guide the user to log in to the correct account. - */ - sessionToRecreate?: AuthenticationSession; - } + // FOR THE CONSUMER export namespace authentication { /** - * Get all authentication sessions matching the desired scopes that this extension has access to. In order to request access, - * use {@link getSession}. To request an additional account, specify {@link AuthenticationGetSessionOptions.clearSessionPreference} - * and {@link AuthenticationGetSessionOptions.createIfNone} together. + * Get all accounts that the user is logged in to for the specified provider. + * Use this paired with {@link getSession} in order to get an authentication session for a specific account. * * Currently, there are only two authentication providers that are contributed from built in extensions * to the editor 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 - * @returns A thenable that resolves to a readonly array of authentication sessions. - */ - export function getSessions(providerId: string, scopes: readonly string[]): Thenable; + * Note: Getting accounts does not imply that your extension has access to that account or its authentication sessions. You can verify access to the account by calling {@link getSession}. + * + * @param providerId The id of the provider to use + * @returns A thenable that resolves to a readonly array of authentication accounts. + */ + export function getAccounts(providerId: string): Thenable; } - /** - * The options passed in to the provider when creating a session. - */ - export interface AuthenticationProviderCreateSessionOptions { + export interface AuthenticationGetSessionOptions { /** - * The session that is being asked to be recreated. If this is passed in, the provider should - * attempt to recreate the session based on the information in this session. + * The account that you would like to get a session for. This is passed down to the Authentication Provider to be used for creating the correct session. */ - sessionToRecreate?: AuthenticationSession; + account?: AuthenticationSessionAccountInformation; + } + + // FOR THE AUTH PROVIDER + + export interface AuthenticationProviderSessionOptions { + /** + * The account that is being asked about. If this is passed in, the provider should + * attempt to return the sessions that are only related to this account. + */ + account?: AuthenticationSessionAccountInformation; } export interface AuthenticationProvider { + /** + * Get a list of sessions. + * @param scopes An optional list of scopes. If provided, the sessions returned should match + * these permissions, otherwise all sessions should be returned. + * @param options Additional options for getting sessions. + * @returns A promise that resolves to an array of authentication sessions. + */ + getSessions(scopes: readonly string[] | undefined, options: AuthenticationProviderSessionOptions): Thenable; /** * Prompts a user to login. * @@ -57,6 +65,25 @@ declare module 'vscode' { * @param options Additional options for creating a session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: readonly string[], options: AuthenticationProviderCreateSessionOptions): Thenable; + createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Thenable; + } + + // THE BELOW IS THE OLD PROPOSAL WHICH WILL BE REMOVED BUT KEPT UNTIL THE NEW PROPOSAL IS ADOPTED BY ALL PARTIES + + export namespace authentication { + /** + * Get all authentication sessions matching the desired scopes that this extension has access to. In order to request access, + * use {@link getSession}. To request an additional account, specify {@link AuthenticationGetSessionOptions.clearSessionPreference} + * and {@link AuthenticationGetSessionOptions.createIfNone} together. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to the editor 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 + * @returns A thenable that resolves to a readonly array of authentication sessions. + * @deprecated Use {@link getAccounts} instead. + */ + export function getSessions(providerId: string, scopes: readonly string[]): Thenable; } }