From c0f49f60f702253236c58067d67dce8185590145 Mon Sep 17 00:00:00 2001 From: Hari-egov Date: Wed, 3 Dec 2025 17:06:59 +0530 Subject: [PATCH] Cookies Based Auth In Mono-UI --- web/rainmaker/CHANGELOG.md | 111 ++++++++++++++++++ .../specs/utils/localStorageUtils/index.js | 47 +++++++- .../src/ui-utils/api.js | 33 ++++-- .../egov-ui-kit-dev/src/hocs/withData.js | 13 +- .../egov-ui-kit-dev/src/redux/auth/actions.js | 69 +++++------ .../src/redux/auth/middleware.js | 12 +- .../egov-ui-kit-dev/src/redux/auth/reducer.js | 12 +- .../egov-ui-kit-dev/src/utils/api.js | 87 ++++++++------ .../egov-ui-kit-dev/src/utils/commons.js | 19 ++- .../src/utils/localStorageUtils/index.js | 54 +++++++-- .../packages/citizen/src/ui-utils/api.js | 32 ++++- 11 files changed, 369 insertions(+), 120 deletions(-) create mode 100644 web/rainmaker/CHANGELOG.md diff --git a/web/rainmaker/CHANGELOG.md b/web/rainmaker/CHANGELOG.md new file mode 100644 index 0000000000..6418f691d6 --- /dev/null +++ b/web/rainmaker/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Changed - Session-Based Authentication Migration + +**Date:** 2025-12-03 + +#### Overview +Migrated from token-based authentication (storing access tokens in localStorage) to session-based authentication (using HttpOnly cookies managed by the server). This change improves security by: +- Removing token storage from client-side localStorage +- Using HttpOnly cookies that are not accessible via JavaScript +- Passing session ID in request headers instead of request body + +#### Breaking Changes + +1. **Token Storage Removed** + - `setAccessToken()` and `setRefreshToken()` are now no-op functions (they log warnings) + - Tokens are no longer stored in localStorage + - Session is managed via HttpOnly cookies set by the server + +2. **API Request Format Changed** + - `authToken` field removed from `RequestInfo` object in request body + - Session ID now passed in `Session-Id` HTTP header + - All axios instances configured with `withCredentials: true` for cookie support + +3. **Redux State Changes** + - `token` field removed from auth reducer state + - Authentication state now relies on `hasValidSession()` instead of checking for access token + +#### Files Modified + +**Core Authentication Utilities:** +- `dev-packages/egov-ui-kit-dev/src/utils/localStorageUtils/index.js` + - Added `getSessionId()` - reads session from cookies + - Added `hasValidSession()` - checks for valid session + - Modified `getAccessToken()` - now returns session ID for backward compatibility + - Modified `setAccessToken()` and `setRefreshToken()` - now no-op with warnings + - Modified `clearUserDetails()` - now also clears session cookies + +**API Request Wrappers:** +- `dev-packages/egov-ui-kit-dev/src/utils/api.js` + - Added `getSessionHeaders()` helper + - Added axios request interceptor to inject Session-Id header + - Modified `wrapRequestBody()` - removed authToken from RequestInfo + - Modified `loginRequest()` - added `withCredentials: true` + - Modified `uploadFile()` - added session headers and credentials + - Modified `commonApiPost()` - removed authToken handling + - Changed error type from `INVALID_TOKEN` to `SESSION_EXPIRED` + +- `packages/citizen/src/ui-utils/api.js` + - Added session header management + - Removed authToken from RequestInfo + +- `dev-packages/egov-ui-framework-core/src/ui-utils/api.js` + - Added session header management + - Removed authToken from RequestInfo + +**Redux Auth Module:** +- `dev-packages/egov-ui-kit-dev/src/redux/auth/actions.js` + - Modified `authenticated()` - no longer stores tokens + - Modified `refreshTokenRequest()` - now triggers logout (deprecated) + - Modified `logout()` - simplified to clear session and redirect + +- `dev-packages/egov-ui-kit-dev/src/redux/auth/reducer.js` + - Removed `token` from initial state and action handlers + - Now uses `hasValidSession()` for initial auth check + +- `dev-packages/egov-ui-kit-dev/src/redux/auth/middleware.js` + - Removed authToken from RequestInfo objects + - Changed to trigger logout on SESSION_EXPIRED error + +**Utility Functions:** +- `dev-packages/egov-ui-kit-dev/src/utils/commons.js` + - Added `hasSessionExpired()` function + - Modified `hasTokenExpired()` to call `hasSessionExpired()` for backward compatibility + - Updated imports to use `getSessionId` + +**Higher-Order Components:** +- `dev-packages/egov-ui-kit-dev/src/hocs/withData.js` + - Changed to use `hasValidSession()` instead of `getAccessToken()` + +**Trade Licence Module:** +- `dev-packages/egov-tradelicence-dev/src/ui-config/screens/specs/utils/localStorageUtils/index.js` + - Added session-based authentication functions (mirrors main localStorageUtils) + +#### Migration Notes + +1. **Backend Requirements** + - Server must set session cookie on successful login (primary: `SESSION_ID`, alternatives: `sessionId`, `JSESSIONID`, `session-id`) + - Server must validate `Session-Id` header on API requests + - Server must handle logout by invalidating session and clearing cookie + - Cookies should be set with `HttpOnly`, `Secure`, and `SameSite` attributes + +2. **CORS Configuration** + - Server must allow credentials in CORS configuration + - `Access-Control-Allow-Credentials: true` + - `Access-Control-Allow-Origin` must be specific origin (not `*`) + +3. **Backward Compatibility** + - `getAccessToken()` still works but returns session ID + - `setAccessToken()` and `setRefreshToken()` log deprecation warnings + - `hasTokenExpired()` still works but calls `hasSessionExpired()` + +#### Security Improvements + +- **XSS Protection:** Tokens no longer accessible via `localStorage` or JavaScript +- **CSRF Protection:** Session cookies can be configured with `SameSite` attribute +- **Token Theft Prevention:** HttpOnly cookies cannot be stolen via XSS attacks diff --git a/web/rainmaker/dev-packages/egov-tradelicence-dev/src/ui-config/screens/specs/utils/localStorageUtils/index.js b/web/rainmaker/dev-packages/egov-tradelicence-dev/src/ui-config/screens/specs/utils/localStorageUtils/index.js index d917b6b608..a47ffbc94a 100644 --- a/web/rainmaker/dev-packages/egov-tradelicence-dev/src/ui-config/screens/specs/utils/localStorageUtils/index.js +++ b/web/rainmaker/dev-packages/egov-tradelicence-dev/src/ui-config/screens/specs/utils/localStorageUtils/index.js @@ -1,11 +1,36 @@ +const appName = process.env.REACT_APP_NAME; +// Cookie utility functions for session-based authentication +const getCookie = (name) => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +}; -const appName = process.env.REACT_APP_NAME; +const deleteCookie = (name) => { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; +}; -//GET methods +// Session-based authentication - Get session ID from cookie +export const getSessionId = () => { + return getCookie('SESSION_ID') || getCookie('sessionId') || getCookie('JSESSIONID') || getCookie('session-id'); +}; + +// Deprecated: kept for backward compatibility during migration +// Returns session ID instead of access token (no token storage) export const getAccessToken = () => { - return localStorageGet(`token`); + return getSessionId(); }; + +// Check if user has valid session +export const hasValidSession = () => { + const userInfo = getUserInfo(); + const sessionId = getSessionId(); + return !!(userInfo && sessionId); +}; + +//GET methods export const getUserInfo = () => { return localStorageGet("user-info"); }; @@ -32,12 +57,18 @@ export const getStoredModulesList = () =>{ export const setUserInfo = (userInfo) => { localStorageSet("user-info", userInfo, null); }; + +// Deprecated: No-op functions - tokens are no longer stored +// Session is managed via HttpOnly cookies set by the server export const setAccessToken = (token) => { - localStorageSet("token", token, null); + // No-op: Session is managed via cookies, not localStorage + console.warn('setAccessToken is deprecated. Session is managed via cookies.'); }; export const setRefreshToken = (refreshToken) => { - localStorageSet("refresh-token", refreshToken, null); + // No-op: Refresh tokens are no longer used with session-based auth + console.warn('setRefreshToken is deprecated. Session is managed via cookies.'); }; + export const setTenantId = (tenantId) => { localStorageSet("tenant-id", tenantId, null); }; @@ -56,9 +87,15 @@ export const setStoredModulesList =(storedModuleList) =>{ //Remove Items (LOGOUT) export const clearUserDetails = () => { + // Clear localStorage items Object.keys(localStorage).forEach((key) => { window.localStorage.removeItem(key); }); + // Clear session cookies + deleteCookie('SESSION_ID'); + deleteCookie('sessionId'); + deleteCookie('JSESSIONID'); + deleteCookie('session-id'); }; //Role specific get-set Methods export const localStorageGet = (key, path) => { diff --git a/web/rainmaker/dev-packages/egov-ui-framework-core/src/ui-utils/api.js b/web/rainmaker/dev-packages/egov-ui-framework-core/src/ui-utils/api.js index cab47685ba..91532d3c80 100644 --- a/web/rainmaker/dev-packages/egov-ui-framework-core/src/ui-utils/api.js +++ b/web/rainmaker/dev-packages/egov-ui-framework-core/src/ui-utils/api.js @@ -3,20 +3,40 @@ import { fetchFromLocalStorage, addQueryArg, getDateInEpoch, isPublicSearch } fr import { toggleSpinner } from "egov-ui-framework/ui-redux/screen-configuration/actions"; import store from "../ui-redux/store"; import { - getAccessToken, + getSessionId, getTenantId, getLocale } from "egov-ui-kit/utils/localStorageUtils"; +// Get session-based headers for API requests +const getSessionHeaders = () => { + const sessionId = getSessionId(); + const headers = { + "Content-Type": "application/json" + }; + if (sessionId) { + headers['Session-Id'] = sessionId; + } + return headers; +}; + const instance = axios.create({ baseURL: window.location.origin, - headers: { - "Content-Type": "application/json" + headers: getSessionHeaders(), + withCredentials: true // Enable cookies to be sent with requests +}); + +// Update headers before each request to include latest session ID +instance.interceptors.request.use((config) => { + const sessionId = getSessionId(); + if (sessionId && !isPublicSearch()) { + config.headers['Session-Id'] = sessionId; } + return config; }); const wrapRequestBody = (requestBody, action) => { - const authToken = getAccessToken(); + // Session ID is now passed in headers, not in request body let RequestInfo = { apiId: "Mihy", ver: ".01", @@ -25,10 +45,9 @@ const wrapRequestBody = (requestBody, action) => { did: "1", key: "", msgId: `20170310130900|${getLocale()}`, - requesterId: "", - authToken: authToken + requesterId: "" + // authToken removed - session is managed via cookies/headers }; - if(isPublicSearch()) delete RequestInfo.authToken; return Object.assign( {}, { diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/hocs/withData.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/hocs/withData.js index f7616f1cfd..161924d054 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/hocs/withData.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/hocs/withData.js @@ -6,24 +6,25 @@ import { fetchpgrConstants } from "egov-ui-kit/redux/common/actions"; import { fetchUiCommonConfig, fetchUiCommonConstants } from "egov-ui-kit/redux/app/actions"; import commonConfig from "config/common"; import { fetchGeneralMDMSData } from "egov-ui-kit/redux/common/actions"; -import { getAccessToken } from "egov-ui-kit/utils/localStorageUtils"; +import { hasValidSession } from "egov-ui-kit/utils/localStorageUtils"; import { generalMDMSDataRequestObj, getGeneralMDMSDataDropdownName } from "egov-ui-kit/utils/commons"; const withData = (Component) => { class Wrapper extends React.Component { componentDidMount() { const { searchUser, fetchComplaintCategories, authenticated, fetchpgrConstants, fetchUiCommonConfig, fetchUiCommonConstants, fetchGeneralMDMSData } = this.props; - if (getAccessToken()) { - + // Check for valid session instead of access token + if (hasValidSession()) { + searchUser(); fetchUiCommonConstants(); fetchComplaintCategories(); /* fetchpgrConstants(); fetchUiCommonConfig(); - - fetchGeneralMDMSData(); + + fetchGeneralMDMSData(); */ - + } } diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/actions.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/actions.js index 416bae1e35..671a53e08f 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/actions.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/actions.js @@ -6,10 +6,8 @@ import { prepareFormData } from "egov-ui-kit/utils/commons"; import get from "lodash/get"; import { setTenantId, - getAccessToken, + getSessionId, setUserInfo, - setAccessToken, - setRefreshToken, localStorageSet, localStorageGet, clearUserDetails, @@ -59,22 +57,21 @@ export const authenticating = () => { return { type: authType.AUTHENTICATING }; }; +// Session-based authentication - tokens are no longer stored client-side +// Session is managed via HttpOnly cookies set by the server export const authenticated = (payload = {}) => { const userInfo = fixUserDob(payload["UserRequest"]); - const accessToken = payload.access_token; - const refreshToken = payload.refresh_token; - const expiresIn = payload.expires_in; const lastLoginTime = new Date().getTime(); + // Store user info (not tokens - session managed via cookies) setUserInfo(JSON.stringify(userInfo)); - setAccessToken(accessToken); - setRefreshToken(refreshToken); setTenantId(userInfo.tenantId); - localStorageSet("expires-in", expiresIn); localStorageSet("last-login-time", lastLoginTime); - localStorageSet("CITIZEN.CITY",userInfo.permanentCity); + localStorageSet("CITIZEN.CITY", userInfo.permanentCity); - return { type: authType.AUTHENTICATED, userInfo, accessToken }; + // Note: access_token and refresh_token are no longer stored + // Session is managed via HttpOnly cookies set by the server + return { type: authType.AUTHENTICATED, userInfo, authenticated: true }; }; export const authenticationFailed = () => { @@ -104,20 +101,14 @@ export const searchUser = () => { }; }; +// Deprecated: Session refresh is now handled by the server via cookies +// This function is kept for backward compatibility but will trigger logout on session expiry export const refreshTokenRequest = () => { return async (dispatch) => { - const refreshToken = localStorageGet("refresh-token"); - const grantType = "refresh_token"; - const userType = process.env.REACT_APP_NAME === "Citizen" ? "CITIZEN" : "EMPLOYEE"; - try { - const response = await loginRequest(null, null, refreshToken, grantType, "", userType); - delete response.ResponseInfo; - dispatch(authenticated(response)); - // only option for the time being! - window.location.reload(); - } catch (error) { - dispatch(logout()); - } + // With session-based auth, session refresh is handled by the server + // If session expires, user must re-authenticate + console.warn('refreshTokenRequest is deprecated. Session is managed via server cookies.'); + dispatch(logout()); }; }; @@ -136,28 +127,28 @@ export const sendOTP = (intent) => { }; }; +// Session-based logout - clears local data and calls server logout endpoint export const logout = () => { return async () => { try { - const authToken = getAccessToken(); - if (authToken) { - const response = await httpRequest(AUTH.LOGOUT.URL, AUTH.LOGOUT.ACTION, [{ key: "access_token", value: authToken }]); - } else { - clearUserDetails(); - process.env.REACT_APP_NAME === "Citizen" - ? window.location.replace(`${window.basename}/user/register`) - : window.location.replace(`${window.basename}/user/login`); - return; + const sessionId = getSessionId(); + if (sessionId) { + // Call server logout endpoint to invalidate session + // Server will clear the session cookie + await httpRequest(AUTH.LOGOUT.URL, AUTH.LOGOUT.ACTION, []); } } catch (error) { - console.log(error); - clearUserDetails(); + console.log('Logout request error:', error); + // Continue with local cleanup even if server request fails } - // whatever happens the client should clear the user details - // let userInfo=getUserInfo(); - // let userRole=get(userInfo,"roles[0].code"); + + // Clear all local user data and cookies clearUserDetails(); - // window.location.replace(`${window.basename}/user/login`) - window.location.replace(`${window.basename}/user/login`); + + // Redirect to appropriate login page + const redirectUrl = process.env.REACT_APP_NAME === "Citizen" + ? `${window.basename}/user/register` + : `${window.basename}/user/login`; + window.location.replace(redirectUrl); }; }; diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/middleware.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/middleware.js index f18f659ab4..ec384f8e9e 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/middleware.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/middleware.js @@ -1,6 +1,6 @@ -import { refreshTokenRequest } from "egov-ui-kit/redux/auth/actions"; +import { logout } from "egov-ui-kit/redux/auth/actions"; import { USER_SEARCH_SUCCESS } from "./actionTypes"; -import { getAccessToken, getTenantId } from "egov-ui-kit/utils/localStorageUtils"; +import { getSessionId, getTenantId } from "egov-ui-kit/utils/localStorageUtils"; import { getNotificationCount, getNotifications } from "../app/actions"; import get from "lodash/get" @@ -18,6 +18,7 @@ const auth = (store) => (next) => (action) => { value: permanentCity ? permanentCity : getTenantId(), }, ]; + // Session ID is passed in headers, not in request body const requestBody = { RequestInfo: { apiId: "org.egov.pt", @@ -28,7 +29,7 @@ const auth = (store) => (next) => (action) => { key: "xyz", msgId: "654654", requesterId: "61", - authToken: getAccessToken(), + // authToken removed - session is managed via cookies/headers }, }; if ((window.location.pathname === "/" || window.location.pathname === "/citizen/" || window.location.pathname === "/employee/inbox")) { @@ -38,8 +39,9 @@ const auth = (store) => (next) => (action) => { } } - if (/(_ERROR|_FAILURE)$/.test(type) && action.error === "INVALID_TOKEN") { - store.dispatch(refreshTokenRequest()); + // Handle session expiry - logout user when session expires + if (/(_ERROR|_FAILURE)$/.test(type) && (action.error === "SESSION_EXPIRED" || action.error === "INVALID_TOKEN")) { + store.dispatch(logout()); } else { next(action); } diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/reducer.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/reducer.js index 0a12a78345..8a8c6ea704 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/reducer.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/redux/auth/reducer.js @@ -1,17 +1,17 @@ import * as actionTypes from "./actionTypes"; -import { getTenantId, getAccessToken, getUserInfo } from "egov-ui-kit/utils/localStorageUtils"; +import { getTenantId, getSessionId, getUserInfo, hasValidSession } from "egov-ui-kit/utils/localStorageUtils"; const userInfo = JSON.parse(getUserInfo()); -const authenticated = userInfo ? true : false; +// Session-based auth: check for valid session instead of token +const authenticated = hasValidSession(); const tenantId = getTenantId(); -const token = getAccessToken(); const intialState = { authenticating: false, authenticated, authenticationFailed: !authenticated, userInfo, - token, + // token removed - session is managed via cookies tenantId, }; @@ -30,7 +30,7 @@ const auth = (state = intialState, action) => { authenticationFailed: false, authenticating: false, userInfo: action.userInfo, - token: action.accessToken, + // token removed - session is managed via cookies }; case actionTypes.AUTHENTICATION_FAILED: return { ...state, authenticated: false, authenticationFailed: true, authenticating: false }; @@ -43,7 +43,7 @@ const auth = (state = intialState, action) => { authenticationFailed: false, authenticating: false, userInfo: {}, - token: "", + // token removed - session is managed via cookies }; case actionTypes.SEND_OTP_STARTED: return { ...state, authenticating: true }; diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/api.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/api.js index 6fd8435b54..a5fe1808f5 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/api.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/api.js @@ -1,8 +1,8 @@ import axios from "axios"; import commonConfig from "egov-ui-kit/config/common.js"; -import { getAccessToken, getLocale, getTenantId, localStorageGet, localStorageSet, setLocale, setTenantId } from "egov-ui-kit/utils/localStorageUtils"; +import { getSessionId, getLocale, getTenantId, localStorageGet, localStorageSet, setLocale, setTenantId } from "egov-ui-kit/utils/localStorageUtils"; import some from "lodash/some"; -import { addQueryArg, hasTokenExpired, prepareForm } from "./commons"; +import { addQueryArg, hasSessionExpired, prepareForm } from "./commons"; import store from "ui-redux/store"; import { hideSpinner,showSpinner } from "egov-ui-kit/redux/common/actions"; @@ -19,17 +19,36 @@ axios.interceptors.response.use( } ); -const instance = axios.create({ - baseURL: window.location.origin, - headers: { +// Get session-based headers for API requests +const getSessionHeaders = () => { + const sessionId = getSessionId(); + const headers = { "Content-Type": "application/json", 'X-Frame-Options': 'sameorigin' - }, + }; + if (sessionId) { + headers['Session-Id'] = sessionId; + } + return headers; +}; + +const instance = axios.create({ + baseURL: window.location.origin, + headers: getSessionHeaders(), + withCredentials: true, // Enable cookies to be sent with requests }); -const wrapRequestBody = (requestBody, action, customRequestInfo) => { - const authToken = getAccessToken(); +// Update headers before each request to include latest session ID +instance.interceptors.request.use((config) => { + const sessionId = getSessionId(); + if (sessionId) { + config.headers['Session-Id'] = sessionId; + } + return config; +}); +const wrapRequestBody = (requestBody, action, customRequestInfo) => { + // Session ID is now passed in headers, not in request body let RequestInfo = { apiId: "Rainmaker", ver: ".01", @@ -39,7 +58,7 @@ const wrapRequestBody = (requestBody, action, customRequestInfo) => { key: "", msgId: `20170310130900|${getLocale()}`, // requesterId: "", - authToken, + // authToken removed - session is managed via cookies/headers }; RequestInfo = { ...RequestInfo, ...customRequestInfo }; return Object.assign( @@ -81,8 +100,8 @@ export const multiHttpRequest = async ( } catch (error) { const { data, status } = error.response[0]; - if (hasTokenExpired(status, data)) { - apiError = "INVALID_TOKEN"; + if (hasSessionExpired(status, data)) { + apiError = "SESSION_EXPIRED"; } else { apiError = (data.hasOwnProperty("Errors") && data.Errors && data.Errors.length && data.Errors[0].message) || @@ -140,8 +159,8 @@ export const httpRequestForAssessmentCancellation = async ( } catch (error) { const { data, status } = error.response; - if (hasTokenExpired(status, data)) { - apiError = "INVALID_TOKEN"; + if (hasSessionExpired(status, data)) { + apiError = "SESSION_EXPIRED"; } else { apiError = (data.hasOwnProperty("Errors") && data.Errors && data.Errors.length && data.Errors[0].message) || @@ -201,8 +220,8 @@ export const httpRequest = async ( } catch (error) { const { data, status } = error.response; - if (hasTokenExpired(status, data)) { - apiError = "INVALID_TOKEN"; + if (hasSessionExpired(status, data)) { + apiError = "SESSION_EXPIRED"; } else { apiError = (data.hasOwnProperty("Errors") && data.Errors && data.Errors.length && data.Errors[0].message) || @@ -218,11 +237,17 @@ export const httpRequest = async ( export const uploadFile = async (endPoint, module, file, ulbLevel) => { // Bad idea to fetch from local storage, change as feasible const tenantId = getTenantId() ? (ulbLevel ? getTenantId() : getTenantId().split(".")[0]) : ""; + const sessionId = getSessionId(); + const uploadHeaders = { + "Content-Type": "multipart/form-data", + }; + if (sessionId) { + uploadHeaders['Session-Id'] = sessionId; + } const uploadInstance = axios.create({ baseURL: window.location.origin, - headers: { - "Content-Type": "multipart/form-data", - }, + headers: uploadHeaders, + withCredentials: true, }); const requestParams = { @@ -248,6 +273,7 @@ export const uploadFile = async (endPoint, module, file, ulbLevel) => { } }; +// Session-based login - server sets session cookie on successful login export const loginRequest = async (username = null, password = null, refreshToken = "", grantType = "password", tenantId = "", userType) => { tenantId = tenantId ? tenantId : commonConfig.tenantId; const loginInstance = axios.create({ @@ -256,12 +282,14 @@ export const loginRequest = async (username = null, password = null, refreshToke "Content-Type": "application/x-www-form-urlencoded", Authorization: "Basic ZWdvdi11c2VyLWNsaWVudDplZ292LXVzZXItc2VjcmV0", }, + withCredentials: true, // Enable cookies to be received and stored }); let apiError = "Api Error"; var params = new URLSearchParams(); username && params.append("username", username); password && params.append("password", password); + // refreshToken is deprecated for session-based auth but kept for backward compatibility refreshToken && params.append("refresh_token", refreshToken); params.append("grant_type", grantType); params.append("scope", "read"); @@ -272,6 +300,8 @@ export const loginRequest = async (username = null, password = null, refreshToke const response = await loginInstance.post("/user/oauth/token", params); const responseStatus = parseInt(response.status, 10); if (responseStatus === 200 || responseStatus === 201) { + // Session cookie is automatically set by the server + // Return user data (tokens are no longer stored client-side) return response.data; } } catch (error) { @@ -290,21 +320,12 @@ export const commonApiPost = ( doNotOverride = false, isTimeLong = true, noPageSize = false, - authToken = "", + authToken = "", // Deprecated - kept for backward compatibility userInfo = "", isStateLevel = false, offset = 0 ) => { - // const RequestInfo = { - // apiId: "Rainmaker", - // ver: ".01", - // ts: "", - // did: "1", - // key: "", - // msgId: "20170310130900|en_IN", - // requesterId: "", - // authToken, - // }; + // Session-based auth - authToken removed from RequestInfo const RequestInfo = { apiId: "emp", ver: "1.0", @@ -314,7 +335,7 @@ export const commonApiPost = ( key: "abcdkey", msgId: "20170310130900", requesterId: "", - authToken, + // authToken removed - session is managed via cookies/headers }; var url = context; if (url && url[url.length - 1] === "/") url = url.substring(0, url.length - 1); @@ -341,15 +362,11 @@ export const commonApiPost = ( url += "&offset=" + offset; - RequestInfo.authToken = getAccessToken(); + // Session ID is passed in headers via axios interceptor, not in RequestInfo if (isTimeLong) { RequestInfo.ts = new Date().getTime(); } - if (authToken) { - RequestInfo["authToken"] = authToken; - } - body["RequestInfo"] = RequestInfo; if (userInfo) { diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/commons.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/commons.js index 73f2d48c33..bfe45c86a6 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/commons.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/commons.js @@ -6,7 +6,7 @@ import { toggleSnackbarAndSetText } from "egov-ui-kit/redux/app/actions"; import { setFieldProperty } from "egov-ui-kit/redux/form/actions"; import { httpRequest } from "egov-ui-kit/utils/api"; import { TENANT } from "egov-ui-kit/utils/endPoints"; -import { getAccessToken, getTenantId, getUserInfo, localStorageGet, localStorageSet } from "egov-ui-kit/utils/localStorageUtils"; +import { getSessionId, getTenantId, getUserInfo, localStorageGet, localStorageSet, hasValidSession } from "egov-ui-kit/utils/localStorageUtils"; import Label from "egov-ui-kit/utils/translationNode"; import get from "lodash/get"; import isEmpty from "lodash/isEmpty"; @@ -732,10 +732,23 @@ export const trimObj = (obj) => { return obj; }; +// Deprecated: Use hasSessionExpired instead export const hasTokenExpired = (status, data) => { + return hasSessionExpired(status, data); +}; + +// Check if session has expired based on API response +export const hasSessionExpired = (status, data) => { + // 401 Unauthorized indicates session expiry if (status === 401) { - if (data && data.Errors && Array.isArray(data.Errors) && data.Errors.length > 0 && data.Errors[0].code === "InvalidAccessTokenException") - return true; + // Check for specific error codes that indicate session/token expiry + if (data && data.Errors && Array.isArray(data.Errors) && data.Errors.length > 0) { + const errorCode = data.Errors[0].code; + return errorCode === "InvalidAccessTokenException" || + errorCode === "SessionExpiredException" || + errorCode === "InvalidSessionException"; + } + return true; // 401 without specific error still means unauthorized } return false; }; diff --git a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/localStorageUtils/index.js b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/localStorageUtils/index.js index 013cdbb928..7bbea8c9b6 100644 --- a/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/localStorageUtils/index.js +++ b/web/rainmaker/dev-packages/egov-ui-kit-dev/src/utils/localStorageUtils/index.js @@ -1,8 +1,18 @@ -import { use } from "react"; +const appName = process.env.REACT_APP_NAME; +// Cookie utility functions for session-based authentication +const getCookie = (name) => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +}; -const appName = process.env.REACT_APP_NAME; -//Fileter User Object +const deleteCookie = (name) => { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; +}; + +//Filter User Object export const removeFields = (user = {}, fields = []) => { if (!user || typeof user !== "object") return user; // safety check @@ -16,7 +26,7 @@ export const removeFields = (user = {}, fields = []) => { return updatedUser; }; -//Encryption and decryption +//Encryption and decryption export const encryptUserDetails = (user={}) => { user = JSON.parse(user) @@ -38,10 +48,26 @@ export const decryptUserDetails = (user ={})=>{ user = {...user, mobileNumber : mobileNumber, emailId : emailId, name : name, dob : dob} return JSON.stringify(user); } -//GET methods + +// Session-based authentication - Get session ID from cookie +export const getSessionId = () => { + return getCookie('SESSION_ID') || getCookie('sessionId') || getCookie('JSESSIONID') || getCookie('session-id'); +}; + +// Deprecated: kept for backward compatibility during migration +// Returns session ID instead of access token (no token storage) export const getAccessToken = () => { - return localStorageGet(`token`); + return getSessionId(); +}; + +// Check if user has valid session +export const hasValidSession = () => { + const userInfo = getUserInfo(); + const sessionId = getSessionId(); + return !!(userInfo && sessionId); }; + +//GET methods export const getUserInfo = () => { return decryptUserDetails(localStorageGet("user-info")); @@ -84,12 +110,18 @@ export const setUserInfo = (userInfo) => { userInfo = encryptUserDetails(userInfo) localStorageSet("user-info", userInfo, null); }; + +// Deprecated: No-op functions - tokens are no longer stored +// Session is managed via HttpOnly cookies set by the server export const setAccessToken = (token) => { - localStorageSet("token", token, null); + // No-op: Session is managed via cookies, not localStorage + console.warn('setAccessToken is deprecated. Session is managed via cookies.'); }; export const setRefreshToken = (refreshToken) => { - localStorageSet("refresh-token", refreshToken, null); + // No-op: Refresh tokens are no longer used with session-based auth + console.warn('setRefreshToken is deprecated. Session is managed via cookies.'); }; + export const setTenantId = (tenantId) => { localStorageSet("tenant-id", tenantId, null); }; @@ -107,9 +139,15 @@ export const setStoredModulesList =(storedModuleList) =>{ }; //Remove Items (LOGOUT) export const clearUserDetails = () => { + // Clear localStorage items Object.keys(localStorage).forEach((key) => { window.localStorage.removeItem(key); }); + // Clear session cookies + deleteCookie('SESSION_ID'); + deleteCookie('sessionId'); + deleteCookie('JSESSIONID'); + deleteCookie('session-id'); }; //Role specific get-set Methods export const localStorageGet = (key, path) => { diff --git a/web/rainmaker/packages/citizen/src/ui-utils/api.js b/web/rainmaker/packages/citizen/src/ui-utils/api.js index 27c8c57545..3f5fc328fb 100644 --- a/web/rainmaker/packages/citizen/src/ui-utils/api.js +++ b/web/rainmaker/packages/citizen/src/ui-utils/api.js @@ -1,16 +1,36 @@ import axios from "axios"; import { fetchFromLocalStorage, addQueryArg, getDateInEpoch } from "egov-ui-framework/ui-utils/commons"; -import { getAccessToken, getTenantId, getLocale } from "egov-ui-kit/utils/localStorageUtils"; +import { getSessionId, getTenantId, getLocale } from "egov-ui-kit/utils/localStorageUtils"; + +// Get session-based headers for API requests +const getSessionHeaders = () => { + const sessionId = getSessionId(); + const headers = { + "Content-Type": "application/json", + }; + if (sessionId) { + headers['Session-Id'] = sessionId; + } + return headers; +}; const instance = axios.create({ baseURL: window.location.origin, - headers: { - "Content-Type": "application/json", - }, + headers: getSessionHeaders(), + withCredentials: true, // Enable cookies to be sent with requests +}); + +// Update headers before each request to include latest session ID +instance.interceptors.request.use((config) => { + const sessionId = getSessionId(); + if (sessionId) { + config.headers['Session-Id'] = sessionId; + } + return config; }); const wrapRequestBody = (requestBody, action) => { - const authToken = getAccessToken(); + // Session ID is now passed in headers, not in request body let RequestInfo = { apiId: "Rainmaker", ver: ".01", @@ -20,7 +40,7 @@ const wrapRequestBody = (requestBody, action) => { key: "", msgId: `20170310130900|${getLocale()}`, requesterId: "", - authToken, + // authToken removed - session is managed via cookies/headers }; return Object.assign( {},