Skip to content

Commit fece1dd

Browse files
committed
invalidate sessions
1 parent a7db15b commit fece1dd

File tree

2 files changed

+85
-77
lines changed

2 files changed

+85
-77
lines changed

extensions/gitpod/src/auth.ts

Lines changed: 82 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,15 @@ const newConfig = {
146146
* @returns a promise that resolves with newest added `vscode.AuthenticationSession`, or if no session is found, `null`
147147
*/
148148
async function waitForAuthenticationSession(context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession | null> {
149-
console.log('Waiting for the onchange event');
150-
151149
// Wait until a session is added to the context's secret store
152150
const authPromise = promiseFromEvent(context.secrets.onDidChange, (changeEvent: vscode.SecretStorageChangeEvent, resolve): void => {
153151
if (changeEvent.key === 'gitpod.authSessions') {
154152
resolve(changeEvent.key);
155153
}
156154
});
157-
const data: any = await authPromise.promise;
158-
159-
console.log(data);
160-
161-
console.log('Retrieving the session');
155+
await authPromise.promise;
162156

163-
const currentSessions = await getValidSessions(context);
157+
const currentSessions = await readSessions(context);
164158
if (currentSessions.length > 0) {
165159
return currentSessions[currentSessions.length - 1];
166160
} else {
@@ -169,27 +163,29 @@ async function waitForAuthenticationSession(context: vscode.ExtensionContext): P
169163
}
170164
}
171165

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);
166+
export async function readSessions(context: vscode.ExtensionContext): Promise<vscode.AuthenticationSession[]> {
167+
let sessions = await getAuthSessions(context);
168+
sessions = sessions.filter(session => validateSession(session));
169+
await storeAuthSessions(sessions, context);
170+
return sessions;
171+
}
180172

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];
173+
export async function validateSession(session: vscode.AuthenticationSession): Promise<boolean> {
174+
try {
175+
const hash = crypto.createHash('sha256').update(session.accessToken, 'utf8').digest('hex');
176+
const tokenScopes = new Set(await withServerApi(session.accessToken, service => service.server.getGitpodTokenScopes(hash)));
177+
for (const scope of gitpodScopes) {
178+
if (!tokenScopes.has(scope)) {
179+
return false;
180+
}
185181
}
182+
return true;
183+
} catch (e) {
184+
if (e.message !== unauthorizedErr) {
185+
console.error('gitpod: invalid session:', e);
186+
}
187+
return false;
186188
}
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;
193189
}
194190

