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!

0 views
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...