Skip to content

Commit 87b0c54

Browse files
author
Rachel Macfarlane
committed
Make AuthenticationSession a class and remove getAccessToken method, fixes microsoft#91554
1 parent f560b15 commit 87b0c54

10 files changed

Lines changed: 141 additions & 131 deletions

File tree

extensions/github-authentication/src/github.ts

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function isOldSessionData(x: any): x is OldSessionData {
3434
}
3535

3636
export class GitHubAuthenticationProvider {
37-
private _sessions: vscode.AuthenticationSession[] = [];
37+
private _sessions: vscode.AuthenticationSession2[] = [];
3838
private _githubServer = new GitHubServer();
3939

4040
public async initialize(): Promise<void> {
@@ -51,7 +51,7 @@ export class GitHubAuthenticationProvider {
5151

5252
private pollForChange() {
5353
setTimeout(async () => {
54-
let storedSessions: vscode.AuthenticationSession[];
54+
let storedSessions: vscode.AuthenticationSession2[];
5555
try {
5656
storedSessions = await this.readSessions();
5757
} catch (e) {
@@ -94,12 +94,12 @@ export class GitHubAuthenticationProvider {
9494
}, 1000 * 30);
9595
}
9696

97-
private async readSessions(): Promise<vscode.AuthenticationSession[]> {
97+
private async readSessions(): Promise<vscode.AuthenticationSession2[]> {
9898
const storedSessions = await keychain.getToken();
9999
if (storedSessions) {
100100
try {
101101
const sessionData: (SessionData | OldSessionData)[] = JSON.parse(storedSessions);
102-
const sessionPromises = sessionData.map(async (session: SessionData | OldSessionData): Promise<vscode.AuthenticationSession> => {
102+
const sessionPromises = sessionData.map(async (session: SessionData | OldSessionData): Promise<vscode.AuthenticationSession2> => {
103103
const needsUserInfo = isOldSessionData(session) || !session.account;
104104
let userInfo: { id: string, accountName: string };
105105
if (needsUserInfo) {
@@ -117,7 +117,7 @@ export class GitHubAuthenticationProvider {
117117
: session.account?.id ?? userInfo!.id
118118
},
119119
scopes: session.scopes,
120-
getAccessToken: () => Promise.resolve(session.accessToken)
120+
accessToken: session.accessToken
121121
};
122122
});
123123

@@ -136,24 +136,14 @@ export class GitHubAuthenticationProvider {
136136
}
137137

138138
private async storeSessions(): Promise<void> {
139-
const sessionData: SessionData[] = await Promise.all(this._sessions.map(async session => {
140-
const resolvedAccessToken = await session.getAccessToken();
141-
return {
142-
id: session.id,
143-
account: session.account,
144-
scopes: session.scopes,
145-
accessToken: resolvedAccessToken
146-
};
147-
}));
148-
149-
await keychain.setToken(JSON.stringify(sessionData));
139+
await keychain.setToken(JSON.stringify(this._sessions));
150140
}
151141

152-
get sessions(): vscode.AuthenticationSession[] {
142+
get sessions(): vscode.AuthenticationSession2[] {
153143
return this._sessions;
154144
}
155145

156-
public async login(scopes: string): Promise<vscode.AuthenticationSession> {
146+
public async login(scopes: string): Promise<vscode.AuthenticationSession2> {
157147
const token = scopes === 'vso' ? await this.loginAndInstallApp(scopes) : await this._githubServer.login(scopes);
158148
const session = await this.tokenToSession(token, scopes.split(' '));
159149
await this.setToken(session);
@@ -174,19 +164,12 @@ export class GitHubAuthenticationProvider {
174164
this._githubServer.manuallyProvideToken();
175165
}
176166

177-
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
167+
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession2> {
178168
const userInfo = await this._githubServer.getUserInfo(token);
179-
return {
180-
id: uuid(),
181-
getAccessToken: () => Promise.resolve(token),
182-
account: {
183-
displayName: userInfo.accountName,
184-
id: userInfo.id
185-
},
186-
scopes: scopes
187-
};
169+
return new vscode.AuthenticationSession2(uuid(), token, { displayName: userInfo.accountName, id: userInfo.id }, scopes);
188170
}
189-
private async setToken(session: vscode.AuthenticationSession): Promise<void> {
171+
172+
private async setToken(session: vscode.AuthenticationSession2): Promise<void> {
190173
const sessionIndex = this._sessions.findIndex(s => s.id === session.id);
191174
if (sessionIndex > -1) {
192175
this._sessions.splice(sessionIndex, 1, session);

extensions/microsoft-authentication/src/AADHelper.ts

Lines changed: 69 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,9 @@ export class AzureActiveDirectoryService {
204204
}, 1000 * 30);
205205
}
206206

207-
private convertToSession(token: IToken): vscode.AuthenticationSession {
208-
return {
209-
id: token.sessionId,
210-
getAccessToken: () => this.resolveAccessToken(token),
211-
account: token.account,
212-
scopes: token.scope.split(' ')
213-
};
207+
private async convertToSession(token: IToken): Promise<vscode.AuthenticationSession2> {
208+
const resolvedToken = await this.resolveAccessToken(token);
209+
return new vscode.AuthenticationSession2(token.sessionId, resolvedToken, token.account, token.scope.split(' '));
214210
}
215211

216212
private async resolveAccessToken(token: IToken): Promise<string> {
@@ -243,77 +239,81 @@ export class AzureActiveDirectoryService {
243239
}
244240
}
245241

246-
get sessions(): vscode.AuthenticationSession[] {
247-
return this._tokens.map(token => this.convertToSession(token));
242+
get sessions(): Promise<vscode.AuthenticationSession2[]> {
243+
return Promise.all(this._tokens.map(token => this.convertToSession(token)));
248244
}
249245

250-
public async login(scope: string): Promise<void> {
246+
public async login(scope: string): Promise<vscode.AuthenticationSession2> {
251247
Logger.info('Logging in...');
248+
return new Promise(async (resolve, reject) => {
249+
if (vscode.env.uiKind === vscode.UIKind.Web) {
250+
resolve(this.loginWithoutLocalServer(scope));
251+
return;
252+
}
252253

253-
if (vscode.env.uiKind === vscode.UIKind.Web) {
254-
await this.loginWithoutLocalServer(scope);
255-
return;
256-
}
254+
const nonce = crypto.randomBytes(16).toString('base64');
255+
const { server, redirectPromise, codePromise } = createServer(nonce);
257256

258-
const nonce = crypto.randomBytes(16).toString('base64');
259-
const { server, redirectPromise, codePromise } = createServer(nonce);
257+
let token: IToken | undefined;
258+
try {
259+
const port = await startServer(server);
260+
vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`));
261+
262+
const redirectReq = await redirectPromise;
263+
if ('err' in redirectReq) {
264+
const { err, res } = redirectReq;
265+
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
266+
res.end();
267+
throw err;
268+
}
260269

261-
let token: IToken | undefined;
262-
try {
263-
const port = await startServer(server);
264-
vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`));
265-
266-
const redirectReq = await redirectPromise;
267-
if ('err' in redirectReq) {
268-
const { err, res } = redirectReq;
269-
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
270-
res.end();
271-
throw err;
272-
}
270+
const host = redirectReq.req.headers.host || '';
271+
const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1];
272+
const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port;
273273

274-
const host = redirectReq.req.headers.host || '';
275-
const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1];
276-
const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port;
274+
const state = `${updatedPort},${encodeURIComponent(nonce)}`;
277275

278-
const state = `${updatedPort},${encodeURIComponent(nonce)}`;
276+
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
277+
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
278+
const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
279279

280-
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
281-
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
282-
const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
280+
await redirectReq.res.writeHead(302, { Location: loginUrl });
281+
redirectReq.res.end();
283282

284-
await redirectReq.res.writeHead(302, { Location: loginUrl });
285-
redirectReq.res.end();
283+
const codeRes = await codePromise;
284+
const res = codeRes.res;
286285

287-
const codeRes = await codePromise;
288-
const res = codeRes.res;
286+
try {
287+
if ('err' in codeRes) {
288+
throw codeRes.err;
289+
}
290+
token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope);
291+
this.setToken(token, scope);
292+
Logger.info('Login successful');
293+
res.writeHead(302, { Location: '/' });
294+
const session = await this.convertToSession(token);
295+
resolve(session);
296+
res.end();
297+
} catch (err) {
298+
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
299+
res.end();
300+
reject(err.message);
301+
}
302+
} catch (e) {
303+
Logger.error(e.message);
289304

290-
try {
291-
if ('err' in codeRes) {
292-
throw codeRes.err;
305+
// If the error was about starting the server, try directly hitting the login endpoint instead
306+
if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') {
307+
await this.loginWithoutLocalServer(scope);
293308
}
294-
token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope);
295-
this.setToken(token, scope);
296-
Logger.info('Login successful');
297-
res.writeHead(302, { Location: '/' });
298-
res.end();
299-
} catch (err) {
300-
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
301-
res.end();
302-
throw new Error(err.message);
303-
}
304-
} catch (e) {
305-
Logger.error(e.message);
306309

307-
// If the error was about starting the server, try directly hitting the login endpoint instead
308-
if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') {
309-
await this.loginWithoutLocalServer(scope);
310+
reject(e.message);
311+
} finally {
312+
setTimeout(() => {
313+
server.close();
314+
}, 5000);
310315
}
311-
throw new Error(e.message);
312-
} finally {
313-
setTimeout(() => {
314-
server.close();
315-
}, 5000);
316-
}
316+
});
317317
}
318318

319319
private getCallbackEnvironment(callbackUri: vscode.Uri): string {
@@ -333,7 +333,7 @@ export class AzureActiveDirectoryService {
333333
}
334334
}
335335

336-
private async loginWithoutLocalServer(scope: string): Promise<IToken> {
336+
private async loginWithoutLocalServer(scope: string): Promise<vscode.AuthenticationSession2> {
337337
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`));
338338
const nonce = crypto.randomBytes(16).toString('base64');
339339
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
@@ -348,7 +348,7 @@ export class AzureActiveDirectoryService {
348348
});
349349
vscode.env.openExternal(uri);
350350

351-
const timeoutPromise = new Promise((_: (value: IToken) => void, reject) => {
351+
const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession2) => void, reject) => {
352352
const wait = setTimeout(() => {
353353
clearTimeout(wait);
354354
reject('Login timed out.');
@@ -358,9 +358,9 @@ export class AzureActiveDirectoryService {
358358
return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]);
359359
}
360360

361-
private async handleCodeResponse(state: string, codeVerifier: string, scope: string) {
361+
private async handleCodeResponse(state: string, codeVerifier: string, scope: string): Promise<vscode.AuthenticationSession2> {
362362
let uriEventListener: vscode.Disposable;
363-
return new Promise((resolve: (value: IToken) => void, reject) => {
363+
return new Promise((resolve: (value: vscode.AuthenticationSession2) => void, reject) => {
364364
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
365365
try {
366366
const query = parseQuery(uri);
@@ -374,7 +374,8 @@ export class AzureActiveDirectoryService {
374374
const token = await this.exchangeCodeForToken(code, codeVerifier, scope);
375375
this.setToken(token, scope);
376376

377-
resolve(token);
377+
const session = await this.convertToSession(token);
378+
resolve(session);
378379
} catch (err) {
379380
reject(err);
380381
}

extensions/microsoft-authentication/src/extension.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ export async function activate(context: vscode.ExtensionContext) {
2626
login: async (scopes: string[]) => {
2727
try {
2828
telemetryReporter.sendTelemetryEvent('login');
29-
await loginService.login(scopes.sort().join(' '));
30-
const session = loginService.sessions[loginService.sessions.length - 1];
29+
const session = await loginService.login(scopes.sort().join(' '));
3130
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
32-
return loginService.sessions[0]!;
31+
return session;
3332
} catch (e) {
3433
telemetryReporter.sendTelemetryEvent('loginFailed');
3534
throw e;

src/vs/editor/common/modes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1411,7 +1411,7 @@ export interface RenameProvider {
14111411
*/
14121412
export interface AuthenticationSession {
14131413
id: string;
1414-
getAccessToken(): Thenable<string>;
1414+
accessToken: string;
14151415
account: {
14161416
displayName: string;
14171417
id: string;

src/vs/vscode.proposed.d.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,41 @@ declare module 'vscode' {
2828
scopes: string[];
2929
}
3030

31+
export class AuthenticationSession2 {
32+
/**
33+
* The identifier of the authentication session.
34+
*/
35+
readonly id: string;
36+
37+
/**
38+
* The access token.
39+
*/
40+
readonly accessToken: string;
41+
42+
/**
43+
* The account associated with the session.
44+
*/
45+
readonly account: {
46+
/**
47+
* The human-readable name of the account.
48+
*/
49+
readonly displayName: string;
50+
51+
/**
52+
* The unique identifier of the account.
53+
*/
54+
readonly id: string;
55+
};
56+
57+
/**
58+
* The permissions granted by the session's access token. Available scopes
59+
* are defined by the authentication provider.
60+
*/
61+
readonly scopes: string[];
62+
63+
constructor(id: string, accessToken: string, account: { displayName: string, id: string }, scopes: string[]);
64+
}
65+
3166
/**
3267
* An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed.
3368
*/
@@ -112,12 +147,12 @@ declare module 'vscode' {
112147
/**
113148
* Returns an array of current sessions.
114149
*/
115-
getSessions(): Thenable<ReadonlyArray<AuthenticationSession>>;
150+
getSessions(): Thenable<ReadonlyArray<AuthenticationSession2>>;
116151

117152
/**
118153
* Prompts a user to login.
119154
*/
120-
login(scopes: string[]): Thenable<AuthenticationSession>;
155+
login(scopes: string[]): Thenable<AuthenticationSession2>;
121156

122157
/**
123158
* Removes the session corresponding to session id.
@@ -170,7 +205,7 @@ declare module 'vscode' {
170205
* @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions and
171206
* `createIfNone` was not specified.
172207
*/
173-
export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable<AuthenticationSession | undefined>;
208+
export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable<AuthenticationSession2 | undefined>;
174209

175210
/**
176211
* @deprecated

0 commit comments

Comments
 (0)