(2025-05-23T22:37:08.370Z) [All cookies via js-cookie:] 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:151:12
(2025-05-23T22:37:08.370Z) [useSignup.ts] csrf token from cookie: 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:235:12
(2025-05-23T22:37:08.372Z) [All cookies via js-cookie:] 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:151:12
(2025-05-23T22:37:08.372Z) [useSignup.ts] csrf token from cookie: 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:235:12
XHRGET
http://localhost:5000/api/api-security/csrf-token
[HTTP/1.1 200 OK 4ms]

(2025-05-23T22:37:08.449Z) [All cookies via js-cookie:] 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:151:12
(2025-05-23T22:37:08.449Z) [csrfServices.ts] Token from backend response: avMQZSykK-aT-VJSk0WhbZFVWvrZ3zLPEtAwInslFmYEt306bfBbjOKvZBPfIckD_iEXjr2ICoNBqVxMKWsuJA== loggerUtils.ts:151:12
(2025-05-23T22:37:08.450Z) [csrfServices.ts] Token from cookie: 0c9f439c83a3f38ceaa38988985ce2b9e873dee010ad5ed0d9d85522aa94ab97a:2:{i:0;s:5:"_csrf";i:1;s:32:"nDm_ATpjqV6ALdhnotMtdW8LSylnRN8B";} loggerUtils.ts:151:12

// src/services/csrfServices.ts
import Cookies from 'js-cookie';
import apiClient from './apiClientServices';
import { ENDPOINTS } from '../constants/apiConstants';
import logger from '../utils/loggerUtils';


/**
* CSRF Token manager class.
* Handles fetching token from backend, reading from cookie,
* caching token internally, and providing easy access.
*
* On app startup, call:
* 	
* 	await csrfTokenManager.init();
* 
* In your axios interceptor (or wherever you send API requests):
* 
* 	import { csrfTokenManager } from './csrfServices';
*
* 	apiClient.interceptors.request.use(config =>
* 	{
* 		const token = csrfTokenManager.getToken();
* 		if (token)
* 		{
* 			config.headers['X-CSRF-Token'] = token;
* 		}
* 		
* 		return config;
* 	});
*/
export const CSRF_COOKIE_NAME = '_csrf';

export type CsrfData =
{
	csrfParam: string;
	csrfToken: string;
};

/**
* Fetch the CSRF token and param from the backend endpoint.
* This triggers the backend to set the CSRF cookie.
* Throws an error if token or param are missing.
*/
export async function fetchCsrfToken(): Promise<CsrfData>
{
	const response = await apiClient.get(ENDPOINTS.security.csrfToken);
	const { csrfParam, csrfToken } = response.data;
	
	if (!csrfParam || !csrfToken)
	{
		throw new Error('Failed to fetch CSRF token');
	}
	
	// wait 50-100ms to allow cookie to become available
	await new Promise(resolve => setTimeout(resolve, 50));
	
	const cookieToken = getCsrfTokenFromCookie();
	
	if (!cookieToken)
	{
		throw new Error('CSRF token cookie missing.');
	}
	
	/**
	* The backend sends the CSRF token in the JSON response as a plain string.
	*
	* While the backend also sets a CSRF cookie (e.g., named 'csrfToken'), this cookie's value
	* is signed and serialized by the Yii framework to prevent tampering.
	* That means the cookie does NOT contain the raw token string but a protected value
	* including a signature and serialized data.
	* 
	* Because of that, attempting to read the CSRF token directly from the cookie on the frontend
	* and compare it with the token from backend JSON response will fail due to mismatched formats.
	* 
	* Therefore, it is best practice to trust the backend JSON token directly for sending in headers 
	* and not attempt to read or validate the CSRF token from the cookie value.
	* 
	* This approach avoids dealing with Yii's cookie signing mechanics on the client side,
	* simplifies the CSRF token management,
	* and ensures synchronization without false mismatches.
	*/
	logger.log('csrfServices.ts', 'Token from backend response:', csrfToken);
	logger.log('csrfServices.ts', 'Token from cookie:', cookieToken);
	
	return { csrfParam, csrfToken };
}

/**
* Read the CSRF token directly from the CSRF cookie.
* Returns undefined if cookie is missing.
*/
export function getCsrfTokenFromCookie(): string | undefined
{
	logger.log('All cookies via js-cookie:', Cookies.get(CSRF_COOKIE_NAME));

	return Cookies.get(CSRF_COOKIE_NAME);
}

/**
* Clear old CSRF cookie
*/
export async function refreshCsrfToken(): Promise<CsrfData>
{
	// Optionally clear old cookie
	Cookies.remove(CSRF_COOKIE_NAME);
	
	// Now fetch new token; backend sets new signed cookie on response
	return fetchCsrfToken();
}

class CsrfTokenManager
{
	private csrfParam?: string;
	private csrfToken?: string;
	private initPromise?: Promise<void>;
	
