Ever looked at your node_modules and wondered why you need a massive library just to sign a small JSON object?
If you are building a Node.js backend, you already have a powerhouse security tool built-in: the node:crypto module.
Today, we’re going to build a custom, secure authentication token system that is faster, leaner, and more secure than standard JWT libraries.
Why skip the library?
Zero Dependencies: No jsonwebtoken means one less package to audit for vulnerabilities.
Performance:
node:cryptois a nativeC++binding in Node; it’s blazingly fast.No "Algorithm Switching" Attacks: Most JWT bugs happen because the library allows the client to choose the algorithm (like
alg: none). In our code, we hardcode the security.
The Logic
A secure token has two parts:
The Payload: A
Base64URLencoded string containing user data(ID, Username, Avatar).The Signature: An HMAC (Hash-based Message Authentication Code) that proves the payload hasn't been tampered with.
1. The Signing Function
This function takes your user data, adds an expiration timestamp, and signs it using a secret key from your .env.
import { createHmac } from "node:crypto";
const TOKEN_SECRET = process.env.TOKEN_SECRET;
export const signToken = (payload: object) => {
// Add 15 minute expiration
const claims = {
...payload,
exp: Date.now() + 15 * 60 * 1000,
};
// 1. Encode data to a URL-safe string
const encodedPayload = Buffer.from(JSON.stringify(claims)).toString("base64url");
// 2. Create the signature (The "Security Seal")
const signature = createHmac("sha256", TOKEN_SECRET!)
.update(encodedPayload)
.digest("base64url");
// 3. Combine them with a dot
return `${encodedPayload}.${signature}`;
};
2. The Verification Function
When the client sends the token back, we need to ensure it's still valid. We use timingSafeEqual to prevent timing attacks, a subtle way hackers guess secrets by measuring how long a string comparison takes.
import { createHmac, timingSafeEqual } from "node:crypto";
export const verifyToken = (token: string) => {
const [encodedPayload, signature] = token.split(".");
if (!encodedPayload || !signature) throw new Error("Malformed token");
// Re-calculate what the signature should be
const expectedSignature = createHmac("sha256", process.env.TOKEN_SECRET!)
.update(encodedPayload)
.digest("base64url");
// Use timingSafeEqual for high-level security
const isValid = timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) throw new Error("Invalid signature!");
const claims = JSON.parse(Buffer.from(encodedPayload, "base64url").toString());
// Check if expired
if (Date.now() > claims.exp) throw new Error("Token expired");
return claims;
};
Pro-Tip: Watch your Header Size!
Since we are putting "heavy" data like avatars and usernames inside the token, the string can get long.
Shorten your keys: Use u for username and img for avatar.
Keep it lean: Only store data the UI needs immediately.
Conclusion
By using node:crypto, you gain total control over your authentication. You aren't just following a tutorial; you're understanding the math and logic that keeps the web secure.
Are you still using JWT libraries, or have you moved to native crypto? Let me know in the comments!
Top comments (0)