195191
/**
@@ -232,16 +228,23 @@ export async function setSettingsSync(enabled?: boolean): Promise<void> {
232228
}
233229
}
234230

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 () => {
231+
class GitpodServerApi extends vscode.Disposable {
232+
233+
readonly service: GitpodConnection;
234+
private readonly socket: ReconnectingWebSocket;
235+
private readonly onWillCloseEmitter = new vscode.EventEmitter<number | undefined>();
236+
readonly onWillClose = this.onWillCloseEmitter.event;
237+
238+
constructor(accessToken: string) {
239+
super(() => {
240+
this.close();
241+
this.onWillCloseEmitter.dispose();
242+
});
243+
const factory = new JsonRpcProxyFactory<GitpodServer>();
244+
this.service = new GitpodServiceImpl<GitpodClient, GitpodServer>(factory.createProxy());
245+
246+
let retry = 1;
247+
const maxRetries = 3;
245248
class GitpodServerWebSocket extends WebSocket {
246249
constructor(address: string, protocols?: string | string[]) {
247250
super(address, protocols, {
@@ -250,35 +253,63 @@ async function createApiWebSocket(accessToken: string): Promise<{ gitpodService:
250253
'Authorization': `Bearer ${accessToken}`
251254
}
252255
});
256+
this.on('unexpected-response', (_, resp) => {
257+
this.terminate();
258+
259+
// if mal-formed handshake request (unauthorized, forbidden) or client actions (redirect) are required then fail immediately
260+
// otherwise try several times and fail, maybe temporarily unavailable, like server restart
261+
if (retry++ >= maxRetries || (typeof resp.statusCode === 'number' && 300 <= resp.statusCode && resp.statusCode < 500)) {
262+
socket.close(resp.statusCode);
263+
}
264+
});
253265
}
254266
}
255-
const webSocketMaxRetries = 3;
256-
const webSocket = new ReconnectingWebSocket(`${getBaseURL().replace('https', 'wss')}/api/v1`, undefined, {
267+
const socket = new ReconnectingWebSocket(`${getBaseURL().replace('https', 'wss')}/api/v1`, undefined, {
268+
maxReconnectionDelay: 10000,
257269
minReconnectionDelay: 1000,
270+
reconnectionDelayGrowFactor: 1.5,
258271
connectionTimeout: 10000,
259-
maxRetries: webSocketMaxRetries - 1,
272+
maxRetries: Infinity,
260273
debug: false,
261274
startClosed: false,
262275
WebSocket: GitpodServerWebSocket
263276
});
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-
}
277+
socket.onerror = e => {
278+
console.error('gitpod: server api: failed to open socket:', e);
271279
};
272280

273281
doListen({
274-
webSocket,
282+
webSocket: socket,
275283
logger: new ConsoleLogger(),
276284
onConnection: connection => factory.listen(connection),
277285
});
278-
return webSocket;
279-
})();
286+
this.socket = socket;
287+
}
288+
289+
private close(statusCode?: number): void {
290+
this.onWillCloseEmitter.fire(statusCode);
291+
try {
292+
this.socket.close();
293+
} catch (e) {
294+
console.error('gitpod: server api: failed to close socket:', e);
295+
}
296+
}
280297

281-
return { gitpodService, pendignWebSocket };
298+
}
299+
300+
const unauthorizedErr = 'unauthorized';
301+
function withServerApi<T>(accessToken: string, cb: (service: GitpodConnection) => Promise<T>): Promise<T> {
302+
const api = new GitpodServerApi(accessToken);
303+
return Promise.race([
304+
cb(api.service),
305+
new Promise<T>((_, reject) => api.onWillClose(statusCode => {
306+
if (statusCode === 401) {
307+
reject(new Error(unauthorizedErr));
308+
} else {
309+
reject(new Error('closed'));
310+
}
311+
}))
312+
]).finally(() => api.dispose());
282313
}
283314

284315
interface ExchangeTokenResponse {
@@ -315,13 +346,10 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
315346
}
316347

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

322-
const { gitpodService, pendignWebSocket } = await createApiWebSocket(accessToken);
323-
const user = await gitpodService.server.getLoggedInUser();
324-
(await pendignWebSocket).close();
352+
const user = await withServerApi(accessToken, service => service.server.getLoggedInUser());
325353
return {
326354
id: 'gitpod.user',
327355
account: {
@@ -337,22 +365,6 @@ export async function resolveAuthenticationSession(scopes: readonly string[], co
337365
}
338366
}
339367

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-
356368
/**
357369
* Creates a URL to be opened for the whole OAuth2 flow to kick-off
358370
* @returns a `URL` string containing the whole auth URL
@@ -361,8 +373,6 @@ async function createOauth2URL(context: vscode.ExtensionContext, options: { auth
361373
const { authorizationURI, clientID, redirectURI, scopes } = options;
362374
const { codeChallenge, codeVerifier }: { codeChallenge: string, codeVerifier: string } = create();
363375

364-
console.log(`Verifier: ${codeVerifier}`);
365-
366376
let query = '';
367377
function set(field: string, value: string): void {
368378
if (query) {
@@ -422,13 +432,11 @@ export async function createSession(scopes: readonly string[], context: vscode.E
422432
reject('Login timed out.');
423433
}, 1000 * 60 * 5); // 5 minutes
424434
});
425-
console.log(gitpodAuth);
426435
const opened = await vscode.env.openExternal(gitpodAuth as any);
427436
if (!opened) {
428437
const selected = await vscode.window.showErrorMessage(`Couldn't open ${gitpodAuth} automatically, please copy and paste it to your browser manually.`, 'Copy', 'Cancel');
429438
if (selected === 'Copy') {
430439
vscode.env.clipboard.writeText(gitpodAuth);
431-
console.log('Copied auth URL');
432440
}
433441
}
434442

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)