All files / src/hooks useCsrfToken.ts

0% Statements 0/127
100% Branches 0/0
0% Functions 0/1
0% Lines 0/127

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                                                                                                                                                                                                               
/**
 * CSRF Token Hook
 *
 * Provides CSRF token management for React components.
 * Uses NextAuth's built-in CSRF token endpoint.
 *
 * Usage:
 * ```typescript
 * const { csrfToken, getCsrfHeaders, isLoading } = useCsrfToken();
 *
 * // Use in fetch requests
 * const response = await fetch('/api/some-endpoint', {
 *   method: 'POST',
 *   headers: {
 *     'Content-Type': 'application/json',
 *     ...getCsrfHeaders(),
 *   },
 *   body: JSON.stringify(data),
 * });
 * ```
 */

"use client";

import { useState, useEffect, useCallback } from "react";

const CSRF_HEADER_NAME = "x-csrf-token";
const TOKEN_REFRESH_INTERVAL = 50 * 60 * 1000; // 50 minutes

interface UseCsrfTokenReturn {
  csrfToken: string | null;
  isLoading: boolean;
  error: Error | null;
  getCsrfHeaders: () => Record<string, string>;
  refreshToken: () => Promise<void>;
}

export function useCsrfToken(): UseCsrfTokenReturn {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchToken = useCallback(async () => {
    try {
      setIsLoading(true);
      setError(null);

      // Use NextAuth's built-in CSRF endpoint
      const response = await fetch("/api/auth/csrf", {
        method: "GET",
        credentials: "same-origin"});

      if (!response.ok) {
        throw new Error(`Failed to fetch CSRF token: ${response.status}`);
      }

      const data = await response.json();
      // NextAuth returns { csrfToken: "..." }
      setCsrfToken(data.csrfToken);
    } catch (err) {
      setError(err instanceof Error ? err : new Error("Unknown error"));
    } finally {
      setIsLoading(false);
    }
  }, []);

  // Fetch token on mount
  useEffect(() => {
    fetchToken();
  }, [fetchToken]);

  // Auto-refresh token periodically
  useEffect(() => {
    const intervalId = setInterval(fetchToken, TOKEN_REFRESH_INTERVAL);
    return () => clearInterval(intervalId);
  }, [fetchToken]);

  // Helper function to get headers for requests
  const getCsrfHeaders = useCallback((): Record<string, string> => {
    if (!csrfToken) {
      return {};
    }
    return {
      [CSRF_HEADER_NAME]: csrfToken};
  }, [csrfToken]);

  return {
    csrfToken,
    isLoading,
    error,
    getCsrfHeaders,
    refreshToken: fetchToken};
}

/**
 * Higher-order function to add CSRF headers to fetch requests
 *
 * Usage:
 * ```typescript
 * const { getCsrfHeaders } = useCsrfToken();
 * const secureFetch = createSecureFetch(getCsrfHeaders);
 *
 * // All requests will include CSRF headers
 * const response = await secureFetch('/api/endpoint', {
 *   method: 'POST',
 *   body: JSON.stringify(data),
 * });
 * ```
 */
export function createSecureFetch(
  getCsrfHeaders: () => Record<string, string>
): typeof fetch {
  return async (input: RequestInfo | URL, init?: RequestInit) => {
    const csrfHeaders = getCsrfHeaders();

    const mergedInit: RequestInit = {
      ...init,
      credentials: "same-origin",
      headers: {
        ...init?.headers,
        ...csrfHeaders}};

    return fetch(input, mergedInit);
  };
}

export default useCsrfToken;