mirror of
https://github.com/Microsoft/vscode
synced 2024-10-06 03:17:00 +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 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);
|
||||
|
|
|
@ -95,6 +95,11 @@ export class AzureActiveDirectoryService {
|
|||
private _uriHandler: UriEventHandler;
|
||||
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() {
|
||||
this._uriHandler = new UriEventHandler();
|
||||
this._disposables.push(vscode.window.registerUriHandler(this._uriHandler));
|
||||
|
@ -385,10 +390,28 @@ export class AzureActiveDirectoryService {
|
|||
}, 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);
|
||||
}
|
||||
|
||||
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(state: string, codeVerifier: string, scope: string): Promise<vscode.AuthenticationSession> {
|
||||
private async handleCodeResponse(scope: string): Promise<vscode.AuthenticationSession> {
|
||||
let uriEventListener: vscode.Disposable;
|
||||
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
|
||||
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
|
||||
|
@ -396,12 +419,18 @@ export class AzureActiveDirectoryService {
|
|||
const query = parseQuery(uri);
|
||||
const code = query.code;
|
||||
|
||||
const acceptedStates = this._pendingStates.get(scope) || [];
|
||||
// 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.');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const session = await this.convertToSession(token);
|
||||
|
|
Loading…
Reference in a new issue