Stop using JWT libraries: How to build your own Lightweight Tokens with node:crypto
Source: Dev.to

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
jsonwebtokenmeans one less package to audit for vulnerabilities. - Performance –
node:cryptois a native C++ 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 (e.g.,
alg: none). In our code we hard‑code 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
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
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
ufor username andimgfor 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 keep the web secure.
Are you still using JWT libraries, or have you moved to native crypto? Let me know in the comments!