Handle incomplete login requests gracefully, fixes #109102

This commit is contained in:
Rachel Macfarlane
2020-10-23 09:35:23 -07:00
parent 0124f68884
commit 102e0e6d84
2 changed files with 82 additions and 34 deletions

View File

@@ -25,36 +25,7 @@ export const uriHandler = new UriEventHandler;
const onDidManuallyProvideToken = new vscode.EventEmitter<string>();
const exchangeCodeForToken: (state: string) => PromiseAdapter<vscode.Uri, string> =
(state) => async (uri, resolve, reject) => {
Logger.info('Exchanging code for token...');
const query = parseQuery(uri);
const code = query.code;
if (query.state !== state) {
reject('Received mismatched state');
return;
}
try {
const result = await fetch(`https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${state}`, {
method: 'POST',
headers: {
Accept: 'application/json'
}
});
if (result.ok) {
const json = await result.json();
Logger.info('Token exchange success!');
resolve(json.access_token);
} else {
reject(result.statusText);
}
} catch (ex) {
reject(ex);
}
};
function parseQuery(uri: vscode.Uri) {
return uri.query.split('&').reduce((prev: any, current) => {
@@ -67,6 +38,9 @@ function parseQuery(uri: vscode.Uri) {
export class GitHubServer {
private _statusBarItem: vscode.StatusBarItem | undefined;
private _pendingStates = new Map<string, string[]>();
private _codeExchangePromises = new Map<string, Promise<string>>();
private isTestEnvironment(url: vscode.Uri): boolean {
return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:');
}
@@ -91,18 +65,63 @@ export class GitHubServer {
this.updateStatusBarItem(false);
return token;
} else {
const existingStates = this._pendingStates.get(scopes) || [];
this._pendingStates.set(scopes, [...existingStates, state]);
const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`);
await vscode.env.openExternal(uri);
}
// Register a single listener for the URI callback, in case the user starts the login process multiple times
// before completing it.
let existingPromise = this._codeExchangePromises.get(scopes);
if (!existingPromise) {
existingPromise = promiseFromEvent(uriHandler.event, this.exchangeCodeForToken(scopes));
this._codeExchangePromises.set(scopes, existingPromise);
}
return Promise.race([
promiseFromEvent(uriHandler.event, exchangeCodeForToken(state)),
existingPromise,
promiseFromEvent<string, string>(onDidManuallyProvideToken.event)
]).finally(() => {
this._pendingStates.delete(scopes);
this._codeExchangePromises.delete(scopes);
this.updateStatusBarItem(false);
});
}
private exchangeCodeForToken: (scopes: string) => PromiseAdapter<vscode.Uri, string> =
(scopes) => async (uri, resolve, reject) => {
Logger.info('Exchanging code for token...');
const query = parseQuery(uri);
const code = query.code;
const acceptedStates = this._pendingStates.get(scopes) || [];
if (!acceptedStates.includes(query.state)) {
reject('Received mismatched state');
return;
}
try {
const result = await fetch(`https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`, {
method: 'POST',
headers: {
Accept: 'application/json'
}
});
if (result.ok) {
const json = await result.json();
Logger.info('Token exchange success!');
resolve(json.access_token);
} else {
reject(result.statusText);
}
} catch (ex) {
reject(ex);
}
};
private updateStatusBarItem(isStart?: boolean) {
if (isStart && !this._statusBarItem) {
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);