	/**
	* Initialize the token manager by fetching token from backend,
	* and then synchronizing with the token in cookie.
	* Call once on app startup.
	*/
	async init(): Promise<void>
	{
		if (this.initPromise)
		{
			return this.initPromise;
		}
		
		this.initPromise = (async () =>
		{
			try
			{
				const { csrfParam, csrfToken } = await fetchCsrfToken();
			
				this.csrfParam = csrfParam;// Typically "_csrf"
				this.csrfToken = csrfToken;
			}
			catch (error)
			{
				logger.warn('csrfServices.ts', 'Could not fetch CSRF token from backend', error);
			}
			
			// Overwrite with cookie token if present; note cookie value is signed string,
			// but for consistency store it here.
			const cookieToken = getCsrfTokenFromCookie();
			
			if (cookieToken)
			{
				this.csrfToken = cookieToken;
			}
		})();
		
		return this.initPromise;
	}
	
	/**
	* Get the current CSRF token to be used for headers.
	* Prefers cookie token if it exists, else falls back to cached token.
	*/
	getToken(): string | undefined
	{
		// Prefer cookie token (signed) if it exists because Yii validates against cookie
		const cookieToken = getCsrfTokenFromCookie();
		
		if (cookieToken)
		{
			this.csrfToken = cookieToken;
			
			return cookieToken;
		}
		
		// Fall back to fetched token
		return this.csrfToken;
	}
	
	/**
	* Get the CSRF param name (cookie name / form param name).
	*/
	getParam(): string | undefined
	{
		return this.csrfParam;
	}
	
	/* Optional setter if you want to manually update tokens
	setToken(token: string, param?: string): void
	{
		this.csrfToken = token;
		
		if (param)
		{
			this.csrfParam = param;
		}
	}//*/
}

// Export a singleton CSRF token manager instance to be used app-wide.
export const csrfTokenManager = new CsrfTokenManager();


Since I need both the backend return token for the csrf and the cookie at different times, then this code needs to be refactor to reflect this. The
backend code is:

	public function beforeAction($action): bool
	{
		if (in_array($action->id, ['csrf-token', 'refresh-jwt-token', 'submit-form']))
		{
			$this->enableCsrfValidation = false;
		}
		
		return parent::beforeAction($action);
	}
	
	/**
	* Axios is configured properly on frontend to send cookies and headers
	* Must set { withCredentials: true } in Axios config to send cookies cross-site.
	* Add the CSRF token header from cookie or from your actionCsrfToken API, e.g.:
	*
	*	import Cookies from 'js-cookie';
	*	
	*	const csrfToken = Cookies.get('csrfToken'); // cookie name = 'csrfToken'
	*	axios.defaults.withCredentials = true;
	*	axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;
	*	// Alternatively, fetch token from API and set header dynamically
	*/
	public function actionCsrfToken(): array
	{
		$request = \Yii::$app->request;
		
		if (!$request->isGet)
		{
			throw new BadRequestHttpException('Only GET requests are allowed.');
		}
		
		$token = $request->getCsrfToken();
		
		\Yii::debug('CSRF Token from getCsrfToken(): ' . $token, __METHOD__);
		
		/**
		* Explicitly add CSRF cookie to response (forces Set-Cookie header)
		* Note: Setting cookie for CSRF token is handled by Yii
		*
		$response->cookies->add(
			new Cookie([
				//'name' => $request->csrfParam,// This would use '_csrf'
				'name' => 'csrfToken',
				'value' => $token,
				'httpOnly' => false,// Always false for csrf token
				'secure' => YII_ENV_PROD ? true : false,// True if you use HTTPS in production
				'sameSite' => YII_ENV_PROD ? Cookie::SAME_SITE_NONE : Cookie::SAME_SITE_LAX,
				'path' => '/',// Ensure cookie always overrides previous cookies on all paths
			])
		);//*/
		
		$response = \Yii::$app->response;
		
		//\Yii::debug("CSRF token to send to client: $token", __METHOD__);
		//\Yii::debug('Sending CSRF cookie with params: ' . json_encode($response->cookies->toArray()), __METHOD__);
		
		// No need to set format here explicitly if contentNegotiator behavior is active,
		//\Yii::$app->response->format = Response::FORMAT_JSON;
		
		\Yii::$app->response->statusCode = 200;
		
		/**
		* Return the raw CSRF token in JSON for frontend use.
		* - The CSRF cookie sent to the client is signed and serialized by Yii for security,
		*   so its value is NOT the raw token string and cannot be reliably read or used by JavaScript.
		* - The frontend needs the raw CSRF token string to include in request headers (e.g., 'X-CSRF-Token')
		*   or POST parameters to comply with Yii's CSRF validation mechanism.
		* - Returning the raw token via JSON allows the frontend to securely obtain the usable token 
		*   without parsing or trusting the signed cookie.
		* - The signed cookie enforces token integrity and ties the token to the user's session,
		*   while the raw token from JSON is the secret value the client must send back for validation.
		*
		* Example for typical usage flow
		* 
		*	// At app start
		*	await csrfTokenManager.init();
		*	
		*	// When sending API request or submitting form:
		*	const token = csrfTokenManager.getToken();
		*	
		*	apiClient.post('/user/update', formData, {
		*		headers: {
		*			'X-CSRF-Token': token
		*		}
		*	});
		*
		* Or, if submitting a form traditionally:
		*
		*	<input type="hidden" name="_csrf" value="TOKEN_FROM_BACKEND_JSON" />
		*/
		return [
			'csrfParam' => $request->csrfParam,
			'csrfToken' => $token,
		];
	}