mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229 (#8962)
* Merge from vscode 3c6f6af7347d38e87bc6406024e8dcf9e9bce229 * skip failing tests * update mac build image
This commit is contained in:
committed by
Karl Burtram
parent
0eaee18dc4
commit
fefe1454de
@@ -4,52 +4,131 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as querystring from 'querystring';
|
||||
import { keychain } from './keychain';
|
||||
import { toBase64UrlEncoding } from './utils';
|
||||
import * as vscode from 'vscode';
|
||||
import { createServer, startServer } from './authServer';
|
||||
import { keychain } from './keychain';
|
||||
import Logger from './logger';
|
||||
import { toBase64UrlEncoding } from './utils';
|
||||
|
||||
const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
|
||||
const loginEndpointUrl = 'https://login.microsoftonline.com/';
|
||||
const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56';
|
||||
const scope = 'https://management.core.windows.net/.default offline_access';
|
||||
const tenant = 'common';
|
||||
const tenant = 'organizations';
|
||||
|
||||
interface IToken {
|
||||
expiresIn: string; // How long access token is valid, in seconds
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
|
||||
displayName: string;
|
||||
scope: string;
|
||||
sessionId: string; // The account id + the scope
|
||||
}
|
||||
|
||||
interface ITokenClaims {
|
||||
tid: string;
|
||||
email?: string;
|
||||
unique_name?: string;
|
||||
oid?: string;
|
||||
altsecid?: string;
|
||||
scp: string;
|
||||
}
|
||||
|
||||
interface IStoredSession {
|
||||
id: string;
|
||||
refreshToken: string;
|
||||
scope: string; // Scopes are alphabetized and joined with a space
|
||||
}
|
||||
|
||||
export const onDidChangeSessions = new vscode.EventEmitter<void>();
|
||||
|
||||
export class AzureActiveDirectoryService {
|
||||
private _token: IToken | undefined;
|
||||
private _refreshTimeout: NodeJS.Timeout | undefined;
|
||||
private _tokens: IToken[] = [];
|
||||
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
const existingRefreshToken = await keychain.getToken();
|
||||
if (existingRefreshToken) {
|
||||
await this.refreshToken(existingRefreshToken);
|
||||
const storedData = await keychain.getToken();
|
||||
if (storedData) {
|
||||
try {
|
||||
const sessions = this.parseStoredData(storedData);
|
||||
const refreshes = sessions.map(async session => {
|
||||
try {
|
||||
await this.refreshToken(session.refreshToken, session.scope);
|
||||
} catch (e) {
|
||||
await this.logout(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(refreshes);
|
||||
} catch (e) {
|
||||
await this.clearSessions();
|
||||
}
|
||||
}
|
||||
|
||||
this.pollForChange();
|
||||
}
|
||||
|
||||
private parseStoredData(data: string): IStoredSession[] {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
private async storeTokenData(): Promise<void> {
|
||||
const serializedData: IStoredSession[] = this._tokens.map(token => {
|
||||
return {
|
||||
id: token.sessionId,
|
||||
refreshToken: token.refreshToken,
|
||||
scope: token.scope
|
||||
};
|
||||
});
|
||||
|
||||
await keychain.setToken(JSON.stringify(serializedData));
|
||||
}
|
||||
|
||||
private pollForChange() {
|
||||
setTimeout(async () => {
|
||||
const refreshToken = await keychain.getToken();
|
||||
// Another window has logged in, generate access token for this instance.
|
||||
if (refreshToken && !this._token) {
|
||||
await this.refreshToken(refreshToken);
|
||||
onDidChangeSessions.fire();
|
||||
let didChange = false;
|
||||
const storedData = await keychain.getToken();
|
||||
if (storedData) {
|
||||
try {
|
||||
const sessions = this.parseStoredData(storedData);
|
||||
let promises = sessions.map(async session => {
|
||||
const matchesExisting = this._tokens.some(token => token.scope === session.scope && token.sessionId === session.id);
|
||||
if (!matchesExisting) {
|
||||
try {
|
||||
await this.refreshToken(session.refreshToken, session.scope);
|
||||
didChange = true;
|
||||
} catch (e) {
|
||||
await this.logout(session.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
promises = promises.concat(this._tokens.map(async token => {
|
||||
const matchesExisting = sessions.some(session => token.scope === session.scope && token.sessionId === session.id);
|
||||
if (!matchesExisting) {
|
||||
await this.logout(token.sessionId);
|
||||
didChange = true;
|
||||
}
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (e) {
|
||||
Logger.error(e.message);
|
||||
// if data is improperly formatted, remove all of it and send change event
|
||||
this.clearSessions();
|
||||
didChange = true;
|
||||
}
|
||||
} else {
|
||||
if (this._tokens.length) {
|
||||
// Log out all
|
||||
await this.clearSessions();
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Another window has logged out
|
||||
if (!refreshToken && this._token) {
|
||||
await this.logout();
|
||||
if (didChange) {
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
|
||||
@@ -57,31 +136,30 @@ export class AzureActiveDirectoryService {
|
||||
}, 1000 * 30);
|
||||
}
|
||||
|
||||
private tokenToAccount(token: IToken): vscode.Session {
|
||||
private convertToSession(token: IToken): vscode.Session {
|
||||
return {
|
||||
id: '',
|
||||
id: token.sessionId,
|
||||
accessToken: token.accessToken,
|
||||
displayName: this.getDisplayNameFromToken(token.accessToken)
|
||||
displayName: token.displayName,
|
||||
scopes: token.scope.split(' ')
|
||||
};
|
||||
}
|
||||
|
||||
private getDisplayNameFromToken(accessToken: string): string {
|
||||
let displayName = 'user@example.com';
|
||||
private getTokenClaims(accessToken: string): ITokenClaims {
|
||||
try {
|
||||
// TODO fixme
|
||||
displayName = JSON.parse(atob(accessToken.split('.')[1]));
|
||||
return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
|
||||
} catch (e) {
|
||||
// Fall back to example display name
|
||||
Logger.error(e.message);
|
||||
throw new Error('Unable to read token claims');
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
get sessions(): vscode.Session[] {
|
||||
return this._token ? [this.tokenToAccount(this._token)] : [];
|
||||
return this._tokens.map(token => this.convertToSession(token));
|
||||
}
|
||||
|
||||
public async login(): Promise<void> {
|
||||
public async login(scope: string): Promise<void> {
|
||||
Logger.info('Logging in...');
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
const { server, redirectPromise, codePromise } = createServer(nonce);
|
||||
|
||||
@@ -118,11 +196,13 @@ export class AzureActiveDirectoryService {
|
||||
if ('err' in codeRes) {
|
||||
throw codeRes.err;
|
||||
}
|
||||
token = await this.exchangeCodeForToken(codeRes.code, codeVerifier);
|
||||
this.setToken(token);
|
||||
token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope);
|
||||
this.setToken(token, scope);
|
||||
Logger.info('Login successful');
|
||||
res.writeHead(302, { Location: '/' });
|
||||
res.end();
|
||||
} catch (err) {
|
||||
Logger.error(err.message);
|
||||
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
|
||||
res.end();
|
||||
}
|
||||
@@ -133,28 +213,48 @@ export class AzureActiveDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private async setToken(token: IToken): Promise<void> {
|
||||
this._token = token;
|
||||
|
||||
if (this._refreshTimeout) {
|
||||
clearTimeout(this._refreshTimeout);
|
||||
private async setToken(token: IToken, scope: string): Promise<void> {
|
||||
const existingToken = this._tokens.findIndex(t => t.sessionId === token.sessionId);
|
||||
if (existingToken) {
|
||||
this._tokens.splice(existingToken, 1, token);
|
||||
} else {
|
||||
this._tokens.push(token);
|
||||
}
|
||||
|
||||
this._refreshTimeout = setTimeout(async () => {
|
||||
const existingTimeout = this._refreshTimeouts.get(token.sessionId);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
|
||||
this._refreshTimeouts.set(token.sessionId, setTimeout(async () => {
|
||||
try {
|
||||
await this.refreshToken(token.refreshToken);
|
||||
await this.refreshToken(token.refreshToken, scope);
|
||||
} catch (e) {
|
||||
await this.logout();
|
||||
await this.logout(token.sessionId);
|
||||
} finally {
|
||||
onDidChangeSessions.fire();
|
||||
}
|
||||
}, 1000 * (parseInt(token.expiresIn) - 10));
|
||||
}, 1000 * (parseInt(token.expiresIn) - 10)));
|
||||
|
||||
await keychain.setToken(token.refreshToken);
|
||||
this.storeTokenData();
|
||||
}
|
||||
|
||||
private async exchangeCodeForToken(code: string, codeVerifier: string): Promise<IToken> {
|
||||
private getTokenFromResponse(buffer: Buffer[], scope: string): IToken {
|
||||
const json = JSON.parse(Buffer.concat(buffer).toString());
|
||||
const claims = this.getTokenClaims(json.access_token);
|
||||
return {
|
||||
expiresIn: json.expires_in,
|
||||
accessToken: json.access_token,
|
||||
refreshToken: json.refresh_token,
|
||||
scope,
|
||||
sessionId: claims.tid + (claims.oid || claims.altsecid) + scope,
|
||||
displayName: claims.email || claims.unique_name || 'user@example.com'
|
||||
};
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -182,12 +282,7 @@ export class AzureActiveDirectoryService {
|
||||
});
|
||||
result.on('end', () => {
|
||||
if (result.statusCode === 200) {
|
||||
const json = JSON.parse(Buffer.concat(buffer).toString());
|
||||
resolve({
|
||||
expiresIn: json.expires_in,
|
||||
accessToken: json.access_token,
|
||||
refreshToken: json.refresh_token
|
||||
});
|
||||
resolve(this.getTokenFromResponse(buffer, scope));
|
||||
} else {
|
||||
reject(new Error('Unable to login.'));
|
||||
}
|
||||
@@ -202,13 +297,15 @@ export class AzureActiveDirectoryService {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
Logger.error(e.message);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshToken(refreshToken: string): Promise<IToken> {
|
||||
private async refreshToken(refreshToken: string, scope: string): Promise<IToken> {
|
||||
return new Promise((resolve: (value: IToken) => void, reject) => {
|
||||
Logger.info('Refreshing token...');
|
||||
const postData = querystring.stringify({
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientId,
|
||||
@@ -231,16 +328,12 @@ export class AzureActiveDirectoryService {
|
||||
});
|
||||
result.on('end', async () => {
|
||||
if (result.statusCode === 200) {
|
||||
const json = JSON.parse(Buffer.concat(buffer).toString());
|
||||
const token = {
|
||||
expiresIn: json.expires_in,
|
||||
accessToken: json.access_token,
|
||||
refreshToken: json.refresh_token
|
||||
};
|
||||
this.setToken(token);
|
||||
const token = this.getTokenFromResponse(buffer, scope);
|
||||
this.setToken(token, scope);
|
||||
Logger.info('Token refresh success');
|
||||
resolve(token);
|
||||
} else {
|
||||
await this.logout();
|
||||
Logger.error('Refreshing token failed');
|
||||
reject(new Error('Refreshing token failed.'));
|
||||
}
|
||||
});
|
||||
@@ -250,16 +343,41 @@ export class AzureActiveDirectoryService {
|
||||
|
||||
post.end();
|
||||
post.on('error', err => {
|
||||
Logger.error(err.message);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
delete this._token;
|
||||
await keychain.deleteToken();
|
||||
if (this._refreshTimeout) {
|
||||
clearTimeout(this._refreshTimeout);
|
||||
public async logout(sessionId: string) {
|
||||
Logger.info(`Logging out of session '${sessionId}'`);
|
||||
const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId);
|
||||
if (tokenIndex > -1) {
|
||||
this._tokens.splice(tokenIndex, 1);
|
||||
}
|
||||
|
||||
if (this._tokens.length === 0) {
|
||||
await keychain.deleteToken();
|
||||
} else {
|
||||
this.storeTokenData();
|
||||
}
|
||||
|
||||
const timeout = this._refreshTimeouts.get(sessionId);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this._refreshTimeouts.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public async clearSessions() {
|
||||
Logger.info('Logging out of all sessions');
|
||||
this._tokens = [];
|
||||
await keychain.deleteToken();
|
||||
|
||||
this._refreshTimeouts.forEach(timeout => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
|
||||
this._refreshTimeouts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user