diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 503a599d48a..ab523e39929 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -21,6 +21,7 @@ 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 + expiresAt?: number; // UNIX epoch time at which token will expire refreshToken: string; accountName: string; @@ -183,12 +184,33 @@ export class AzureActiveDirectoryService { private convertToSession(token: IToken): vscode.AuthenticationSession { return { id: token.sessionId, - accessToken: () => !token.accessToken ? Promise.reject('Unavailable due to network problems') : Promise.resolve(token.accessToken), + accessToken: () => this.resolveAccessToken(token), accountName: token.accountName, scopes: token.scope.split(' ') }; } + private async resolveAccessToken(token: IToken): Promise { + if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { + Logger.info('Token available from cache'); + return Promise.resolve(token.accessToken); + } + + try { + Logger.info('Token expired or unavailable, trying refresh'); + const refreshedToken = await this.refreshToken(token.refreshToken, token.scope); + if (refreshedToken.accessToken) { + Promise.resolve(token.accessToken); + } else { + throw new Error(); + } + } catch (e) { + throw new Error('Unavailable due to network problems'); + } + + throw new Error('Unavailable due to network problems'); + } + private getTokenClaims(accessToken: string): ITokenClaims { try { return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString()); @@ -374,6 +396,7 @@ export class AzureActiveDirectoryService { const claims = this.getTokenClaims(json.access_token); return { expiresIn: json.expires_in, + expiresAt: Date.now() + json.expires_in * 1000, accessToken: json.access_token, refreshToken: json.refresh_token, scope, diff --git a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts index 03ba0c45dfe..5bdb66fb147 100644 --- a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts +++ b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts @@ -14,6 +14,9 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut private _onDidChangeToken: Emitter = this._register(new Emitter()); readonly onDidChangeToken: Event = this._onDidChangeToken.event; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + private _token: string | undefined; constructor() { @@ -30,4 +33,8 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut this._onDidChangeToken.fire(token); } } + + sendTokenFailed(): void { + this._onTokenFailed.fire(); + } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 9a10d933259..456c21f56b8 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -308,9 +308,11 @@ export interface IUserDataAuthTokenService { _serviceBrand: undefined; readonly onDidChangeToken: Event; + readonly onTokenFailed: Event; getToken(): Promise; setToken(accessToken: string | undefined): Promise; + sendTokenFailed(): void; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index ec38e651598..64f36ee5edc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -96,6 +96,7 @@ export class UserDataAuthTokenServiceChannel implements IServerChannel { listen(_: unknown, event: string): Event { switch (event) { case 'onDidChangeToken': return this.service.onDidChangeToken; + case 'onTokenFailed': return this.service.onTokenFailed; } throw new Error(`Event not found: ${event}`); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 5943d11c42c..537617b5a42 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -133,6 +133,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (context.res.statusCode === 401) { + this.authTokenService.sendTokenFailed(); throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0a34da8ecd0..1d5486d52ab 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -122,6 +122,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources))); + this._register(this.userDataAuthTokenService.onTokenFailed(_ => this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts index 998630e1ef9..6c39298ada7 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts @@ -18,11 +18,15 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut private _onDidChangeToken: Emitter = this._register(new Emitter()); readonly onDidChangeToken: Event = this._onDidChangeToken.event; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, ) { super(); this.channel = sharedProcessService.getChannel('authToken'); + this._register(this.channel.listen('onTokenFailed')(_ => this.sendTokenFailed())); } getToken(): Promise { @@ -32,6 +36,10 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut setToken(token: string | undefined): Promise { return this.channel.call('setToken', token); } + + sendTokenFailed(): void { + this._onTokenFailed.fire(); + } } registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService);