Skip to content

Commit 17814a3

Browse files
akosyakovjeanp413
authored andcommitted
invalidate sessions
1 parent 914c571 commit 17814a3

File tree

2 files changed

+97
-99
lines changed

2 files changed

+97
-99
lines changed

extensions/gitpod/src/auth.ts

Lines changed: 94 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -141,55 +141,44 @@ const newConfig = {
141141
}
142142
};
143143

144-
/**
145-
* Returns a promise which waits until the secret store `gitpod.authSessions` item changes.
146-
* @returns a promise that resolves with newest added `vscode.AuthenticationSession`, or if no session is found, `null`
147-
*/
148-
async function waitForAuthenticationSession(context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession | null> {
149-
console.log('Waiting for the onchange event');
150-
144+
async function waitForAuthenticationSession(context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession> {
151145
// Wait until a session is added to the context's secret store
152-
const authPromise = promiseFromEvent(context.secrets.onDidChange, (changeEvent: vscode.SecretStorageChangeEvent, resolve): void => {
146+
await promiseFromEvent(context.secrets.onDidChange, (changeEvent: vscode.SecretStorageChangeEvent, resolve): void => {
153147
if (changeEvent.key === 'gitpod.authSessions') {
154148
resolve(changeEvent.key);
155149
}
156-
});
157-
const data: any = await authPromise.promise;
150+
}).promise;
158151

159-
console.log(data);
160-
161-
console.log('Retrieving the session');
162-
163-
const currentSessions = await getValidSessions(context);
164-
if (currentSessions.length > 0) {
165-
return currentSessions[currentSessions.length - 1];
166-
} else {
167-
vscode.window.showErrorMessage('Couldn\'t find any auth sessions');
168-
return null;
152+
const currentSessions = await readSessions(context);
153+
if (!currentSessions.length) {
154+
throw new Error('Not found');
169155
}
156+
return currentSessions[currentSessions.length - 1];
170157
}
171158

172-
/**
173-
* Checks all stored auth sessions and returns all valid ones
174-
* @param context the VS Code extension context from which to get the sessions from
175-
* @param scopes optionally, you can specify scopes to check against
176-
* @returns a list of sessions that are valid
177-
*/
178-
export async function getValidSessions(context: vscode.ExtensionContext, scopes?: readonly string[]): Promise<vscode.AuthenticationSession[]> {
179-
const sessions = await getAuthSessions(context);
159+
export async function readSessions(context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession[]> {
160+
let sessions = await getAuthSessions(context);
161+
sessions = sessions.filter(session => validateSession(session));
162+
await storeAuthSessions(sessions, context);
163+
return sessions;
164+
}
180165

181-
for (const [index, session] of sessions.entries()) {
182-
const availableScopes = await checkScopes(session.accessToken);
183-
if (!(scopes || [...gitpodScopes]).every((scope) => availableScopes.includes(scope))) {
184-
delete sessions[index];
166+
export async function validateSession(session: vscode.AuthenticationSession): Promise<boolean> {
167+
try {
168+
const hash = crypto.createHash('sha256').update(session.accessToken, 'utf8').digest('hex');
169+
const tokenScopes = new Set(await withServerApi(session.accessToken, service => service.server.getGitpodTokenScopes(hash)));
170+
for (const scope of gitpodScopes) {
171+
if (!tokenScopes.has(scope)) {
172+
return false;
173+
}
185174
}
175+
return true;
176+
} catch (e) {
177+
if (e.message !== unauthorizedErr) {
178+
console.error('gitpod: invalid session:', e);
179+
}
180+
return false;
186181
}
187-
188-
await storeAuthSessions(sessions, context);
189-
if (sessions.length === 0 && (await getAuthSessions(context)).length !== 0) {
190-
vscode.window.showErrorMessage('Your login session with Gitpod has expired. You need to sign in again.');
191-
}
192-
return sessions;
193182
}
194183

195184
/**
@@ -232,16 +221,23 @@ export async function setSettingsSync(enabled?: boolean): Promise<void> {
232221
}
233222
}
234223

235-
/**
236-
* Creates a WebSocket connection to Gitpod's API
237-
* @param accessToken an access token to create the WS connection with
238-
* @returns a tuple of `gitpodService` and `pendignWebSocket`
239-
*/
240-
async function createApiWebSocket(accessToken: string): Promise<{ gitpodService: GitpodConnection; pendignWebSocket: Promise<ReconnectingWebSocket>; }> {
241-
const factory = new JsonRpcProxyFactory<GitpodServer>();
242-
const gitpodService: GitpodConnection = new GitpodServiceImpl<GitpodClient, GitpodServer>(factory.createProxy()) as any;
243-
console.log(`Using token: ${accessToken}`);
244-
const pendignWebSocket = (async () => {
224+
class GitpodServerApi extends vscode.Disposable {
225+
226+
readonly service: GitpodConnection;
227+
private readonly socket: ReconnectingWebSocket;
228+
private readonly onWillCloseEmitter = new vscode.EventEmitter<number | undefined>();
229+
readonly onWillClose = this.onWillCloseEmitter.event;
230+
231+
constructor(accessToken: string) {
232+
super(() => {
233+
this.close();
234+
this.onWillCloseEmitter.dispose();
235+
});
236+
const factory = new JsonRpcProxyFactory<GitpodServer>();
237+
this.service = new GitpodServiceImpl<GitpodClient, GitpodServer>(factory.createProxy());
238+
239+
let retry = 1;
240+
const maxRetries = 3;
245241
class GitpodServerWebSocket extends WebSocket {
246242
constructor(address: string, protocols?: string | string[]) {
247243
super(address, protocols, {
@@ -250,35 +246,63 @@ async function createApiWebSocket(accessToken: string): Promise<{ gitpodService:
250246
'Authorization': `Bearer ${accessToken}`
251247
}
252248
});
249+
this.on('unexpected-response', (_, resp) => {
250+
this.terminate();
251+
252+
// if mal-formed handshake request (unauthorized, forbidden) or client actions (redirect) are required then fail immediately
253+
// otherwise try several times and fail, maybe temporarily unavailable, like server restart
254+
if (retry++ >= maxRetries || (typeof resp.statusCode === 'number' && 300 <= resp.statusCode && resp.statusCode < 500)) {
255+
socket.close(resp.statusCode);
256+
}
257+
});
253258
}
254259
}
255-
const webSocketMaxRetries = 3;
256-
const webSocket = new ReconnectingWebSocket(`${getBaseURL().replace('https', 'wss')}/api/v1`, undefined, {
260+
const socket = new ReconnectingWebSocket(`${getBaseURL().replace('https', 'wss')}/api/v1`, undefined, {
261+
maxReconnectionDelay: 10000,
257262
minReconnectionDelay: 1000,
263+
reconnectionDelayGrowFactor: 1.5,
258264
connectionTimeout: 10000,
259-
maxRetries: webSocketMaxRetries - 1,
265+
maxRetries: Infinity,
260266
debug: false,
261267
startClosed: false,
262268
WebSocket: GitpodServerWebSocket
263269
});
264-
265-
let retry = 1;
266-
webSocket.onerror = (err) => {
267-
vscode.window.showErrorMessage(`WebSocket error: ${err.message} (#${retry}/${webSocketMaxRetries})`);
268-
if (retry++ === webSocketMaxRetries) {
269-
throw new Error('Maximum websocket connection retries exceeded');
270-
}
270+
socket.onerror = e => {
271+
console.error('gitpod: server api: failed to open socket:', e);
271272
};
272273

273274
doListen({
274-
webSocket,
275+
webSocket: socket,
275276
logger: new ConsoleLogger(),
276277
onConnection: connection => factory.listen(connection),
277278
});
278-
return webSocket;
279-
})();
279+
this.socket = socket;
280+
}
281+
282+
private close(statusCode?: number): void {
283+
this.onWillCloseEmitter.fire(statusCode);
284+
try {
285+
this.socket.close();
286+
} catch (e) {
287+
console.error('gitpod: server api: failed to close socket:', e);
288+
}
289+
}
280290

281-
return { gitpodService, pendignWebSocket };
291+
}
292+
293+
const unauthorizedErr = 'unauthorized';
294+
function withServerApi<T>(accessToken: string, cb: (service: GitpodConnection) => Promise<T>): Promise<T> {
295+
const api = new GitpodServerApi(accessToken);
296+
return Promise.race([
297+
cb(api.service),
298+
new Promise<T>((_, reject) => api.onWillClose(statusCode => {
299+
if (statusCode === 401) {
300+
reject(new Error(unauthorizedErr));
301+
} else {
302+
reject(new Error('closed'));
303+
}
304+
}))
305+
]).finally(() => api.dispose());
282306
}
283307

284308
interface ExchangeTokenResponse {
@@ -315,13 +339,10 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
315339
}
316340

317341
const exchangeTokenData: ExchangeTokenResponse = await exchangeTokenResponse.json();
318-
console.log(exchangeTokenData);
319342
const jwtToken = exchangeTokenData.access_token;
320343
const accessToken = JSON.parse(Buffer.from(jwtToken.split('.')[1], 'base64').toString())['jti'];
321344

322-
const { gitpodService, pendignWebSocket } = await createApiWebSocket(accessToken);
323-
const user = await gitpodService.server.getLoggedInUser();
324-
(await pendignWebSocket).close();
345+
const user = await withServerApi(accessToken, service => service.server.getLoggedInUser());
325346
return {
326347
id: 'gitpod.user',
327348
account: {
@@ -337,22 +358,6 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
337358
}
338359
}
339360

340-
/**
341-
* @returns all of the scopes accessible for `accessToken`
342-
*/
343-
export async function checkScopes(accessToken: string): Promise<string[]> {
344-
try {
345-
const { gitpodService, pendignWebSocket } = await createApiWebSocket(accessToken);
346-
const hash = crypto.createHash('sha256').update(accessToken, 'utf8').digest('hex');
347-
const scopes = await gitpodService.server.getGitpodTokenScopes(hash);
348-
(await pendignWebSocket).close();
349-
return scopes;
350-
} catch (e) {
351-
vscode.window.showErrorMessage(`Couldn't connect: ${e}`);
352-
return [];
353-
}
354-
}
355-
356361
/**
357362
* Creates a URL to be opened for the whole OAuth2 flow to kick-off
358363
* @returns a `URL` string containing the whole auth URL
@@ -361,8 +366,6 @@ async function createOauth2URL(context: vscode.ExtensionContext, options: { auth
361366
const { authorizationURI, clientID, redirectURI, scopes } = options;
362367
const { codeChallenge, codeVerifier }: { codeChallenge: string, codeVerifier: string } = create();
363368

364-
console.log(`Verifier: ${codeVerifier}`);
365-
366369
let query = '';
367370
function set(field: string, value: string): void {
368371
if (query) {
@@ -403,36 +406,31 @@ async function askToEnable(context: vscode.ExtensionContext): Promise<void> {
403406
* @returns a promise which resolves to an `AuthenticationSession`
404407
*/
405408
export async function createSession(scopes: readonly string[], context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession> {
406-
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://gitpod.gitpod-desktop/complete-gitpod-auth`));
407409
if (scopes.some(scope => !gitpodScopes.has(scope))) {
408-
throw new Error('invalid scopes');
410+
throw new Error('Auth failed');
409411
}
410412

413+
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://gitpod.gitpod-desktop/complete-gitpod-auth`));
414+
411415
const gitpodAuth = await createOauth2URL(context, {
412416
clientID: `${vscode.env.uriScheme}-gitpod`,
413417
authorizationURI: `${getBaseURL()}/api/oauth/authorize`,
414418
redirectURI: callbackUri,
415419
scopes: [...gitpodScopes],
416420
});
417421

418-
const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject): void => {
419-
const wait = setTimeout(() => {
420-
clearTimeout(wait);
421-
vscode.window.showErrorMessage('Login timed out, please try to sign in again.');
422-
reject('Login timed out.');
423-
}, 1000 * 60 * 5); // 5 minutes
424-
});
425-
console.log(gitpodAuth);
426422
const opened = await vscode.env.openExternal(gitpodAuth as any);
427423
if (!opened) {
428424
const selected = await vscode.window.showErrorMessage(`Couldn't open ${gitpodAuth} automatically, please copy and paste it to your browser manually.`, 'Copy', 'Cancel');
429425
if (selected === 'Copy') {
430426
vscode.env.clipboard.writeText(gitpodAuth);
431-
console.log('Copied auth URL');
432427
}
433428
}
434429

435-
return Promise.race([timeoutPromise, (await waitForAuthenticationSession(context))!]);
430+
return Promise.race([
431+
waitForAuthenticationSession(context),
432+
new Promise<vscode.AuthenticationSession>((_, reject) => setTimeout(() => reject(new Error('Login timed out.')), 1000 * 60 * 5))
433+
]);
436434
}
437435

438436
/**

extensions/gitpod/src/sessionhandler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/// <reference path='../../../src/vs/vscode.d.ts'/>
66

77
import * as vscode from 'vscode';
8-
import { createSession, storeAuthSessions, getValidSessions } from './auth';
8+
import { createSession, storeAuthSessions, readSessions } from './auth';
99

1010
export default class GitpodAuthSession {
1111
private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
@@ -54,8 +54,8 @@ export default class GitpodAuthSession {
5454
}
5555
}
5656

57-
async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
58-
return getValidSessions(this.context, scopes);
57+
async getSessions(): Promise<vscode.AuthenticationSession[]> {
58+
return readSessions(this.context);
5959
}
6060

6161
public async readSessions(): Promise<vscode.AuthenticationSession[]> {

0 commit comments

Comments
 (0)