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

Published: (February 1, 2026 at 04:53 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Stop using JWT libraries: How to build your own Lightweight Tokens with node:crypto

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.
  • Performancenode:crypto is 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:

  1. The Payload – A Base64URL encoded string containing user data (ID, username, avatar).
  2. 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 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 keep the web secure.

Are you still using JWT libraries, or have you moved to native crypto? Let me know in the comments!

Back to Blog

Related posts

Read more »

Switch on RUST

My path from Java to Rust: changing the technology stack Hello, my name is Garik, and today I want to share with you the story of how I decided to change the t...