mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-13 03:28:33 -05:00
Merge from vscode 0a7364f00514c46c9caceece15e1f82f82e3712f
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
12
extensions/microsoft-authentication/src/env/browser/authServer.ts
vendored
Normal file
12
extensions/microsoft-authentication/src/env/browser/authServer.ts
vendored
Normal 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');
|
||||
}
|
||||
11
extensions/microsoft-authentication/src/env/browser/sha256.ts
vendored
Normal file
11
extensions/microsoft-authentication/src/env/browser/sha256.ts
vendored
Normal 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');
|
||||
}
|
||||
10
extensions/microsoft-authentication/src/env/node/sha256.ts
vendored
Normal file
10
extensions/microsoft-authentication/src/env/node/sha256.ts
vendored
Normal 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');
|
||||
}
|
||||
Reference in New Issue
Block a user