Skip to content
This repository was archived by the owner on Apr 3, 2023. It is now read-only.

Commit b77c650

Browse files
authored
Merge pull request #6 from luneo7/main
Fix concurrency issues that happens when setting internal promise reference in updateToken method
2 parents fee6302 + d37c3c0 commit b77c650

File tree

3 files changed

+106
-70
lines changed

3 files changed

+106
-70
lines changed

src/client.ts

+88-70
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type {
2121
OAuthResponse,
2222
} from './types';
2323

24+
import Deferred from './utils/deferred';
25+
2426
import {
2527
decodeToken,
2628
getRealmUrl,
@@ -100,7 +102,7 @@ export class KeycloakClient implements KeycloakInstance {
100102

101103
private logWarn = this.createLogger(console.warn);
102104

103-
private refreshTokenPromise?: Promise<boolean>;
105+
private refreshQueue: Array<Deferred<boolean>> = [];
104106

105107
private useNonce?: boolean;
106108

@@ -420,6 +422,85 @@ export class KeycloakClient implements KeycloakInstance {
420422
return expiresIn < 0;
421423
}
422424

425+
private async runUpdateToken(
426+
minValidity: number,
427+
deffered: Deferred<boolean>
428+
) {
429+
let shouldRefreshToken: boolean = false;
430+
431+
if (minValidity === -1) {
432+
shouldRefreshToken = true;
433+
this.logInfo('[KEYCLOAK] Refreshing token: forced refresh');
434+
} else if (!this.tokenParsed || this.isTokenExpired(minValidity)) {
435+
shouldRefreshToken = true;
436+
this.logInfo('[KEYCLOAK] Refreshing token: token expired');
437+
}
438+
439+
if (!shouldRefreshToken) {
440+
deffered.resolve(false);
441+
} else {
442+
const tokenUrl = this.endpoints!.token();
443+
444+
const params = new Map<string, string>();
445+
params.set('client_id', this.clientId!);
446+
params.set('grant_type', 'refresh_token');
447+
params.set('refresh_token', this.refreshToken!);
448+
449+
this.refreshQueue.push(deffered);
450+
451+
if (this.refreshQueue.length === 1) {
452+
let timeLocal = new Date().getTime();
453+
454+
try {
455+
const tokenResponse = await this.adapter!.refreshTokens(
456+
tokenUrl,
457+
formatQuerystringParameters(params)
458+
);
459+
460+
if (tokenResponse.error) {
461+
this.clearToken();
462+
throw new Error(tokenResponse.error);
463+
} else {
464+
this.logInfo('[KEYCLOAK] Token refreshed');
465+
466+
timeLocal = (timeLocal + new Date().getTime()) / 2;
467+
468+
this.setToken(
469+
tokenResponse.access_token,
470+
tokenResponse.refresh_token,
471+
tokenResponse.id_token,
472+
timeLocal
473+
);
474+
475+
// Notify onAuthRefreshSuccess event handler if set
476+
this.onAuthRefreshSuccess && this.onAuthRefreshSuccess();
477+
478+
for (
479+
let p = this.refreshQueue.pop();
480+
p != null;
481+
p = this.refreshQueue.pop()
482+
) {
483+
p.resolve(true);
484+
}
485+
}
486+
} catch (err) {
487+
this.logWarn('[KEYCLOAK] Failed to refresh token');
488+
489+
// Notify onAuthRefreshError event handler if set
490+
this.onAuthRefreshError && this.onAuthRefreshError();
491+
492+
for (
493+
let p = this.refreshQueue.pop();
494+
p != null;
495+
p = this.refreshQueue.pop()
496+
) {
497+
p.reject(true);
498+
}
499+
}
500+
}
501+
}
502+
}
503+
423504
/**
424505
* If the token expires within `minValidity` seconds, the token is refreshed.
425506
* If the session status iframe is enabled, the session status is also
@@ -439,79 +520,16 @@ export class KeycloakClient implements KeycloakInstance {
439520
* });
440521
*/
441522
public async updateToken(minValidity: number = 5): Promise<boolean> {
442-
if (!this.refreshToken) {
443-
throw new Error('missing refreshToken');
444-
}
523+
const deffered = new Deferred<boolean>();
445524

446-
if (this.refreshTokenPromise) {
447-
return this.refreshTokenPromise;
525+
if (!this.refreshToken) {
526+
deffered.reject('missing refreshToken');
527+
return deffered.getPromise();
448528
}
449529

450-
this.refreshTokenPromise = new Promise<boolean>(async (resolve, reject) => {
451-
let shouldRefreshToken: boolean = false;
452-
453-
if (minValidity === -1) {
454-
shouldRefreshToken = true;
455-
this.logInfo('[KEYCLOAK] Refreshing token: forced refresh');
456-
} else if (!this.tokenParsed || this.isTokenExpired(minValidity)) {
457-
shouldRefreshToken = true;
458-
this.logInfo('[KEYCLOAK] Refreshing token: token expired');
459-
}
460-
461-
if (!shouldRefreshToken) {
462-
resolve(false);
463-
464-
this.refreshTokenPromise = undefined;
465-
466-
return;
467-
}
468-
469-
const tokenUrl = this.endpoints!.token();
470-
471-
const params = new Map<string, string>();
472-
params.set('client_id', this.clientId!);
473-
params.set('grant_type', 'refresh_token');
474-
params.set('refresh_token', this.refreshToken!);
475-
476-
let timeLocal = new Date().getTime();
477-
478-
try {
479-
const tokenResponse = await this.adapter!.refreshTokens(
480-
tokenUrl,
481-
formatQuerystringParameters(params)
482-
);
483-
484-
this.logInfo('[KEYCLOAK] Token refreshed');
485-
486-
timeLocal = (timeLocal + new Date().getTime()) / 2;
487-
488-
this.setToken(
489-
tokenResponse.access_token,
490-
tokenResponse.refresh_token,
491-
tokenResponse.id_token,
492-
timeLocal
493-
);
494-
495-
// Notify onAuthRefreshSuccess event handler if set
496-
this.onAuthRefreshSuccess && this.onAuthRefreshSuccess();
497-
498-
resolve(true);
499-
} catch (err) {
500-
this.logWarn('[KEYCLOAK] Failed to refresh token');
501-
502-
// Clear tokens
503-
this.clearToken();
504-
505-
// Notify onAuthRefreshError event handler if set
506-
this.onAuthRefreshError && this.onAuthRefreshError();
507-
508-
reject();
509-
}
510-
511-
this.refreshTokenPromise = undefined;
512-
});
530+
this.runUpdateToken(minValidity, deffered);
513531

514-
return this.refreshTokenPromise;
532+
return deffered.getPromise();
515533
}
516534

517535
/**

src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ export interface FetchTokenResponse {
254254
id_token: string | null;
255255

256256
refresh_token: string | null;
257+
258+
error: string | null;
257259
}
258260

259261
export interface KeycloakAdapterConstructor {

src/utils/deferred.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default class Deferred<T> {
2+
private promise: Promise<T>;
3+
public resolve!: (value: T | PromiseLike<T>) => void;
4+
public reject!: (reason?: any) => void;
5+
6+
constructor() {
7+
this.promise = new Promise<T>((resolve, reject) => {
8+
this.reject = reject;
9+
this.resolve = resolve;
10+
});
11+
}
12+
13+
public getPromise(): Promise<T> {
14+
return this.promise;
15+
}
16+
}

0 commit comments

Comments
 (0)