Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions utils/csrf.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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);
}
16 changes: 14 additions & 2 deletions utils/fetchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
// 加上 csrf token
const csrfToken = await generateCSRFToken();
init = {
...init,
headers: {
...init?.headers,
"x-csrf-token": csrfToken,
},
credentials: "include",
};
if (!isTokenExpired()) {
return fetch(input, init);
}
Expand Down