mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 09:35:38 -05:00
Add token expiration handling for AzureMFA auth (#16936)
* refresh azure account token if it's expired before sending query/list requests * fix several connection checks && add more logging * fix async UI glitch during token refreshing * cleanup * minor fix * add test for refreshAzureAccountTokenIfNecessary * address comments * comments * comments * comments * error handling
This commit is contained in:
@@ -66,6 +66,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
private _onConnectionChanged = new Emitter<IConnectionParams>();
|
||||
private _onLanguageFlavorChanged = new Emitter<azdata.DidChangeLanguageFlavorParams>();
|
||||
private _connectionGlobalStatus = new ConnectionGlobalStatus(this._notificationService);
|
||||
private _uriToReconnectPromiseMap: { [uri: string]: Promise<IConnectionResult> } = {};
|
||||
|
||||
private _mementoContext: Memento;
|
||||
private _mementoObj: MementoObject;
|
||||
@@ -863,6 +864,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
this._logService.info(`No security tokens found for account`);
|
||||
}
|
||||
connection.options['azureAccountToken'] = token.token;
|
||||
connection.options['expiresOn'] = token.expiresOn;
|
||||
connection.options['password'] = '';
|
||||
return true;
|
||||
} else {
|
||||
@@ -874,6 +876,62 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Azure access token if it's expired.
|
||||
* @param uri connection uri
|
||||
* @returns true if no need to refresh or successfully refreshed token
|
||||
*/
|
||||
public async refreshAzureAccountTokenIfNecessary(uri: string): Promise<boolean> {
|
||||
const profile = this._connectionStatusManager.getConnectionProfile(uri);
|
||||
if (!profile) {
|
||||
this._logService.warn(`Connection not found for uri ${uri}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
//wait for the pending reconnction promise if any
|
||||
const previousReconnectPromise = this._uriToReconnectPromiseMap[uri];
|
||||
if (previousReconnectPromise) {
|
||||
this._logService.info(`Found pending reconnect promise for uri ${uri}, waiting.`);
|
||||
try {
|
||||
const previousConnectionResult = await previousReconnectPromise;
|
||||
if (previousConnectionResult && previousConnectionResult.connected) {
|
||||
this._logService.info(`Previous pending reconnection for uri ${uri} succeeded.`);
|
||||
return true;
|
||||
}
|
||||
this._logService.info(`Previous pending reconnection for uri ${uri} failed.`);
|
||||
} catch (err) {
|
||||
this._logService.info(`Previous pending reconnect promise for uri ${uri} is rejected with error ${err}, will attempt to reconnect if necessary.`);
|
||||
}
|
||||
}
|
||||
|
||||
const expiry = profile.options.expiresOn;
|
||||
if (typeof expiry === 'number' && !Number.isNaN(expiry)) {
|
||||
const currentTime = new Date().getTime() / 1000;
|
||||
const maxTolerance = 2 * 60; // two minutes
|
||||
if (expiry - currentTime < maxTolerance) {
|
||||
this._logService.info(`Access token expired for connection ${profile.id} with uri ${uri}`);
|
||||
try {
|
||||
const connectionResultPromise = this.connect(profile, uri);
|
||||
this._uriToReconnectPromiseMap[uri] = connectionResultPromise;
|
||||
const connectionResult = await connectionResultPromise;
|
||||
if (!connectionResult) {
|
||||
this._logService.error(`Failed to refresh connection ${profile.id} with uri ${uri}, invalid connection result.`);
|
||||
throw new Error(nls.localize('connection.invalidConnectionResult', "Connection result is invalid"));
|
||||
} else if (!connectionResult.connected) {
|
||||
this._logService.error(`Failed to refresh connection ${profile.id} with uri ${uri}, error code: ${connectionResult.errorCode}, error message: ${connectionResult.errorMessage}`);
|
||||
throw new Error(nls.localize('connection.refreshAzureTokenFailure', "Failed to refresh Azure account token for connection"));
|
||||
}
|
||||
this._logService.info(`Successfully refreshed token for connection ${profile.id} with uri ${uri}, result: ${connectionResult.connected} ${connectionResult.connectionProfile}, isConnected: ${this.isConnected(uri)}, ${this._connectionStatusManager.getConnectionProfile(uri)}`);
|
||||
return true;
|
||||
} finally {
|
||||
delete this._uriToReconnectPromiseMap[uri];
|
||||
}
|
||||
}
|
||||
this._logService.info(`No need to refresh Azure acccount token for connection ${profile.id} with uri ${uri}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Request Senders
|
||||
private async sendConnectRequest(connection: interfaces.IConnectionProfile, uri: string): Promise<boolean> {
|
||||
let connectionInfo = Object.assign({}, {
|
||||
@@ -1240,8 +1298,9 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
||||
return this._connectionStatusManager.isConnected(fileUri) ? this._connectionStatusManager.findConnection(fileUri) : undefined;
|
||||
}
|
||||
|
||||
public listDatabases(connectionUri: string): Thenable<azdata.ListDatabasesResult | undefined> {
|
||||
public async listDatabases(connectionUri: string): Promise<azdata.ListDatabasesResult | undefined> {
|
||||
const self = this;
|
||||
await this.refreshAzureAccountTokenIfNecessary(connectionUri);
|
||||
if (self.isConnected(connectionUri)) {
|
||||
return self.sendListDatabasesRequest(connectionUri);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user