Merge from vscode 0a7364f00514c46c9caceece15e1f82f82e3712f

This commit is contained in:
ADS Merger
2020-07-22 03:06:57 +00:00
parent 53ec7585a9
commit 1b7b54ce14
229 changed files with 5099 additions and 3188 deletions

View File

@@ -3,15 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import * as https from 'https';
import * as randomBytes from 'randombytes';
import * as querystring from 'querystring';
import * as vscode from 'vscode';
import * as uuid from 'uuid';
import { createServer, startServer } from './authServer';
import { v4 as uuid } from 'uuid';
import { keychain } from './keychain';
import Logger from './logger';
import { toBase64UrlEncoding } from './utils';
import fetch from 'node-fetch';
import { sha256 } from './env/node/sha256';
const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
const loginEndpointUrl = 'https://login.microsoftonline.com/';
@@ -21,7 +23,7 @@ const tenant = 'organizations';
interface IToken {
accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
expiresIn?: string; // How long access token is valid, in seconds
expiresIn?: number; // How long access token is valid, in seconds
expiresAt?: number; // UNIX epoch time at which token will expire
refreshToken: string;
@@ -54,6 +56,15 @@ interface IStoredSession {
}
}
export interface ITokenResponse {
access_token: string;
expires_in: number;
ext_expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
function parseQuery(uri: vscode.Uri) {
return uri.query.split('&').reduce((prev: any, current) => {
const queryString = current.split('=');
@@ -208,7 +219,12 @@ export class AzureActiveDirectoryService {
private async convertToSession(token: IToken): Promise<vscode.AuthenticationSession> {
const resolvedToken = await this.resolveAccessToken(token);
return new vscode.AuthenticationSession(token.sessionId, resolvedToken, token.account, token.scope.split(' '));
return {
id: token.sessionId,
accessToken: resolvedToken,
account: token.account,
scopes: token.scope.split(' ')
};
}
private async resolveAccessToken(token: IToken): Promise<string> {
@@ -257,7 +273,7 @@ export class AzureActiveDirectoryService {
return;
}
const nonce = crypto.randomBytes(16).toString('base64');
const nonce = randomBytes(16).toString('base64');
const { server, redirectPromise, codePromise } = createServer(nonce);
let token: IToken | undefined;
@@ -279,8 +295,8 @@ export class AzureActiveDirectoryService {
const state = `${updatedPort},${encodeURIComponent(nonce)}`;
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
await redirectReq.res.writeHead(302, { Location: loginUrl });
@@ -341,14 +357,14 @@ export class AzureActiveDirectoryService {
private async loginWithoutLocalServer(scope: string): Promise<vscode.AuthenticationSession> {
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`));
const nonce = crypto.randomBytes(16).toString('base64');
const nonce = randomBytes(16).toString('base64');
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const callbackEnvironment = this.getCallbackEnvironment(callbackUri);
const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`;
let uri = vscode.Uri.parse(signInUrl);
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
uri = uri.with({
query: `response_type=code&client_id=${encodeURIComponent(clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scope}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`
});
@@ -421,14 +437,13 @@ export class AzureActiveDirectoryService {
onDidChangeSessions.fire({ added: [], removed: [token.sessionId], changed: [] });
}
}
}, 1000 * (parseInt(token.expiresIn) - 30)));
}, 1000 * (token.expiresIn - 30)));
}
this.storeTokenData();
}
private getTokenFromResponse(buffer: Buffer[], scope: string, existingId?: string): IToken {
const json = JSON.parse(Buffer.concat(buffer).toString());
private getTokenFromResponse(json: ITokenResponse, scope: string, existingId?: string): IToken {
const claims = this.getTokenClaims(json.access_token);
return {
expiresIn: json.expires_in,
@@ -445,60 +460,42 @@ export class AzureActiveDirectoryService {
}
private async exchangeCodeForToken(code: string, codeVerifier: string, scope: string): Promise<IToken> {
return new Promise((resolve: (value: IToken) => void, reject) => {
Logger.info('Exchanging login code for token');
try {
const postData = querystring.stringify({
grant_type: 'authorization_code',
code: code,
client_id: clientId,
scope: scope,
code_verifier: codeVerifier,
redirect_uri: redirectUrl
});
Logger.info('Exchanging login code for token');
try {
const postData = querystring.stringify({
grant_type: 'authorization_code',
code: code,
client_id: clientId,
scope: scope,
code_verifier: codeVerifier,
redirect_uri: redirectUrl
});
const tokenUrl = vscode.Uri.parse(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`);
const result = await fetch(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length.toString()
},
body: postData
});
const post = https.request({
host: tokenUrl.authority,
path: tokenUrl.path,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
}, result => {
const buffer: Buffer[] = [];
result.on('data', (chunk: Buffer) => {
buffer.push(chunk);
});
result.on('end', () => {
if (result.statusCode === 200) {
Logger.info('Exchanging login code for token success');
resolve(this.getTokenFromResponse(buffer, scope));
} else {
Logger.error('Exchanging login code for token failed');
reject(new Error('Unable to login.'));
}
});
});
post.write(postData);
post.end();
post.on('error', err => {
reject(err);
});
} catch (e) {
Logger.error(e.message);
reject(e);
if (result.ok) {
Logger.info('Exchanging login code for token success');
const json = await result.json();
return this.getTokenFromResponse(json, scope);
} else {
Logger.error('Exchanging login code for token failed');
throw new Error('Unable to login.');
}
});
} catch (e) {
Logger.error(e.message);
throw e;
}
}
private async refreshToken(refreshToken: string, scope: string, sessionId: string): Promise<IToken> {
return new Promise((resolve: (value: IToken) => void, reject) => {
try {
Logger.info('Refreshing token...');
const postData = querystring.stringify({
refresh_token: refreshToken,
@@ -507,40 +504,29 @@ export class AzureActiveDirectoryService {
scope: scope
});
const post = https.request({
host: 'login.microsoftonline.com',
path: `/${tenant}/oauth2/v2.0/token`,
const result = await fetch(`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
}, result => {
const buffer: Buffer[] = [];
result.on('data', (chunk: Buffer) => {
buffer.push(chunk);
});
result.on('end', async () => {
if (result.statusCode === 200) {
const token = this.getTokenFromResponse(buffer, scope, sessionId);
this.setToken(token, scope);
Logger.info('Token refresh success');
resolve(token);
} else {
Logger.error('Refreshing token failed');
reject(new Error('Refreshing token failed.'));
}
});
'Content-Length': postData.length.toString()
},
body: postData
});
post.write(postData);
post.end();
post.on('error', err => {
Logger.error(err.message);
reject(new Error(REFRESH_NETWORK_FAILURE));
});
});
if (result.ok) {
const json = await result.json();
const token = this.getTokenFromResponse(json, scope, sessionId);
this.setToken(token, scope);
Logger.info('Token refresh success');
return token;
} else {
Logger.error('Refreshing token failed');
throw new Error('Refreshing token failed.');
}
} catch (e) {
Logger.error('Refreshing token failed');
throw e;
}
}
private clearSessionTimeout(sessionId: string): void {

View File

@@ -6,7 +6,6 @@
import * as http from 'http';
import * as url from 'url';
import * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
interface Deferred<T> {
@@ -14,58 +13,17 @@ interface Deferred<T> {
reject: (reason: any) => void;
}
const _typeof = {
number: 'number',
string: 'string',
undefined: 'undefined',
object: 'object',
function: 'function'
};
/**
* @returns whether the provided parameter is undefined.
*/
export function isUndefined(obj: any): obj is undefined {
return typeof (obj) === _typeof.undefined;
}
/**
* @returns whether the provided parameter is undefined or null.
*/
export function isUndefinedOrNull(obj: any): obj is undefined | null {
return isUndefined(obj) || obj === null;
}
/**
* Asserts that the argument passed in is neither undefined nor null.
*/
export function assertIsDefined<T>(arg: T | null | undefined): T {
if (isUndefinedOrNull(arg)) {
function assertIsDefined<T>(arg: T | null | undefined): T {
if (typeof (arg) === 'undefined' || arg === null) {
throw new Error('Assertion Failed: argument is undefined or null');
}
return arg;
}
export function createTerminateServer(server: http.Server) {
const sockets: Record<number, net.Socket> = {};
let socketCount = 0;
server.on('connection', socket => {
const id = socketCount++;
sockets[id] = socket;
socket.on('close', () => {
delete sockets[id];
});
});
return async () => {
const result = new Promise<Error | undefined>(resolve => server.close(resolve));
for (const id in sockets) {
sockets[id].destroy();
}
return result;
};
}
export async function startServer(server: http.Server): Promise<string> {
let portTimer: NodeJS.Timer;

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function startServer(_: any): any {
throw new Error('Not implemented');
}
export function createServer(_: any): any {
throw new Error('Not implemented');
}

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export async function sha256(s: string | Uint8Array): Promise<string> {
const createHash = require('sha.js');
return createHash('sha256').update(s).digest('base64');
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export async function sha256(s: string | Uint8Array): Promise<string> {
return (require('crypto')).createHash('sha256').update(s).digest('base64');
}