Skip to content

Commit f170b56

Browse files
divyav-aotdivyav-aot
andauthored
JWT token refresh issue fixed (#580)
* VERSION updated to v7.0.0-rsbc-v0.3.8 (#75) Co-authored-by: divyav-aot <divyav-aot@github> * JWT token refresh issue fixed (#76) * formioToken refresh integrated with KeycloakService. * made updateJwtToken public static to access in offlineSubmissions. * VERSION updated to v7.0.0-rsbc-v0.3.9 * proper comments added * reverted some changes with constants.ts --------- Co-authored-by: divyav-aot <divyav-aot@github> --------- Co-authored-by: divyav-aot <divyav-aot@github>
1 parent a6fac8d commit f170b56

File tree

6 files changed

+144
-14
lines changed

6 files changed

+144
-14
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v7.0.0-rsbc-v0.3.8
1+
v7.0.0-rsbc-v0.3.9

forms-flow-rsbcservice/src/services/offlineSubmissions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ class OfflineSubmissions {
2727
*/
2828
public static async processOfflineSubmissions(): Promise<void> {
2929
try {
30+
// Access and JWT token refresh before processing the drafts and submissions
3031
await this.retryToken();
31-
32+
await KeycloakService.updateJwtToken();
33+
3234
const submissions =
3335
await OfflineFetchService.fetchAllNonActiveOfflineSubmissions();
3436
const processPromises = [

forms-flow-service/src/constants/constants.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@ export const MULTITENANCY_ENABLED =
1515

1616
//application details
1717
export const APPLICATION_NAME =
18-
(window as any).REACT_APP_APPLICATION_NAME || "roadsafety"
19-
// "formsflow.ai";
18+
(window as any)._env_?.REACT_APP_APPLICATION_NAME ?? "formsflow.ai";
2019

2120
// Used in encyrpting and decrypting the token from local storage.
22-
export const TOKEN_ENCRYPTION_KEY =
23-
(window as any).REACT_APP_TOKEN_ENCRYPTION_KEY ||
24-
"8f4a4e01b639aa73d2b5bdb6e9f2e6aef471b3dbba3d5e48e3a798fc3d6d6cbb";
21+
export const TOKEN_ENCRYPTION_KEY = (window as any)._env_
22+
?.REACT_APP_TOKEN_ENCRYPTION_KEY;
23+
24+
//default value for FORMIO_JWT_EXPIRE
25+
export const DEFAULT_FORMIO_JWT_EXPIRE = 240;
26+
27+
// Used for jwt token refresh.
28+
export const FORMIO_JWT_EXPIRE =
29+
(window as any)._env_?.REACT_APP_FORMIO_JWT_EXPIRE ??
30+
DEFAULT_FORMIO_JWT_EXPIRE;
31+
32+
export const WEB_BASE_URL =
33+
(window as any)._env_?.REACT_APP_WEB_BASE_URL ?? "http://localhost:5000";
34+
export const API = {
35+
FORMIO_ROLES: `${WEB_BASE_URL}/formio/roles`,
36+
};

forms-flow-service/src/keycloak/KeycloakService.ts

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import StorageService from "../storage/storageService";
77
import HelperServices from "../helpers/helperServices";
88
import {
99
APPLICATION_NAME,
10-
APP_BASE_ROUTE
10+
APP_BASE_ROUTE,
11+
DEFAULT_FORMIO_JWT_EXPIRE,
12+
FORMIO_JWT_EXPIRE,
1113
} from "../constants/constants";
14+
import { getUpdatedJwtToken } from "../request/jwtTokenService";
1215

13-
class KeycloakService {
16+
class KeycloakService {
1417
/**
1518
* Used to create Keycloak object
1619
*/
@@ -20,8 +23,9 @@ import {
2023
private token: string | undefined;
2124
private _tokenParsed: KeycloakTokenParsed | undefined;
2225
private timerId: any = 0;
23-
private userData: any
24-
private isInitialized: boolean = false; // Track if Keycloak is initialized
26+
private static jwtTimerId: any = 0;
27+
private userData: any;
28+
private isInitialized: boolean = false; // Track if Keycloak is initialized
2529

2630
private constructor(url: string, realm: string, clientId: string, tenantId?: string) {
2731
this._keycloakConfig = {
@@ -166,6 +170,87 @@ import {
166170
this.keycloackUpdateToken();
167171
};
168172

173+
/**
174+
* Extracts the JWT token from the response headers and saves it
175+
* to local storage for future authenticated requests.
176+
*
177+
* @param response - The HTTP response containing the JWT token in headers.
178+
*/
179+
private static saveJwtToken(response: any): void {
180+
const jwtToken = response.headers["x-jwt-token"];
181+
if (jwtToken) {
182+
StorageService.save(StorageService.User.FORMIO_TOKEN, jwtToken);
183+
}
184+
}
185+
186+
/**
187+
* Fetches a new JWT token using the `getUpdatedJwtToken()` method and
188+
* stores it using `saveJwtToken()`. This is useful for refreshing
189+
* the token periodically or on session extension.
190+
*/
191+
public static async updateJwtToken(): Promise<void> {
192+
try {
193+
const response = await getUpdatedJwtToken();
194+
if (response) {
195+
this.saveJwtToken(response);
196+
}
197+
} catch (error) {
198+
console.error("Failed to update JWT token", error);
199+
}
200+
}
201+
202+
/**
203+
* Stops the polling mechanism for refreshing the JWT token
204+
* by clearing the interval timer.
205+
*/
206+
private static clearPolling(): void {
207+
if (this.jwtTimerId) {
208+
clearInterval(this.jwtTimerId);
209+
this.jwtTimerId = 0;
210+
console.log("Polling stopped.");
211+
}
212+
}
213+
214+
215+
/**
216+
* Starts a background polling mechanism to refresh the JWT token periodically before it expires.
217+
*
218+
* - Retrieves the JWT expiration interval from the environment/config (`FORMIO_JWT_EXPIRE`).
219+
* - If the value is invalid or missing, falls back to a default (`DEFAULT_FORMIO_JWT_EXPIRE`).
220+
* - Adds a small buffer (2 seconds) to ensure the token is refreshed slightly before actual expiration.
221+
* - Clears any existing polling interval before starting a new one.
222+
* - Sets up a `setInterval` that checks for online status and asynchronously refreshes the JWT token.
223+
* - If `skipTimer` is true or the application is not `"roadsafety"`, the interval is set to 0 (executes immediately).
224+
*
225+
* @param skipTimer - If true, bypasses the default interval check (used for forced/immediate refresh).
226+
*/
227+
private static refreshJwtToken(skipTimer: boolean = false): void {
228+
const parsedValue = Number(FORMIO_JWT_EXPIRE);
229+
const jwtExpireMinutes = isNaN(parsedValue)
230+
? DEFAULT_FORMIO_JWT_EXPIRE
231+
: parsedValue;
232+
233+
// 2 seconds buffer (2000 ms)
234+
const checkInterval = jwtExpireMinutes * 60 * 1000 - 2000;
235+
236+
this.clearPolling(); // Clear previous interval before starting new
237+
238+
this.jwtTimerId = setInterval(
239+
() => {
240+
(async () => {
241+
if (!navigator.onLine) {
242+
console.debug("Offline: Skipping token refresh.");
243+
return;
244+
}
245+
await this.updateJwtToken();
246+
})(); // Async IIFE inside setInterval
247+
},
248+
!skipTimer && APPLICATION_NAME === "roadsafety" ? checkInterval : 0
249+
);
250+
console.log(
251+
`JWT polling started with interval: ${checkInterval} ms (${jwtExpireMinutes} minutes - 2 seconds)`
252+
);
253+
}
169254

170255
/**
171256
*
@@ -218,6 +303,7 @@ import {
218303
"online",
219304
() => {
220305
this.retryTokenRefresh(); // This will be executed when the 'online' event occurs
306+
KeycloakService.updateJwtToken();
221307
},
222308
{ once: false }
223309
);
@@ -235,6 +321,7 @@ import {
235321
callback(true);
236322
});
237323
this.refreshToken();
324+
KeycloakService.refreshJwtToken();
238325
}
239326
}
240327
else {
@@ -254,6 +341,7 @@ import {
254341
*/
255342
public userLogout (): void {
256343
this.isInitialized = false; // Reset initialization state
344+
KeycloakService.clearPolling();
257345
StorageService.clear();
258346
this.logout();
259347
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import RequestService from "./requestService";
2+
import { API } from "../constants/constants";
3+
4+
export const getUpdatedJwtToken = async (): Promise<any> => {
5+
try {
6+
const response = await RequestService.httpGETRequest(
7+
API.FORMIO_ROLES,
8+
null,
9+
null
10+
);
11+
if (response) {
12+
return response;
13+
} else {
14+
console.log(`No user roles found!`);
15+
return null;
16+
}
17+
} catch (error: any) {
18+
if (error?.response?.data) {
19+
console.log(error.response.data?.message);
20+
} else {
21+
console.log(`Failed to fetch user roles!`);
22+
}
23+
throw error;
24+
}
25+
};

forms-flow-service/src/storage/storageService.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ enum User {
22
AUTH_TOKEN = "AUTH_TOKEN",
33
USER_DETAILS = "USER_DETAILS",
44
USER_ROLE = "USER_ROLE",
5-
REFRESH_TOKEN = "REFRESH_TOKEN"
5+
REFRESH_TOKEN = "REFRESH_TOKEN",
6+
FORMIO_TOKEN = "formioToken",
67
}
7-
8+
89
class StorageService {
910
public static readonly User = User;
1011
/**
@@ -23,7 +24,9 @@ enum User {
2324
* @param value
2425
*/
2526
public static save(key: string, value: string): void {
26-
sessionStorage.setItem(key, value);
27+
if (key !== this.User.FORMIO_TOKEN) {
28+
sessionStorage.setItem(key, value);
29+
}
2730
localStorage.setItem(key,value);
2831
}
2932
/**

0 commit comments

Comments
 (0)