Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/authentication/common/authentication.ts
13401 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
import type { AuthenticationGetSessionOptions, AuthenticationGetSessionPresentationOptions, AuthenticationSession } from 'vscode';
6
import { createServiceIdentifier } from '../../../util/common/services';
7
8
/**
9
* A stricter version of {@link AuthenticationGetSessionPresentationOptions} that requires
10
* a `detail` message explaining why authentication is needed. This forces callers to provide
11
* meaningful context to the user instead of passing a bare `true` or `{}`.
12
*/
13
export type StrictAuthenticationPresentationOptions = AuthenticationGetSessionPresentationOptions & { detail: string };
14
import { Emitter, Event } from '../../../util/vs/base/common/event';
15
import { Disposable } from '../../../util/vs/base/common/lifecycle';
16
import { derived } from '../../../util/vs/base/common/observableInternal';
17
import { AuthPermissionMode, AuthProviderId, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
18
import { ILogService } from '../../log/common/logService';
19
import { CopilotToken } from './copilotToken';
20
import { ICopilotTokenManager } from './copilotTokenManager';
21
import { ICopilotTokenStore } from './copilotTokenStore';
22
23
// Minimum set of scopes needed for Copilot to work
24
export const GITHUB_SCOPE_USER_EMAIL = ['user:email'];
25
26
// Old list of scopes still used for backwards compatibility
27
export const GITHUB_SCOPE_READ_USER = ['read:user'];
28
29
// The same scopes that GitHub Pull Request, GitHub Repositories, and others use
30
export const GITHUB_SCOPE_ALIGNED = ['read:user', 'user:email', 'repo', 'workflow'];
31
32
export class MinimalModeError extends Error {
33
constructor() {
34
super('The authentication service is in minimal mode.');
35
this.name = 'MinimalModeError';
36
}
37
}
38
39
export const IAuthenticationService = createServiceIdentifier<IAuthenticationService>('IAuthenticationService');
40
export interface IAuthenticationService {
41
42
readonly _serviceBrand: undefined;
43
44
/**
45
* Whether the authentication service is in minimal mode. If true, the authentication service will not attempt to
46
* fetch the permissive token. This means that:
47
* * {@link getGitHubSession} interactive flows with 'permissive' kind will always throw an error
48
* * {@link getGitHubSession} silent flows with 'permissive' kind and {@link permissiveGitHubSession} will always return undefined
49
*/
50
readonly isMinimalMode: boolean;
51
52
/**
53
* Event emitter that will fire an event every time the authentication status changes. This is used for example to detect when the user
54
* logs out of GitHub or when they log in with a more permissive token.
55
*
56
* @note For best practice of handling of the user's authentication state, you should react to this event.
57
*/
58
readonly onDidAuthenticationChange: Event<void>;
59
60
/**
61
* @deprecated Use {@link onDidAuthenticationChange} instead. This event fires when the access token changes and not the copilot token.
62
*/
63
readonly onDidAccessTokenChange: Event<void>;
64
65
/**
66
* Checks if there is currently any session available in the cache. Does not make any network requests and does not
67
* call out to the underlying authentication provider.
68
*
69
* @note See {@link getAnyGitHubToken} for more information and for an async version by calling {@link getGitHubSession} with kind 'any' and `{ silent: true }`.
70
* @note For best practice of handling of the user's authentication state, you should react to {@link onDidAuthenticationChange}.
71
* @note This token will have at least the `user:email` scope to be able to access the minimum Copilot API.
72
*/
73
readonly anyGitHubSession: AuthenticationSession | undefined;
74
75
/**
76
* Checks if there is currently a permissive session available in the cache. Does not make any network requests and does not
77
* call out to the underlying authentication provider.
78
*
79
* @note See {@link getPermissiveGitHubToken} for more information and for an async version by calling {@link getGitHubSession} with kind 'permissive' and `{ silent: true }`.
80
* @note For best practice of handling of the user's authentication state, you should react to {@link onDidAuthenticationChange}.
81
* @returns undefined if no auth session is available or Minimal Mode is enabled. Otherwise, returns an auth session with the `repo` scope.
82
*/
83
readonly permissiveGitHubSession: AuthenticationSession | undefined;
84
85
/**
86
* Gets a GitHub session capable of calling GitHub APIs.
87
* @param kind - The kind of session that you need. **Your choice here should be thoughtful.**
88
* - 'permissive': You need a session that can access the user's private repositories or needs write access.
89
* - 'any': You only need a session that can access public information about the user.
90
* @param options - Options for getting the session.
91
* @returns Promise<AuthenticationSession> - The requested authentication session.
92
* @throws MinimalModeError - If kind is 'permissive' and the authentication service is in minimal mode.
93
* @throws Error - If no session is acquired (user cancels).
94
*/
95
getGitHubSession(kind: 'permissive' | 'any', options: AuthenticationGetSessionOptions & { createIfNone: StrictAuthenticationPresentationOptions }): Promise<AuthenticationSession>;
96
97
/**
98
* Gets a GitHub session capable of calling GitHub APIs.
99
* @param kind - The kind of session that you need. **Your choice here should be thoughtful.**
100
* - 'permissive': You need a session that can access the user's private repositories or needs write access.
101
* - 'any': You only need a session that can access public information about the user.
102
* @param options - Options for getting the session.
103
* @returns Promise<AuthenticationSession> - The requested authentication session.
104
* @throws MinimalModeError - If kind is 'permissive' and the authentication service is in minimal mode.
105
* @throws Error - If no session is acquired (user cancels).
106
*/
107
getGitHubSession(kind: 'permissive' | 'any', options: AuthenticationGetSessionOptions & { forceNewSession: StrictAuthenticationPresentationOptions }): Promise<AuthenticationSession>;
108
109
/**
110
* Gets a GitHub session capable of calling GitHub APIs.
111
* @param kind - The kind of session that you need. **Your choice here should be thoughtful.**
112
* - 'permissive': You need a session that can access the user's private repositories or needs write access.
113
* - 'any': You only need a session that can access public information about the user.
114
* @param options - Options for getting the session.
115
* @returns Promise<AuthenticationSession> - The requested authentication session. OR
116
* @returns Promise<undefined> - If no session is available or kind is 'permissive' and the authentication service is in minimal mode.
117
* @see {@link isMinimalMode} for more information about minimal mode.
118
*/
119
getGitHubSession(kind: 'permissive' | 'any', options: Omit<AuthenticationGetSessionOptions, 'createIfNone' | 'forceNewSession'>): Promise<AuthenticationSession | undefined>;
120
121
/**
122
* Checks if there is currently a Copilot token available in the cache. Does not make any network requests.
123
* See {@link getCopilotToken} for more information and for an async version.
124
*
125
* @note we omit token here because it is possibly expired. If you need it, use {@link getCopilotToken} instead as it includes a refresh mechanism.
126
* @note For best practice of handling of the user's authentication state, you should react to {@link onDidAuthenticationChange}.
127
*/
128
readonly copilotToken: Omit<CopilotToken, 'token'> | undefined;
129
130
131
/**
132
* Return the token needed to authenticate with the speculative decoding endpoint.
133
* This token is public as it is set via a request to the ChatMLFetcher and reset either via expiration or a 403 response from the SD endpoint.
134
* @note There is no guarantee this is a valid token and it can still reject due to 403 with the SD endpoint
135
*/
136
speculativeDecodingEndpointToken: string | undefined;
137
138
/**
139
* Return a currently valid Copilot token, retrieving a fresh one if
140
* necessary.
141
*
142
* @param force will force a refresh of the token, even if not expired
143
* @returns a Copilot token or throws an error if none is found.
144
* @note For best practice of handling of the user's authentication state, you should react to {@link onDidAuthenticationChange}.
145
*/
146
getCopilotToken(force?: boolean): Promise<CopilotToken>;
147
148
/**
149
* Drop the current Copilot token as we received an HTTP error while trying
150
* to use it that indicates it's no longer valid.
151
*/
152
resetCopilotToken(httpError?: number): void;
153
154
/**
155
* Fired when the authentication state changes for ado.
156
*/
157
readonly onDidAdoAuthenticationChange: Event<void>;
158
159
/**
160
* Returns a valid Azure DevOps session for the user
161
*/
162
getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise<string | undefined>;
163
}
164
165
export abstract class BaseAuthenticationService extends Disposable implements IAuthenticationService {
166
declare readonly _serviceBrand: undefined;
167
168
private readonly _onDidAuthenticationChange = this._register(new Emitter<void>());
169
readonly onDidAuthenticationChange: Event<void> = this._onDidAuthenticationChange.event;
170
171
protected fireAuthenticationChange(source: string): void {
172
const hasSession = !!this.copilotToken;
173
this._logService.info(`AuthenticationService: firing onDidAuthenticationChange from ${source}. Has token: ${hasSession}`);
174
this._onDidAuthenticationChange.fire();
175
}
176
177
protected readonly _onDidAccessTokenChange = this._register(new Emitter<void>());
178
readonly onDidAccessTokenChange: Event<void> = this._onDidAccessTokenChange.event;
179
180
protected readonly _onDidAdoAuthenticationChange = this._register(new Emitter<void>());
181
readonly onDidAdoAuthenticationChange: Event<void> = this._onDidAdoAuthenticationChange.event;
182
183
constructor(
184
@ILogService protected readonly _logService: ILogService,
185
@ICopilotTokenStore protected readonly _tokenStore: ICopilotTokenStore,
186
@ICopilotTokenManager private readonly _tokenManager: ICopilotTokenManager,
187
@IConfigurationService protected readonly _configurationService: IConfigurationService,
188
) {
189
super();
190
this._register(_tokenManager.onDidCopilotTokenRefresh(() => {
191
this._logService.debug('Handling CopilotToken refresh.');
192
void this._handleAuthChangeEvent();
193
}));
194
}
195
196
//#region isMinimalMode
197
198
protected _isMinimalMode = derived(r => this._configurationService.getConfigObservable(ConfigKey.Shared.AuthPermissions).read(r) === AuthPermissionMode.Minimal);
199
get isMinimalMode(): boolean {
200
return this._isMinimalMode.get();
201
}
202
203
//#endregion
204
205
//#region Any GitHub Token
206
207
protected _anyGitHubSession: AuthenticationSession | undefined;
208
get anyGitHubSession(): AuthenticationSession | undefined {
209
return this._anyGitHubSession;
210
}
211
212
//#endregion
213
214
//#region Permissive GitHub Token
215
216
protected _permissiveGitHubSession: AuthenticationSession | undefined;
217
get permissiveGitHubSession(): AuthenticationSession | undefined {
218
return this._permissiveGitHubSession;
219
}
220
221
//#endregion
222
223
//#region GitHub Session
224
225
abstract getGitHubSession(kind: 'permissive' | 'any', options: AuthenticationGetSessionOptions & { createIfNone: StrictAuthenticationPresentationOptions }): Promise<AuthenticationSession>;
226
abstract getGitHubSession(kind: 'permissive' | 'any', options: AuthenticationGetSessionOptions & { forceNewSession: StrictAuthenticationPresentationOptions }): Promise<AuthenticationSession>;
227
abstract getGitHubSession(kind: 'permissive' | 'any', options: Omit<AuthenticationGetSessionOptions, 'createIfNone' | 'forceNewSession'>): Promise<AuthenticationSession | undefined>;
228
229
//#endregion
230
231
//#region Ado
232
233
protected _anyAdoSession: AuthenticationSession | undefined;
234
get anyAdoSession(): AuthenticationSession | undefined {
235
return this._anyAdoSession;
236
}
237
protected abstract getAnyAdoSession(options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined>;
238
239
//#endregion
240
241
//#region Copilot Token
242
243
private _copilotTokenError: Error | undefined;
244
get copilotToken(): CopilotToken | undefined {
245
return this._tokenStore.copilotToken;
246
}
247
async getCopilotToken(force?: boolean): Promise<CopilotToken> {
248
try {
249
const token = await this._tokenManager.getCopilotToken(force);
250
this._tokenStore.copilotToken = token;
251
this._copilotTokenError = undefined;
252
return token;
253
} catch (afterError) {
254
this._tokenStore.copilotToken = undefined;
255
const beforeError = this._copilotTokenError;
256
this._copilotTokenError = afterError;
257
// This handles the case where the user still can't get a Copilot Token,
258
// but the error has change. I.e. They go from being not signed in (no copilot token can be minted)
259
// to an account that doesn't have a valid subscription (no copilot token can be minted).
260
// NOTE: if either error is undefined, this event should be fired elsewhere already.
261
if (beforeError && afterError && beforeError.message !== afterError.message) {
262
this.fireAuthenticationChange('getCopilotToken error change');
263
}
264
throw afterError;
265
}
266
}
267
268
resetCopilotToken(httpError?: number): void {
269
this._tokenStore.copilotToken = undefined;
270
this._tokenManager.resetCopilotToken(httpError);
271
}
272
273
//#endregion
274
275
// #region Speculative decoding endpoint token
276
public speculativeDecodingEndpointToken: string | undefined;
277
// #endregion
278
279
//#region ADO Token
280
abstract getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise<string | undefined>;
281
//#endregion
282
283
protected async _handleAuthChangeEvent(): Promise<void> {
284
const anyGitHubSessionBefore = this._anyGitHubSession;
285
const permissiveGitHubSessionBefore = this._permissiveGitHubSession;
286
const anyAdoSessionBefore = this._anyAdoSession;
287
const copilotTokenBefore = this._tokenStore.copilotToken;
288
const copilotTokenErrorBefore = this._copilotTokenError;
289
290
// Update caches
291
const resolved = await Promise.allSettled([
292
this.getGitHubSession('any', { silent: true }),
293
this.getGitHubSession('permissive', { silent: true }),
294
this.getAnyAdoSession({ silent: true }),
295
]);
296
for (const res of resolved) {
297
if (res.status === 'rejected') {
298
this._logService.error(`Error getting a session: ${res.reason}`);
299
}
300
}
301
302
if (
303
anyGitHubSessionBefore?.accessToken !== this._anyGitHubSession?.accessToken ||
304
permissiveGitHubSessionBefore?.accessToken !== this._permissiveGitHubSession?.accessToken
305
) {
306
this._onDidAccessTokenChange.fire();
307
this._logService.debug('Auth state changed, minting a new CopilotToken...');
308
// The auth state has changed, so mint a new Copilot token
309
try {
310
await this.getCopilotToken(true);
311
} catch (e) {
312
// Ignore errors
313
}
314
this._logService.debug('Minted a new CopilotToken.');
315
return;
316
}
317
318
if (anyAdoSessionBefore?.accessToken !== this._anyAdoSession?.accessToken) {
319
this._logService.debug(`Ado auth state changed, firing event. Had token before: ${!!anyAdoSessionBefore?.accessToken}. Has token now: ${!!this._anyAdoSession?.accessToken}.`);
320
this._onDidAdoAuthenticationChange.fire();
321
}
322
323
// Auth state hasn't changed, but the Copilot token might have
324
try {
325
await this.getCopilotToken();
326
} catch (e) {
327
// Ignore errors
328
}
329
330
if (copilotTokenBefore?.token !== this._tokenStore.copilotToken?.token ||
331
// React to errors changing too (i.e. I go from zero session to a session that doesn't have Copilot access)
332
copilotTokenErrorBefore?.message !== this._copilotTokenError?.message
333
) {
334
this._logService.debug('CopilotToken state changed, firing event.');
335
this.fireAuthenticationChange('handleAuthChangeEvent');
336
}
337
this._logService.debug('Finished handling auth change event.');
338
}
339
}
340
341
export function authProviderId(configurationService: IConfigurationService): AuthProviderId {
342
return (
343
configurationService.getConfig(ConfigKey.Shared.AuthProvider) === AuthProviderId.GitHubEnterprise
344
? AuthProviderId.GitHubEnterprise
345
: AuthProviderId.GitHub
346
);
347
}
348
349