From 68d1db7e74c009d1b4280cd5c97744d3b781280d Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:24:59 +0530 Subject: [PATCH] feat: add auth refresh token flow if token expires (#5490) --- .../src/helpers/backend/GQLClient.ts | 15 ++++-- .../src/helpers/isValidUser.ts | 54 +++++++++++++------ .../hoppscotch-common/src/platform/auth.ts | 6 +++ .../src/platform/auth.ts | 4 ++ .../src/platform/auth/desktop/index.ts | 5 ++ .../src/platform/auth/web/index.ts | 4 ++ 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/packages/hoppscotch-common/src/helpers/backend/GQLClient.ts b/packages/hoppscotch-common/src/helpers/backend/GQLClient.ts index 42019b56..20ba2fc0 100644 --- a/packages/hoppscotch-common/src/helpers/backend/GQLClient.ts +++ b/packages/hoppscotch-common/src/helpers/backend/GQLClient.ts @@ -96,11 +96,20 @@ const createHoppClient = () => { willAuthError() { return platform.auth.willBackendHaveAuthError() }, - didAuthError() { - return false + didAuthError(error) { + // Check for specific error patterns that indicate expired token + return error.graphQLErrors.some( + (e) => + e.message.includes("auth/fail") || + e.message.includes("jwt expired") || + e.extensions?.code === "UNAUTHENTICATED" + ) }, async refreshAuth() { - // TODO + const refresh = platform.auth.refreshAuthToken + // should we logout if refreshAuthToken is not defined? + if (!refresh) return + await refresh() }, } }), diff --git a/packages/hoppscotch-common/src/helpers/isValidUser.ts b/packages/hoppscotch-common/src/helpers/isValidUser.ts index 3289133d..bedf1e51 100644 --- a/packages/hoppscotch-common/src/helpers/isValidUser.ts +++ b/packages/hoppscotch-common/src/helpers/isValidUser.ts @@ -7,8 +7,28 @@ export type ValidUserResponse = { export const SESSION_EXPIRED = "Session expired. Please log in again." +/** + * Attempts to refresh the authentication token + * @returns Promise resolving to a ValidUserResponse with the result + */ +const attemptTokenRefresh = async (): Promise => { + if (!platform.auth.refreshAuthToken) + return { valid: false, error: SESSION_EXPIRED } + + try { + const refreshSuccessful = await platform.auth.refreshAuthToken() + return { + valid: refreshSuccessful, + error: refreshSuccessful ? "" : SESSION_EXPIRED, + } + } catch { + return { valid: false, error: SESSION_EXPIRED } + } +} + /** * Validates user authentication and token validity by making an API call. + * Refreshes tokens if they are expired. * * This function is kept separate from `handleTokenValidation()` to enable different use cases: * - Silent validation for conditional UI states (e.g., disabling components on token expiration) @@ -23,22 +43,26 @@ export const SESSION_EXPIRED = "Session expired. Please log in again." export const isValidUser = async (): Promise => { const user = platform.auth.getCurrentUser() - if (user) { - try { - // If the platform provides a method to verify auth tokens, use it else assume tokens are valid (for central instance where firebase handles it) - const hasValidTokens = platform.auth.verifyAuthTokens - ? await platform.auth.verifyAuthTokens() - : true + // If no user is logged in, consider it valid (allows public actions) + if (!user) return { valid: true, error: "" } - return { - valid: hasValidTokens, - error: hasValidTokens ? "" : SESSION_EXPIRED, + try { + // If the platform provides a method to verify auth tokens, use it + if (platform.auth.verifyAuthTokens) { + const hasValidTokens = await platform.auth.verifyAuthTokens() + + if (hasValidTokens) { + return { valid: true, error: "" } } - } catch (error) { - return { valid: false, error: SESSION_EXPIRED } - } - } - // allow user to perform actions without being logged in - return { valid: true, error: "" } + // Try token refresh if verification failed + return attemptTokenRefresh() + } + + // For platforms without token verification capability + return { valid: true, error: "" } + } catch (error) { + // Handle errors from token verification + return attemptTokenRefresh() + } } diff --git a/packages/hoppscotch-common/src/platform/auth.ts b/packages/hoppscotch-common/src/platform/auth.ts index 231c6e2c..152ba63f 100644 --- a/packages/hoppscotch-common/src/platform/auth.ts +++ b/packages/hoppscotch-common/src/platform/auth.ts @@ -281,4 +281,10 @@ export type AuthPlatformDef = { * @returns True if tokens are valid, false otherwise */ verifyAuthTokens?: () => Promise + + /** Refreshes the authentication tokens for the current user + * For self-hosted, this should refresh the tokens with the backend + * @returns True if tokens were refreshed successfully, false otherwise + */ + refreshAuthToken?: () => Promise } diff --git a/packages/hoppscotch-selfhost-desktop/src/platform/auth.ts b/packages/hoppscotch-selfhost-desktop/src/platform/auth.ts index b76d0381..f215bb0b 100644 --- a/packages/hoppscotch-selfhost-desktop/src/platform/auth.ts +++ b/packages/hoppscotch-selfhost-desktop/src/platform/auth.ts @@ -395,6 +395,10 @@ export const def: AuthPlatformDef = { }) }, + async refreshAuthToken() { + return refreshToken() + }, + /** * Verifies if the current user's authentication tokens are valid * @returns True if tokens are valid, false otherwise diff --git a/packages/hoppscotch-selfhost-web/src/platform/auth/desktop/index.ts b/packages/hoppscotch-selfhost-web/src/platform/auth/desktop/index.ts index 5315ae5c..f375ad93 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/auth/desktop/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/auth/desktop/index.ts @@ -527,6 +527,11 @@ export const def: AuthPlatformDef = { }) }, + async refreshAuthToken() { + const refreshed = await refreshToken() + return refreshed ?? false + }, + /** * Verifies if the current user's authentication tokens are valid * @returns True if tokens are valid, false otherwise diff --git a/packages/hoppscotch-selfhost-web/src/platform/auth/web/index.ts b/packages/hoppscotch-selfhost-web/src/platform/auth/web/index.ts index a3174f58..d36b986e 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/auth/web/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/auth/web/index.ts @@ -353,6 +353,10 @@ export const def: AuthPlatformDef = { }) }, + async refreshAuthToken() { + return refreshToken() + }, + async processMagicLink() { if (this.isSignInWithEmailLink(window.location.href)) { const deviceIdentifier =