A Modern Take on Enhancing API Security Beyond Authentication ππ
In the evolving world of web security, authentication and authorization are the backbone of most secure systems. However, relying solely on these mechanisms without addressing CSRF (Cross-Site Request Forgery) or brute-force risks can leave your APIs exposed. A malicious actor can inspect network calls, mimic headers, and execute requests outside your browserβusing tools like Postman, CURL, or custom scripts.
Sounds alarming, right? Even top platforms like X (formerly Twitter), Facebook, TikTok, and Instagram are not immune to this issue. So, how do we fight back? Introducing Advanced CSRF Protection using RSA encryptionβa lightweight yet effective way to prevent unauthorized API usage, brute-force attempts, and stale requests.
This technique adds an extra layer of security on top of your existing authentication system. By encrypting request metadata like the method, URL, and timestamp using RSA, we ensure each API request is uniquely validated and cannot be replayed or reused.
How Does It Work? π οΈ
We leverage RSA asymmetric encryption to validate the integrity of API requests. Here's the flow:
- Generate an RSA key pair (public and private keys).You can use tools like cryptotools.net/rsagen.
- The public key is stored on the client (ideally in environment variables).
- Before making an API request, we encrypt a payload containing:
- The request method
- The request URL
- A current timestamp
- This encrypted payload is sent as a custom
csrf
header. - On the backend:
- The payload is decrypted using the private RSA key.
- Validation ensures:
- The method and URL match.
- The timestamp isnβt stale (e.g., older than 15 seconds).
- If validation passes, the request proceeds; otherwise, itβs rejected.
This approach ensures that every API request is tightly bound to specific parameters, preventing replay attacks, stale requests, and brute-force abuse.
Client-Side Implementation (React + Axios) βοΈ
To streamline CSRF handling & ensure that it'll be available for all our request, weβll use Axios interceptors. Hereβs the setup in TypeScript:
import axios, { AxiosError } from "axios";
import appConfig from "config"; // Import your app config
import { rsaCrypt } from "Utils/rsaCrypt"; // RSA utility
const axiosClient = axios.create({
baseURL: appConfig.BASE_URL,
headers: {
"Content-Type": "application/json",
},
});
// Helper function to create the CSRF payload
const getCsrfPayload = (method: string, url: string) => {
return {
url,
method,
timestamp: Date.now(),
};
};
// Axios request interceptor
axiosClient.interceptors.request.use(
async (config) => {
if (config && config.headers) {
// Generate CSRF payload
const csrfPayload = getCsrfPayload(config.method!, config.url!);
// Encrypt the payload
const csrfToken = await rsaCrypt.encrypt(JSON.stringify(csrfPayload));
if (csrfToken) {
config.headers["csrf"] = csrfToken; // Attach the encrypted token to headers
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Axios response interceptor
axiosClient.interceptors.response.use(
function (response) {
return response;
},
async function (error: AxiosError<{ code: string; message: string }>) {
return Promise.reject(error);
}
);
export default axiosClient;
Backend Implementation (Node.js + Express) π‘οΈ
On the server, weβll validate the incoming CSRF token using the RSA private key and ensure the request integrity.
Middleware for CSRF Validation
import { NextFunction, Request, Response } from "express";
import { BadRequestError } from "../utils/custom.error";
import { rsaEncrypt } from "../utils/rsa.crypt";
type EncPayload = {
url: string;
method: string;
timestamp: number;
};
export async function csrfValidation(req: Request, res: Response, next: NextFunction) {
const urlPath = decodeURIComponent(req.originalUrl.split("?")[0]?.trim() || "");
const method = req.method.toLowerCase();
const csrf = req.headers["csrf"];
if (!csrf || Array.isArray(csrf)) {
throw new BadRequestError("CSRF token is missing or malformed.", "CSRF_TOKEN_MISSING");
}
const now = Date.now();
const payload = decryptPayload(csrf);
if (!payload) {
throw new BadRequestError("Failed to decrypt or parse CSRF token.", "CSRF_TOKEN_INVALID");
}
const payloadUrlPath = decodeURIComponent(payload.url.split("?")[0]?.trim() || "");
if (urlPath !== payloadUrlPath || payload.method.toLowerCase() !== method) {
throw new BadRequestError(
"CSRF token validation failed: URL or method mismatch.",
"CSRF_VALIDATION_FAILED"
);
}
const queryExpiry = 15 * 1000; // 15 seconds
if (now - payload.timestamp > queryExpiry) {
throw new BadRequestError("CSRF token has expired.", "CSRF_TOKEN_EXPIRED");
}
next();
}
function decryptPayload<T = EncPayload>(payload: string): T | undefined {
try {
return JSON.parse(rsaEncrypt.decrypt(payload)) as T;
} catch (error) {
console.error("Error decrypting payload:", error);
return undefined;
}
}
Key Features π―
- Asymmetric Encryption: Only the server knows the private key, ensuring tokens cannot be forged.
- Request Freshness: Timestamps limit the token's validity to 15 seconds, preventing replay attacks.
- Method & URL Validation: Ensures tokens are tied to specific actions, blocking brute-force scripts.
I implemented this approach in one of my projects, Winzy Social, to enhance the security of API interactions and prevent unauthorized or stale requests. Feel free to check it out!
Demo & Source Code π
GitHub Repository: https://github.com/Nedum84/csrf-with-rsa
Live Demo: https://nedum84.github.io/csrf-with-rsa
Conclusion π
This approach enhances your API security in addition to existing authentication and authorization mechanisms. By introducing encrypted, timestamped CSRF tokens, you can prevent malicious actors from abusing your APIs outside the browser.
Give this a shot and level up your application security! π
Let's Connect π
- Twitter: @thenelson_o
- LinkedIn: Nelson Odo
What Do You Think? π
Share your thoughts! You can improve the code to serve your use case. Did this post help you?
Have ideas or feedback to share? Letβs continue the conversation in the comments below! π
Top comments (1)
Thanks for sharing.
And thanks for leaving a demo to work with