Skip to content

Commit df3ae4a

Browse files
author
Rachel Macfarlane
committed
Refresh auth tokens when server returns 401, fixes microsoft#89629
1 parent 2158e77 commit df3ae4a

7 files changed

Lines changed: 44 additions & 1 deletion

File tree

extensions/vscode-account/src/AADHelper.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface IToken {
2121
accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
2222

2323
expiresIn?: string; // How long access token is valid, in seconds
24+
expiresAt?: number; // UNIX epoch time at which token will expire
2425
refreshToken: string;
2526

2627
accountName: string;
@@ -183,12 +184,33 @@ export class AzureActiveDirectoryService {
183184
private convertToSession(token: IToken): vscode.AuthenticationSession {
184185
return {
185186
id: token.sessionId,
186-
accessToken: () => !token.accessToken ? Promise.reject('Unavailable due to network problems') : Promise.resolve(token.accessToken),
187+
accessToken: () => this.resolveAccessToken(token),
187188
accountName: token.accountName,
188189
scopes: token.scope.split(' ')
189190
};
190191
}
191192

193+
private async resolveAccessToken(token: IToken): Promise<string> {
194+
if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) {
195+
Logger.info('Token available from cache');
196+
return Promise.resolve(token.accessToken);
197+
}
198+
199+
try {
200+
Logger.info('Token expired or unavailable, trying refresh');
201+
const refreshedToken = await this.refreshToken(token.refreshToken, token.scope);
202+
if (refreshedToken.accessToken) {
203+
Promise.resolve(token.accessToken);
204+
} else {
205+
throw new Error();
206+
}
207+
} catch (e) {
208+
throw new Error('Unavailable due to network problems');
209+
}
210+
211+
throw new Error('Unavailable due to network problems');
212+
}
213+
192214
private getTokenClaims(accessToken: string): ITokenClaims {
193215
try {
194216
return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
@@ -374,6 +396,7 @@ export class AzureActiveDirectoryService {
374396
const claims = this.getTokenClaims(json.access_token);
375397
return {
376398
expiresIn: json.expires_in,
399+
expiresAt: Date.now() + json.expires_in * 1000,
377400
accessToken: json.access_token,
378401
refreshToken: json.refresh_token,
379402
scope,

src/vs/platform/userDataSync/common/userDataAuthTokenService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
1414
private _onDidChangeToken: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
1515
readonly onDidChangeToken: Event<string | undefined> = this._onDidChangeToken.event;
1616

17+
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
18+
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
19+
1720
private _token: string | undefined;
1821

1922
constructor() {
@@ -30,4 +33,8 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
3033
this._onDidChangeToken.fire(token);
3134
}
3235
}
36+
37+
sendTokenFailed(): void {
38+
this._onTokenFailed.fire();
39+
}
3340
}

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,11 @@ export interface IUserDataAuthTokenService {
308308
_serviceBrand: undefined;
309309

310310
readonly onDidChangeToken: Event<string | undefined>;
311+
readonly onTokenFailed: Event<void>;
311312

312313
getToken(): Promise<string | undefined>;
313314
setToken(accessToken: string | undefined): Promise<void>;
315+
sendTokenFailed(): void;
314316
}
315317

316318
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');

src/vs/platform/userDataSync/common/userDataSyncIpc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class UserDataAuthTokenServiceChannel implements IServerChannel {
9696
listen(_: unknown, event: string): Event<any> {
9797
switch (event) {
9898
case 'onDidChangeToken': return this.service.onDidChangeToken;
99+
case 'onTokenFailed': return this.service.onTokenFailed;
99100
}
100101
throw new Error(`Event not found: ${event}`);
101102
}

src/vs/platform/userDataSync/common/userDataSyncStoreService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
133133
}
134134

135135
if (context.res.statusCode === 401) {
136+
this.authTokenService.sendTokenFailed();
136137
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
137138
}
138139

src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
122122
this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled());
123123
this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status)));
124124
this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources)));
125+
this._register(this.userDataAuthTokenService.onTokenFailed(_ => this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId)));
125126
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled)));
126127
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e)));
127128
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e)));

src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
1818
private _onDidChangeToken: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
1919
readonly onDidChangeToken: Event<string | undefined> = this._onDidChangeToken.event;
2020

21+
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
22+
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
23+
2124
constructor(
2225
@ISharedProcessService sharedProcessService: ISharedProcessService,
2326
) {
2427
super();
2528
this.channel = sharedProcessService.getChannel('authToken');
29+
this._register(this.channel.listen<void[]>('onTokenFailed')(_ => this.sendTokenFailed()));
2630
}
2731

2832
getToken(): Promise<string | undefined> {
@@ -32,6 +36,10 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
3236
setToken(token: string | undefined): Promise<undefined> {
3337
return this.channel.call('setToken', token);
3438
}
39+
40+
sendTokenFailed(): void {
41+
this._onTokenFailed.fire();
42+
}
3543
}
3644

3745
registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService);

0 commit comments

Comments
 (0)