From 06563c3388b728eef3a180c5dc8b845d289442d9 Mon Sep 17 00:00:00 2001 From: DonOmalVindula Date: Tue, 9 Apr 2024 12:17:19 +0530 Subject: [PATCH 1/2] Handle 401 failures gracefully --- lib/src/helpers/authentication-helper.ts | 49 +++++++++++++++++++++++- lib/src/models/http-client.ts | 14 ++++++- lib/src/utils/spa-utils.ts | 17 +++++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/lib/src/helpers/authentication-helper.ts b/lib/src/helpers/authentication-helper.ts index 09cb1fc3..52739839 100644 --- a/lib/src/helpers/authentication-helper.ts +++ b/lib/src/helpers/authentication-helper.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2022-2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -49,6 +49,7 @@ import { HttpClientInstance, HttpError, HttpRequestConfig, + HttpRequestInterface, HttpResponse, MainThreadClientConfig, Message, @@ -65,6 +66,7 @@ export class AuthenticationHelper< protected _dataLayer: DataLayer; protected _spaHelper: SPAHelper; protected _instanceID: number; + protected _isTokenRefreshing: boolean; public constructor( authClient: AsgardeoAuthClient, @@ -74,6 +76,7 @@ export class AuthenticationHelper< this._dataLayer = this._authenticationClient.getDataLayer(); this._spaHelper = spaHelper; this._instanceID = this._authenticationClient.getInstanceID(); + this._isTokenRefreshing = false; } public enableHttpHandler(httpClient: HttpClientInstance): void { @@ -199,6 +202,34 @@ export class AuthenticationHelper< } } + protected async retryFailedRequests (failedRequest: HttpRequestInterface): Promise { + const httpClient = failedRequest.httpClient; + const requestConfig = failedRequest.requestConfig; + const isHttpHandlerEnabled = failedRequest.isHttpHandlerEnabled; + const httpErrorCallback = failedRequest.httpErrorCallback; + const httpFinishCallback = failedRequest.httpFinishCallback; + + // Wait until the token is refreshed. + await SPAUtils.until(() => !this._isTokenRefreshing); + + try { + const httpResponse = await httpClient.request(requestConfig); + + return Promise.resolve(httpResponse); + } catch (error: any) { + if (isHttpHandlerEnabled) { + if (typeof httpErrorCallback === "function") { + await httpErrorCallback(error); + } + if (typeof httpFinishCallback === "function") { + httpFinishCallback(); + } + } + + return Promise.reject(error); + } + } + public async httpRequest( httpClient: HttpClientInstance, requestConfig: HttpRequestConfig, @@ -229,13 +260,29 @@ export class AuthenticationHelper< }) .catch(async (error: HttpError) => { if (error?.response?.status === 401 || !error?.response) { + if (this._isTokenRefreshing) { + return this.retryFailedRequests({ + enableRetrievingSignOutURLFromSession, + httpClient, + httpErrorCallback, + httpFinishCallback, + isHttpHandlerEnabled, + requestConfig + }); + } + + this._isTokenRefreshing = true; // Try to refresh the token let refreshAccessTokenResponse: BasicUserInfo; try { refreshAccessTokenResponse = await this.refreshAccessToken( enableRetrievingSignOutURLFromSession ); + + this._isTokenRefreshing = false; } catch (refreshError: any) { + this._isTokenRefreshing = false; + if (isHttpHandlerEnabled) { if (typeof httpErrorCallback === "function") { await httpErrorCallback({ diff --git a/lib/src/models/http-client.ts b/lib/src/models/http-client.ts index 521542c9..d3f8f442 100755 --- a/lib/src/models/http-client.ts +++ b/lib/src/models/http-client.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020-2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -17,7 +17,8 @@ */ import { AxiosRequestConfig } from "axios"; -import { HttpError, HttpResponse } from "."; +import { HttpClientInstance, HttpError, HttpResponse } from "."; +import { SPACustomGrantConfig } from ".."; export interface HttpClient { requestStartCallback: () => void; @@ -26,6 +27,15 @@ export interface HttpClient { requestFinishCallback: () => void; } +export interface HttpRequestInterface { + httpClient: HttpClientInstance, + requestConfig: HttpRequestConfig, + isHttpHandlerEnabled?: boolean, + httpErrorCallback?: (error: HttpError) => void | Promise, + httpFinishCallback?: () => void, + enableRetrievingSignOutURLFromSession?: (config: SPACustomGrantConfig) => void +} + export interface HttpRequestConfig extends AxiosRequestConfig { attachToken?: boolean; shouldEncodeToFormData?: boolean; diff --git a/lib/src/utils/spa-utils.ts b/lib/src/utils/spa-utils.ts index 754fb3c2..c0b8eba7 100644 --- a/lib/src/utils/spa-utils.ts +++ b/lib/src/utils/spa-utils.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020-2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -215,4 +215,19 @@ export class SPAUtils { await new Promise((resolve) => setTimeout(resolve, timeToWait * 1000)); } + + /** + * Waits for a condition before executing the rest of the code in non-blocking manner. + * + * @param condition {() => boolean} - Condition to be checked. + * @param timeout {number} - Time in miliseconds. + */ + public static until = ( + condition: () => boolean, + timeout: number = 500 + ) => { + const poll = (done) => (condition() ? done() : setTimeout(() => poll(done), timeout)); + + return new Promise(poll); + }; } From 0223d6a985cbf9da13c7e1579fbdf8cd2c1bc916 Mon Sep 17 00:00:00 2001 From: DonOmalVindula Date: Tue, 9 Apr 2024 14:05:00 +0530 Subject: [PATCH 2/2] Add config to disable token refresh logic --- lib/src/client.ts | 1 + lib/src/helpers/spa-helper.ts | 7 +++++++ lib/src/models/client-config.ts | 1 + 3 files changed, 9 insertions(+) diff --git a/lib/src/client.ts b/lib/src/client.ts index 8a60a584..a074c45c 100755 --- a/lib/src/client.ts +++ b/lib/src/client.ts @@ -54,6 +54,7 @@ const DefaultConfig: Partial> = { checkSessionInterval: 3, clientHost: origin, enableOIDCSessionManagement: false, + periodicTokenRefresh: false, sessionRefreshInterval: 300, storage: Storage.SessionStorage }; diff --git a/lib/src/helpers/spa-helper.ts b/lib/src/helpers/spa-helper.ts index 6199e334..72b5de54 100644 --- a/lib/src/helpers/spa-helper.ts +++ b/lib/src/helpers/spa-helper.ts @@ -33,6 +33,13 @@ export class SPAHelper MainThreadClientConfig | WebWorkerClientConfig > ): Promise { + const shouldRefreshAutomatically: boolean = (await this._dataLayer.getConfigData())?.periodicTokenRefresh ?? + false; + + if (!shouldRefreshAutomatically) { + return; + } + const sessionData = await this._dataLayer.getSessionData(); if (sessionData.refresh_token) { // Refresh 10 seconds before the expiry time diff --git a/lib/src/models/client-config.ts b/lib/src/models/client-config.ts index 1278307e..2047e373 100644 --- a/lib/src/models/client-config.ts +++ b/lib/src/models/client-config.ts @@ -31,6 +31,7 @@ export interface SPAConfig { sessionRefreshInterval?: number; resourceServerURLs?: string[]; authParams?: Record + periodicTokenRefresh?: boolean; } /**