@csrf-armor/nextjs
Version:
CSRF protection middleware for Next.js applications
182 lines (180 loc) • 5.33 kB
JavaScript
'use client';
//#region src/client/client.ts
/**
* Retrieves the current CSRF token from client-side storage.
*
* Attempts to find the CSRF token in the following order:
* 1. HTTP-only cookie (primary source for most strategies)
* 2. Meta tag fallback (for server-side rendered pages)
*
* The token retrieval is optimized for client-side access while maintaining
* security through HTTP-only cookies where possible.
*
* @param config - Optional configuration for cookie and header names
* @returns The CSRF token string, or null if not found or not in browser
*
* @example
* ```typescript
* // Basic usage
* const token = getCsrfToken();
*
* // With custom configuration
* const token = getCsrfToken({
* cookieName: 'my-csrf-cookie'
* });
*
* if (token) {
* // Use token in requests
* fetch('/api/data', {
* headers: { 'X-CSRF-Token': token }
* });
* }
* ```
*/
function getCsrfToken(config) {
if (typeof window === "undefined") return null;
const cookieName = config?.cookieName ?? "csrf-token";
const csrfCookie = document.cookie.split(";").find((c) => c.trim().startsWith(`${cookieName}=`));
if (csrfCookie) {
const value = csrfCookie.slice(csrfCookie.indexOf("=") + 1).trim();
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
return document.querySelector("meta[name=\"csrf-token\"]")?.getAttribute("content") ?? null;
}
/**
* Creates HTTP headers object with CSRF token for requests.
*
* Convenience function that retrieves the current CSRF token and formats
* it as headers ready to be used with fetch() or other HTTP clients.
*
* @param config - Optional configuration for cookie and header names
* @returns Headers object with CSRF token, or empty object if no token
*
* @example
* ```typescript
* // Basic usage
* const headers = createCsrfHeaders();
* fetch('/api/data', {
* method: 'POST',
* headers
* });
*
* // With custom header name
* const headers = createCsrfHeaders({
* headerName: 'X-Custom-CSRF'
* });
*
* // Merge with existing headers
* const headers = {
* 'Content-Type': 'application/json',
* ...createCsrfHeaders()
* };
* ```
*/
function createCsrfHeaders(config) {
const token = getCsrfToken(config);
if (!token) return {};
return { [config?.headerName ?? "x-csrf-token"]: token };
}
/**
* Enhanced fetch function with automatic CSRF token injection.
*
* A drop-in replacement for the standard fetch() function that automatically
* includes the CSRF token in request headers. Merges CSRF headers with any
* existing headers in the request.
*
* @param input - URL or Request object for the fetch
* @param init - Optional request initialization options
* @param config - Optional CSRF configuration
* @returns Promise resolving to fetch Response
*
* @example
* ```typescript
* // Basic POST request with automatic CSRF protection
* const response = await csrfFetch('/api/data', {
* method: 'POST',
* body: JSON.stringify({ name: 'John' }),
* headers: { 'Content-Type': 'application/json' }
* });
*
* // GET request (CSRF token included for consistency)
* const data = await csrfFetch('/api/users').then(r => r.json());
*
* // With custom configuration
* const response = await csrfFetch('/api/data', {
* method: 'DELETE'
* }, {
* headerName: 'X-Custom-CSRF',
* cookieName: 'my-csrf-token'
* });
* ```
*/
function csrfFetch(input, init, config) {
const headers = new Headers(input instanceof Request ? input.headers : void 0);
for (const [key, value] of new Headers(init?.headers)) headers.set(key, value);
for (const [key, value] of Object.entries(createCsrfHeaders(config))) headers.set(key, value);
return fetch(input, {
...init,
headers
});
}
/**
* Refreshes the CSRF token by making a lightweight server request.
*
* Sends a HEAD request to the server to trigger token refresh, then returns
* the new token from response headers or cookies. This is useful when tokens
* expire or when you need to ensure you have the latest token.
*
* The function gracefully handles failures by returning the current token
* if the refresh request fails.
*
* @param config - Optional configuration for endpoints and header names
* @returns Promise resolving to the refreshed token, or null if unavailable
*
* @example
* ```typescript
* // Basic token refresh
* const newToken = await refreshCsrfToken();
* if (newToken) {
* console.log('Token refreshed:', newToken);
* }
*
* // Refresh with custom endpoint
* const token = await refreshCsrfToken({
* refreshEndpoint: '/api/csrf/refresh',
* headerName: 'X-Custom-CSRF'
* });
*
* // Use in error handling
* try {
* await csrfFetch('/api/data', { method: 'POST' });
* } catch (error) {
* if (error.status === 403) {
* await refreshCsrfToken();
* // Retry the request
* }
* }
* ```
*/
async function refreshCsrfToken(config) {
if (typeof window === "undefined") return null;
try {
const response = await fetch(config?.refreshEndpoint ?? window.location.pathname, {
method: "HEAD",
credentials: "same-origin"
});
const headerName = config?.headerName ?? "x-csrf-token";
const newToken = response.headers.get(headerName);
if (newToken) return newToken;
return getCsrfToken(config);
} catch {
return getCsrfToken(config);
}
}
//#endregion
export { createCsrfHeaders, csrfFetch, getCsrfToken, refreshCsrfToken };
//# sourceMappingURL=client.js.map