mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Handle incomplete login requests gracefully, fixes #109102
This commit is contained in:
parent
0124f68884
commit
102e0e6d84
|
@ -25,36 +25,7 @@ export const uriHandler = new UriEventHandler;
|
||||||
|
|
||||||
const onDidManuallyProvideToken = new vscode.EventEmitter<string>();
|
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) {
|
function parseQuery(uri: vscode.Uri) {
|
||||||
return uri.query.split('&').reduce((prev: any, current) => {
|
return uri.query.split('&').reduce((prev: any, current) => {
|
||||||
|
@ -67,6 +38,9 @@ function parseQuery(uri: vscode.Uri) {
|
||||||
export class GitHubServer {
|
export class GitHubServer {
|
||||||
private _statusBarItem: vscode.StatusBarItem | undefined;
|
private _statusBarItem: vscode.StatusBarItem | undefined;
|
||||||
|
|
||||||
|
private _pendingStates = new Map<string, string[]>();
|
||||||
|
private _codeExchangePromises = new Map<string, Promise<string>>();
|
||||||
|
|
||||||
private isTestEnvironment(url: vscode.Uri): boolean {
|
private isTestEnvironment(url: vscode.Uri): boolean {
|
||||||
return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:');
|
return url.authority === 'vscode-web-test-playground.azurewebsites.net' || url.authority.startsWith('localhost:');
|
||||||
}
|
}
|
||||||
|
@ -91,18 +65,63 @@ export class GitHubServer {
|
||||||
this.updateStatusBarItem(false);
|
this.updateStatusBarItem(false);
|
||||||
return token;
|
return token;
|
||||||
} else {
|
} 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`);
|
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);
|
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([
|
return Promise.race([
|
||||||
promiseFromEvent(uriHandler.event, exchangeCodeForToken(state)),
|
existingPromise,
|
||||||
promiseFromEvent<string, string>(onDidManuallyProvideToken.event)
|
promiseFromEvent<string, string>(onDidManuallyProvideToken.event)
|
||||||
]).finally(() => {
|
]).finally(() => {
|
||||||
|
this._pendingStates.delete(scopes);
|
||||||
|
this._codeExchangePromises.delete(scopes);
|
||||||
this.updateStatusBarItem(false);
|
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) {
|
private updateStatusBarItem(isStart?: boolean) {
|
||||||
if (isStart && !this._statusBarItem) {
|
if (isStart && !this._statusBarItem) {
|
||||||
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||||
|
|
|
@ -95,6 +95,11 @@ export class AzureActiveDirectoryService {
|
||||||
private _uriHandler: UriEventHandler;
|
private _uriHandler: UriEventHandler;
|
||||||
private _disposables: vscode.Disposable[] = [];
|
private _disposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
|
// Used to keep track of current requests when not using the local server approach.
|
||||||
|
private _pendingStates = new Map<string, string[]>();
|
||||||
|
private _codeExchangePromises = new Map<string, Promise<vscode.AuthenticationSession>>();
|
||||||
|
private _codeVerfifiers = new Map<string, string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._uriHandler = new UriEventHandler();
|
this._uriHandler = new UriEventHandler();
|
||||||
this._disposables.push(vscode.window.registerUriHandler(this._uriHandler));
|
this._disposables.push(vscode.window.registerUriHandler(this._uriHandler));
|
||||||
|
@ -385,10 +390,28 @@ export class AzureActiveDirectoryService {
|
||||||
}, 1000 * 60 * 5);
|
}, 1000 * 60 * 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]);
|
const existingStates = this._pendingStates.get(scope) || [];
|
||||||
|
this._pendingStates.set(scope, [...existingStates, state]);
|
||||||
|
|
||||||
|
// 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(scope);
|
||||||
|
if (!existingPromise) {
|
||||||
|
existingPromise = this.handleCodeResponse(scope);
|
||||||
|
this._codeExchangePromises.set(scope, existingPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleCodeResponse(state: string, codeVerifier: string, scope: string): Promise<vscode.AuthenticationSession> {
|
this._codeVerfifiers.set(state, codeVerifier);
|
||||||
|
|
||||||
|
return Promise.race([existingPromise, timeoutPromise])
|
||||||
|
.finally(() => {
|
||||||
|
this._pendingStates.delete(scope);
|
||||||
|
this._codeExchangePromises.delete(scope);
|
||||||
|
this._codeVerfifiers.delete(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCodeResponse(scope: string): Promise<vscode.AuthenticationSession> {
|
||||||
let uriEventListener: vscode.Disposable;
|
let uriEventListener: vscode.Disposable;
|
||||||
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
|
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
|
||||||
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
|
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
|
||||||
|
@ -396,12 +419,18 @@ export class AzureActiveDirectoryService {
|
||||||
const query = parseQuery(uri);
|
const query = parseQuery(uri);
|
||||||
const code = query.code;
|
const code = query.code;
|
||||||
|
|
||||||
|
const acceptedStates = this._pendingStates.get(scope) || [];
|
||||||
// Workaround double encoding issues of state in web
|
// Workaround double encoding issues of state in web
|
||||||
if (query.state !== state && decodeURIComponent(query.state) !== state) {
|
if (!acceptedStates.includes(query.state) && !acceptedStates.includes(decodeURIComponent(query.state))) {
|
||||||
throw new Error('State does not match.');
|
throw new Error('State does not match.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await this.exchangeCodeForToken(code, codeVerifier, scope);
|
const verifier = this._codeVerfifiers.get(query.state) ?? this._codeVerfifiers.get(decodeURIComponent(query.state));
|
||||||
|
if (!verifier) {
|
||||||
|
throw new Error('No available code verifier');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await this.exchangeCodeForToken(code, verifier, scope);
|
||||||
this.setToken(token, scope);
|
this.setToken(token, scope);
|
||||||
|
|
||||||
const session = await this.convertToSession(token);
|
const session = await this.convertToSession(token);
|
||||||
|
|
Loading…
Reference in a new issue