From eaab2cea8996da90086d9b83a74b43724cb2ce1b Mon Sep 17 00:00:00 2001 From: Furqan-14 Date: Mon, 16 Feb 2026 15:30:33 +0530 Subject: [PATCH 1/2] fix: security fix --- src/application/authentication/AuthApi.ts | 2 +- .../authentication/Components/SignInForm.tsx | 4 +--- src/context/AuthContext.tsx | 4 ++++ src/lib/apiClient.ts | 12 +++++++----- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/application/authentication/AuthApi.ts b/src/application/authentication/AuthApi.ts index b458210..be0076e 100644 --- a/src/application/authentication/AuthApi.ts +++ b/src/application/authentication/AuthApi.ts @@ -31,7 +31,7 @@ export const authApi = { payload, { ...withTenantHeader(options?.tenantId), successMessage: "Account created", errorMessage: "Failed to create account" } ), - logout: () => apiClient.post("/api/auth/logout", { successMessage: "Signed out", errorMessage: "Failed to sign out" }), + logout: () => apiClient.post("/api/auth/logout", null, { successMessage: "Signed out", errorMessage: "Failed to sign out" }), me: () => apiClient.get("/api/auth/me"), resetPassword: (oldPassword: string, newPassword: string) => apiClient.post<{ message: string }>("/api/auth/reset-password", { diff --git a/src/application/authentication/Components/SignInForm.tsx b/src/application/authentication/Components/SignInForm.tsx index c7dac0c..d327b17 100644 --- a/src/application/authentication/Components/SignInForm.tsx +++ b/src/application/authentication/Components/SignInForm.tsx @@ -14,7 +14,7 @@ export default function SignInForm() { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(""); - const { login } = useAuth(); // Retrieve login function from context + const { login } = useAuth(); const handleSignIn = async (event: React.FormEvent) => { event.preventDefault(); @@ -27,10 +27,8 @@ export default function SignInForm() { }; try { - // Use the context login function to ensure state is updated await login(payload, isChecked); - // Redirect to the dashboard after successful signin. navigate("/dashboard"); } catch (error) { const message = diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 817bd65..3ab930e 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -70,6 +70,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ }; const logout = async () => { + try { + await authApi.logout(); + } catch { + } clearAuthCookies(); setUser(null); window.location.href = "/signin"; diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index eada2e6..680e8ea 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1,16 +1,13 @@ import { toast } from "react-toastify"; import { API_BASE_URL } from "../constant"; -import { AUTH_COOKIE_KEYS, clearAuthCookies, getCookie } from "./authCookies"; +import { AUTH_COOKIE_KEYS, clearAuthCookies, getCookie, setAuthCookies } from "./authCookies"; import { isTokenExpired } from "./jwt"; type ApiRequestOptions = Omit & { body?: unknown; toast?: boolean; - /** Optional custom success message to show in a toast */ successMessage?: string; - /** Optional custom error message to show in a toast */ errorMessage?: string; - /** If true, silences all toast notifications for this request */ silent?: boolean; }; @@ -39,7 +36,9 @@ const refreshAccessToken = async () => { }); if (!res.ok) throw new Error("Refresh failed"); - return res.json(); + const data = await res.json(); + setAuthCookies(data); + return data; }; const request = async ( @@ -72,6 +71,9 @@ const request = async ( await new Promise((resolve) => refreshQueue.push(resolve)); } } catch { + isRefreshing = false; + refreshQueue.forEach((cb) => cb()); + refreshQueue = []; hardLogout(); throw new Error("Session expired"); } From f0114314a6a88ad52b439ae1f35a559daba3e16f Mon Sep 17 00:00:00 2001 From: Furqan-14 Date: Tue, 17 Feb 2026 11:55:12 +0530 Subject: [PATCH 2/2] fix: stronger handling of data --- src/application/authentication/AuthApi.ts | 8 +- src/application/authentication/AuthTypes.ts | 1 + src/application/dashboard/Dashboard.tsx | 22 ++-- .../modules/admin/AdminModuleApi.ts | 100 ++++++------------ .../modules/admin/hooks/useModuleApi.ts | 4 +- src/application/modules/user/UserModuleApi.ts | 21 +--- src/context/AuthContext.tsx | 2 +- src/lib/apiClient.ts | 70 ++++++------ src/lib/authCookies.ts | 31 ++---- src/lib/jwt.ts | 9 -- 10 files changed, 105 insertions(+), 163 deletions(-) delete mode 100644 src/lib/jwt.ts diff --git a/src/application/authentication/AuthApi.ts b/src/application/authentication/AuthApi.ts index be0076e..3d6560f 100644 --- a/src/application/authentication/AuthApi.ts +++ b/src/application/authentication/AuthApi.ts @@ -69,8 +69,6 @@ export const authApi = { silent: true, }), - refresh: (refreshToken: string) => - apiClient.post("/api/auth/refresh", { - refresh_token: refreshToken, - }), -}; + refresh: () => + apiClient.post("/api/auth/refresh", {}, { silent: true }), +}; \ No newline at end of file diff --git a/src/application/authentication/AuthTypes.ts b/src/application/authentication/AuthTypes.ts index 75fcbf3..11e9a94 100644 --- a/src/application/authentication/AuthTypes.ts +++ b/src/application/authentication/AuthTypes.ts @@ -30,6 +30,7 @@ export type AuthUser = { export type SigninRequest = { email: string; password: string; + remember_me?: boolean; }; export type SignupRequest = { diff --git a/src/application/dashboard/Dashboard.tsx b/src/application/dashboard/Dashboard.tsx index 109ef03..15c1e97 100644 --- a/src/application/dashboard/Dashboard.tsx +++ b/src/application/dashboard/Dashboard.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useMemo } from 'react'; -import axios from 'axios'; import { ExternalLink, Box, Search, Rocket } from 'lucide-react'; import { moduleApi, type Module } from '../modules/user/UserModuleApi'; import { useAuth } from '../../context/AuthContext'; @@ -31,14 +30,25 @@ const Dashboard = () => { const handleModuleClick = async (moduleId: string) => { try { const response = await moduleApi.launchModule(moduleId); - - await axios.post(response.target_url, response.payload, { - headers: response.headers, - withCredentials: true + + await fetch(response.target_url, { + method: "POST", + headers: { "Content-Type": "application/json", ...response.headers }, + body: JSON.stringify(response.payload), + credentials: "include", }); if (response.redirect_url) { - window.location.href = response.redirect_url; + try { + const url = new URL(response.redirect_url); + if (url.protocol !== "https:" && url.protocol !== "http:") { + throw new Error("Unsafe protocol"); + } + window.location.href = response.redirect_url; + } catch { + console.error("Invalid redirect URL from SSO"); + alert("Failed to launch module. Please try again."); + } } } catch (error) { alert("Failed to launch module. Please try again."); diff --git a/src/application/modules/admin/AdminModuleApi.ts b/src/application/modules/admin/AdminModuleApi.ts index a22efd7..16cf2e2 100644 --- a/src/application/modules/admin/AdminModuleApi.ts +++ b/src/application/modules/admin/AdminModuleApi.ts @@ -1,5 +1,4 @@ -import axios from 'axios'; -import { API_BASE_URL } from '../../../constant'; +import { apiClient } from '../../../lib/apiClient'; import type { Module, ModuleCreate, @@ -13,85 +12,52 @@ import type { TenantModuleUpdate } from './AdminModuleTypes'; -const api = axios.create({ - baseURL: API_BASE_URL, - withCredentials: true, -}); - export const adminModuleApi = { - listModules: async (): Promise => { - const response = await api.get('/api/admin/modules/'); - return response.data; - }, + listModules: (): Promise => + apiClient.get('/api/admin/modules/', { toast: false }), - createModule: async (data: ModuleCreate): Promise => { - const response = await api.post('/api/admin/modules/', data); - return response.data; - }, + createModule: (data: ModuleCreate): Promise => + apiClient.post('/api/admin/modules/', data, { toast: false }), - getModule: async (moduleId: string): Promise => { - const response = await api.get(`/api/admin/modules/${moduleId}`); - return response.data; - }, + getModule: (moduleId: string): Promise => + apiClient.get(`/api/admin/modules/${moduleId}`, { toast: false }), - updateModule: async (moduleId: string, data: ModuleUpdate): Promise => { - const response = await api.put(`/api/admin/modules/${moduleId}`, data); - return response.data; - }, + updateModule: (moduleId: string, data: ModuleUpdate): Promise => + apiClient.put(`/api/admin/modules/${moduleId}`, data, { toast: false }), - deleteModule: async (moduleId: string): Promise => { - await api.delete(`/api/admin/modules/${moduleId}`); - }, + deleteModule: (moduleId: string): Promise => + apiClient.delete(`/api/admin/modules/${moduleId}`, { toast: false }), - listEnvironments: async (moduleId: string): Promise => { - const response = await api.get(`/api/admin/modules/${moduleId}/environments`); - return response.data; - }, + listEnvironments: (moduleId: string): Promise => + apiClient.get(`/api/admin/modules/${moduleId}/environments`, { toast: false }), - createEnvironment: async (moduleId: string, data: EnvironmentCreate): Promise => { - const response = await api.post(`/api/admin/modules/${moduleId}/environments`, data); - return response.data; - }, + createEnvironment: (moduleId: string, data: EnvironmentCreate): Promise => + apiClient.post(`/api/admin/modules/${moduleId}/environments`, data, { toast: false }), - updateEnvironment: async (moduleId: string, envId: string, data: EnvironmentUpdate): Promise => { - const response = await api.put(`/api/admin/modules/${moduleId}/environments/${envId}`, data); - return response.data; - }, + updateEnvironment: (moduleId: string, envId: string, data: EnvironmentUpdate): Promise => + apiClient.put(`/api/admin/modules/${moduleId}/environments/${envId}`, data, { toast: false }), - setDefaultEnvironment: async (moduleId: string, envId: string): Promise => { - await api.patch(`/api/admin/modules/${moduleId}/environments/${envId}/default`); - }, + setDefaultEnvironment: (moduleId: string, envId: string): Promise => + apiClient.patch(`/api/admin/modules/${moduleId}/environments/${envId}/default`, undefined, { toast: false }), - deleteEnvironment: async (moduleId: string, envId: string): Promise => { - await api.delete(`/api/admin/modules/${moduleId}/environments/${envId}`); - }, + deleteEnvironment: (moduleId: string, envId: string): Promise => + apiClient.delete(`/api/admin/modules/${moduleId}/environments/${envId}`, { toast: false }), - getModulePermissions: async (moduleId: string): Promise => { - const response = await api.get(`/api/admin/modules/${moduleId}/permissions`); - return response.data; - }, + getModulePermissions: (moduleId: string): Promise => + apiClient.get(`/api/admin/modules/${moduleId}/permissions`, { toast: false }), - syncModulePermissions: async (moduleId: string): Promise<{ message: string; synced_count: number }> => { - const response = await api.post(`/api/admin/modules/${moduleId}/permissions/sync`); - return response.data; - }, + syncModulePermissions: (moduleId: string): Promise<{ message: string; synced_count: number }> => + apiClient.post<{ message: string; synced_count: number }>(`/api/admin/modules/${moduleId}/permissions/sync`, undefined, { toast: false }), - listTenantModules: async (tenantId: string): Promise => { - const response = await api.get(`/api/admin/tenants/${tenantId}/modules`); - return response.data; - }, + listTenantModules: (tenantId: string): Promise => + apiClient.get(`/api/admin/tenants/${tenantId}/modules`, { toast: false }), - assignModuleToTenant: async (tenantId: string, data: TenantModuleCreate): Promise => { - const response = await api.post(`/api/admin/tenants/${tenantId}/modules`, data); - return response.data; - }, + assignModuleToTenant: (tenantId: string, data: TenantModuleCreate): Promise => + apiClient.post(`/api/admin/tenants/${tenantId}/modules`, data, { toast: false }), - updateTenantModule: async (tenantId: string, tenantModuleId: string, data: TenantModuleUpdate): Promise => { - const response = await api.put(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, data); - return response.data; - }, + updateTenantModule: (tenantId: string, tenantModuleId: string, data: TenantModuleUpdate): Promise => + apiClient.put(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, data, { toast: false }), - removeTenantModule: async (tenantId: string, tenantModuleId: string): Promise => { - await api.delete(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`); - }, + removeTenantModule: (tenantId: string, tenantModuleId: string): Promise => + apiClient.delete(`/api/admin/tenants/${tenantId}/modules/${tenantModuleId}`, { toast: false }), }; \ No newline at end of file diff --git a/src/application/modules/admin/hooks/useModuleApi.ts b/src/application/modules/admin/hooks/useModuleApi.ts index 415d7a8..8e680c6 100644 --- a/src/application/modules/admin/hooks/useModuleApi.ts +++ b/src/application/modules/admin/hooks/useModuleApi.ts @@ -40,9 +40,9 @@ interface UseModuleApiResult { export const useModuleApi = (): UseModuleApiResult => { const [loading, setLoading] = useState(false); - const handleError = (error: any, action: string) => { + const handleError = (error: unknown, action: string) => { console.error(`Failed to ${action}`, error); - const message = error.response?.data?.detail || `Failed to ${action}`; + const message = error instanceof Error ? error.message : `Failed to ${action}`; toast.error(message); }; diff --git a/src/application/modules/user/UserModuleApi.ts b/src/application/modules/user/UserModuleApi.ts index 257e465..5d10473 100644 --- a/src/application/modules/user/UserModuleApi.ts +++ b/src/application/modules/user/UserModuleApi.ts @@ -1,5 +1,4 @@ -import axios from "axios"; -import { API_BASE_URL } from "../../../constant"; +import { apiClient } from "../../../lib/apiClient"; export interface Module { module_id: string; @@ -19,19 +18,9 @@ export interface LaunchResponse { } export const moduleApi = { - getAvailableModules: async (): Promise => { - const response = await axios.get(`${API_BASE_URL}/api/modules/available`, { - withCredentials: true, - }); - return response.data; - }, + getAvailableModules: (): Promise => + apiClient.get("/api/modules/available", { toast: false }), - launchModule: async (moduleId: string): Promise => { - const response = await axios.post( - `${API_BASE_URL}/api/sso/initiate`, - { module_id: moduleId }, - { withCredentials: true } - ); - return response.data; - }, + launchModule: (moduleId: string): Promise => + apiClient.post("/api/sso/initiate", { module_id: moduleId }, { toast: false }), }; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 3ab930e..5aff237 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -61,7 +61,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ const login = async (credentials: SigninRequest, rememberMe = false) => { setIsLoading(true); try { - const response = await authApi.signin(credentials); + const response = await authApi.signin({ ...credentials, remember_me: rememberMe }); setAuthCookies(response, rememberMe); setUser(response.user); } finally { diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 680e8ea..11a44d7 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1,7 +1,6 @@ import { toast } from "react-toastify"; import { API_BASE_URL } from "../constant"; -import { AUTH_COOKIE_KEYS, clearAuthCookies, getCookie, setAuthCookies } from "./authCookies"; -import { isTokenExpired } from "./jwt"; +import { clearAuthCookies, refreshLoggedInFlag } from "./authCookies"; type ApiRequestOptions = Omit & { body?: unknown; @@ -25,25 +24,39 @@ const hardLogout = () => { }; const refreshAccessToken = async () => { - const refreshToken = getCookie(AUTH_COOKIE_KEYS.refresh); - if (!refreshToken) throw new Error("No refresh token"); - const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refresh_token: refreshToken }), + body: JSON.stringify({}), }); if (!res.ok) throw new Error("Refresh failed"); - const data = await res.json(); - setAuthCookies(data); - return data; +}; + +const waitForRefresh = (): Promise => + new Promise((resolve) => refreshQueue.push(resolve)); + +const doRefresh = async () => { + if (isRefreshing) { + await waitForRefresh(); + return; + } + isRefreshing = true; + try { + await refreshAccessToken(); + refreshLoggedInFlag(); + } finally { + isRefreshing = false; + refreshQueue.forEach((cb) => cb()); + refreshQueue = []; + } }; const request = async ( path: string, - options: ApiRequestOptions = {} + options: ApiRequestOptions = {}, + _isRetry = false ): Promise => { const { body, @@ -57,28 +70,6 @@ const request = async ( const showToast = shouldToast && !silent; - const accessToken = getCookie(AUTH_COOKIE_KEYS.access); - - if (accessToken && isTokenExpired(accessToken)) { - try { - if (!isRefreshing) { - isRefreshing = true; - await refreshAccessToken(); - isRefreshing = false; - refreshQueue.forEach((cb) => cb()); - refreshQueue = []; - } else { - await new Promise((resolve) => refreshQueue.push(resolve)); - } - } catch { - isRefreshing = false; - refreshQueue.forEach((cb) => cb()); - refreshQueue = []; - hardLogout(); - throw new Error("Session expired"); - } - } - const requestHeaders = new Headers(headers); let requestBody: BodyInit | undefined; @@ -103,8 +94,16 @@ const request = async ( body: requestBody, }); - // Global 401 handler - ignore for signin requests as they just mean invalid credentials if (response.status === 401 && !url.includes("/api/auth/signin")) { + if (!_isRetry) { + try { + await doRefresh(); + return request(path, options, true); + } catch { + hardLogout(); + throw new Error("Session expired"); + } + } hardLogout(); throw new Error("Unauthorized"); } @@ -118,20 +117,17 @@ const request = async ( : await response.text(); if (!response.ok) { - // Extract the specific error message from the backend response const backendError = typeof data === "object" && data !== null ? (data as any).detail || (data as any).message : undefined; - // Prioritize backend error, then the custom errorMessage option, then a default const finalErrorMessage = backendError || errorMessage || "Request failed"; if (showToast) { toast.error(finalErrorMessage, API_TOAST_OPTIONS); } - // Throw the actual message so the caller (e.g., SignInForm) can display it throw new Error(finalErrorMessage); } @@ -170,4 +166,4 @@ export const apiClient = { delete: (path: string, options?: ApiRequestOptions) => request(path, { ...options, method: "DELETE" }), -}; +}; \ No newline at end of file diff --git a/src/lib/authCookies.ts b/src/lib/authCookies.ts index 8786cf5..2d9a1f2 100644 --- a/src/lib/authCookies.ts +++ b/src/lib/authCookies.ts @@ -1,12 +1,5 @@ -import { type TokenResponse } from "../application/authentication/AuthTypes"; - -export const AUTH_COOKIE_KEYS = { - access: "access_token", - refresh: "refresh_token", -}; - -const ACCESS_MAX_AGE_SECONDS = 60 * 60; // 1 hour -const REFRESH_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7 days +const LOGGED_IN_KEY = "logged_in"; +const LOGGED_IN_MAX_AGE_SECONDS = 60 * 15; const buildCookieOptions = (maxAgeSeconds?: number) => { const options = ["path=/", "samesite=lax"]; @@ -41,19 +34,17 @@ export const getCookie = (name: string) => { return decodeURIComponent(rawValue); }; -export const setAuthCookies = (tokens: TokenResponse, rememberMe = false) => { - // Store tokens in cookies so they can be reused across page refreshes. - const accessMaxAge = rememberMe ? ACCESS_MAX_AGE_SECONDS : undefined; - const refreshMaxAge = rememberMe ? REFRESH_MAX_AGE_SECONDS : undefined; - - setCookie(AUTH_COOKIE_KEYS.access, tokens.access_token, accessMaxAge); - setCookie(AUTH_COOKIE_KEYS.refresh, tokens.refresh_token, refreshMaxAge); +export const setAuthCookies = (_tokens: unknown, rememberMe = false) => { + const maxAge = rememberMe ? LOGGED_IN_MAX_AGE_SECONDS : undefined; + setCookie(LOGGED_IN_KEY, "1", maxAge); }; export const clearAuthCookies = () => { - // Clear both cookies on logout or auth failure. - deleteCookie(AUTH_COOKIE_KEYS.access); - deleteCookie(AUTH_COOKIE_KEYS.refresh); + deleteCookie(LOGGED_IN_KEY); }; -export const getAccessToken = () => getCookie(AUTH_COOKIE_KEYS.access); +export const refreshLoggedInFlag = () => { + setCookie(LOGGED_IN_KEY, "1", LOGGED_IN_MAX_AGE_SECONDS); +}; + +export const getAccessToken = () => getCookie(LOGGED_IN_KEY); \ No newline at end of file diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts deleted file mode 100644 index a939596..0000000 --- a/src/lib/jwt.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const isTokenExpired = (token: string): boolean => { - try { - const payload = JSON.parse(atob(token.split(".")[1])); - const now = Math.floor(Date.now() / 1000); - return payload.exp < now; - } catch { - return true; - } -};