From 33fdd858a0f7147e3a9dd934b4d476c7e3f180fe Mon Sep 17 00:00:00 2001 From: ZRE Date: Sun, 12 Oct 2025 03:03:36 +0800 Subject: [PATCH] fix csrf --- utils/csrf.ts | 37 +++++++++++++++++++++++++++++++++++++ utils/fetchUtils.ts | 16 ++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 utils/csrf.ts diff --git a/utils/csrf.ts b/utils/csrf.ts new file mode 100644 index 0000000..d180e4b --- /dev/null +++ b/utils/csrf.ts @@ -0,0 +1,37 @@ +'use server'; + +import crypto from 'crypto'; + +const ENCRYPTION_KEY_BASE64 = process.env.ENCRYPTION_KEY || ''; // Replace with your encryption key in Base64 +const RANDOM_BYTES_LENGTH = 16; // Length of random bytes +const GCM_NONCE_LENGTH = 12; // GCM nonce size is 12 bytes + +if (!ENCRYPTION_KEY_BASE64) { + throw new Error('ENCRYPTION_KEY is not set'); +} + +// Decode the Base64 encryption key +const ENCRYPTION_KEY = Buffer.from(ENCRYPTION_KEY_BASE64, 'base64'); + +// Encrypts the token using AES-GCM +function encryptToken(plaintext: string, key: Buffer): string { + const nonce = crypto.randomBytes(GCM_NONCE_LENGTH); + const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce); + + let encrypted = cipher.update(plaintext, 'utf8'); + encrypted = Buffer.concat([encrypted, cipher.final()]); + const authTag = cipher.getAuthTag(); + + const ciphertext = Buffer.concat([encrypted, authTag]); + const result = Buffer.concat([nonce, ciphertext]); + return result.toString('base64'); +} + +// Generates a new CSRF token with a timestamp and random bytes +export async function generateCSRFToken(): Promise { + const timestamp = Math.floor(Date.now() / 1000); // Current timestamp in seconds + const randomBytes = crypto.randomBytes(RANDOM_BYTES_LENGTH).toString('hex'); + const plaintext = `${randomBytes}|${timestamp}`; + + return encryptToken(plaintext, ENCRYPTION_KEY); +} \ No newline at end of file diff --git a/utils/fetchUtils.ts b/utils/fetchUtils.ts index bdf85d3..bfb9345 100644 --- a/utils/fetchUtils.ts +++ b/utils/fetchUtils.ts @@ -3,23 +3,35 @@ import { ApiResponse, RefreshTokenResponse } from "@/types/api/common"; // utils import { isTokenExpired, storeTokenExp } from "./tokenUtils"; +import { generateCSRFToken } from "./csrf"; // 刷新 token -export function refreshToken() { +export async function refreshToken() { return fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/refresh`, { method: "POST", headers: { accept: "application/json", + "x-csrf-token": await generateCSRFToken(), }, credentials: "include", }); } // 如果過期就刷新 token -export function fetchWithRefresh( +export async function fetchWithRefresh( input: RequestInfo, init?: RequestInit ): Promise { + // 加上 csrf token + const csrfToken = await generateCSRFToken(); + init = { + ...init, + headers: { + ...init?.headers, + "x-csrf-token": csrfToken, + }, + credentials: "include", + }; if (!isTokenExpired()) { return fetch(input, init); }