Skip to content
Node.js nd libraries 5 min read

jsonwebtoken: JWT Library

JSON Web Tokens (JWTs) are a compact, URL-safe way to carry signed claims between a server and a client, which makes them a popular choice for stateless authentication and authorization. The jsonwebtoken library is the de facto standard for issuing and validating these tokens in Node.js, exposing a small but careful API around signing, verification, and expiry. Used correctly it lets your API trust a token without a database round-trip; used carelessly it opens the door to forged credentials. This page covers the core flow and the security details that actually matter.

Installing and importing

Install the package and, if you use TypeScript, its types.

npm install jsonwebtoken
npm install -D @types/jsonwebtoken

The library is CommonJS, but it imports cleanly as a default export in ES modules.

// ES modules (package.json "type": "module")
import jwt from "jsonwebtoken";

// CommonJS equivalent
// const jwt = require("jsonwebtoken");

Signing a token

jwt.sign(payload, secretOrPrivateKey, [options]) creates a signed token. The payload becomes the token’s claims, so keep it small and never put secrets (passwords, full user records) in it — the payload is only Base64URL-encoded, not encrypted, and anyone can read it.

import jwt from "jsonwebtoken";

const SECRET = process.env.JWT_SECRET; // load from env, never hardcode

const token = jwt.sign(
  { sub: "user_42", role: "admin" },
  SECRET,
  { expiresIn: "15m", issuer: "devcraftly.com" },
);

console.log(token);

Output:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzE4MzI...

When you pass expiresIn or issuer through options, the library writes the standard exp, iat, and iss claims for you. Pass a number to expiresIn for seconds, or a string like "15m", "2h", or "7d".

Common claims and options

OptionClaim writtenPurpose
expiresInexpToken lifetime (seconds or "15m", "7d")
notBeforenbfToken not valid until this time
issuerissWho issued the token
audienceaudIntended recipient
subjectsubThe principal the token is about
algorithmSigning algorithm (default HS256)
jwtidjtiUnique token id (useful for revocation)

Always set expiresIn. A token with no expiry is valid forever, so a single leaked token becomes a permanent credential.

Choosing an algorithm

The algorithm decides how the signature is computed. HS256 (the default) is symmetric: the same secret signs and verifies, which is simple but means every verifier must hold the signing secret. RS256 and ES256 are asymmetric: you sign with a private key and verify with a public key, so you can distribute the public key freely without letting third parties mint tokens.

import { readFileSync } from "node:fs";

const privateKey = readFileSync("private.pem");

const token = jwt.sign({ sub: "user_42" }, privateKey, {
  algorithm: "RS256",
  expiresIn: "1h",
});
FamilyAlgorithmsKey typeUse when
HMACHS256, HS384, HS512Shared secretOne service signs and verifies
RSARS256, RS384, RS512Public/privateMany services verify, one signs
ECDSAES256, ES384, ES512Public/privateSame as RSA, smaller keys/signatures

Verifying a token

jwt.verify(token, secretOrPublicKey, [options]) checks the signature and the time-based claims, returning the decoded payload or throwing if anything is wrong. Pin the algorithm with the algorithms option — this prevents the classic alg: none and HMAC/RSA confusion attacks where an attacker rewrites the header to bypass your key.

try {
  const payload = jwt.verify(token, SECRET, {
    algorithms: ["HS256"],
    issuer: "devcraftly.com",
  });
  console.log(payload.sub, payload.role);
} catch (err) {
  console.error(err.name, "-", err.message);
}

Output:

user_42 admin

Never call jwt.decode() to authenticate. It reads the payload without checking the signature, so it can be trivially forged. Use decode only for inspecting tokens you already trust.

Handling verification errors

verify throws typed errors so you can respond with the right status code instead of a generic 401.

import jwt from "jsonwebtoken";

function explain(token) {
  try {
    return jwt.verify(token, SECRET, { algorithms: ["HS256"] });
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      throw new Error("Token expired at " + err.expiredAt.toISOString());
    }
    if (err instanceof jwt.NotBeforeError) {
      throw new Error("Token not active yet");
    }
    // JsonWebTokenError covers bad signature, malformed token, etc.
    throw new Error("Invalid token: " + err.message);
  }
}
Error classTriggered by
TokenExpiredErrorexp is in the past (has expiredAt)
NotBeforeErrornbf is in the future (has date)
JsonWebTokenErrorBad signature, malformed token, wrong issuer/audience

Middleware to protect routes

A small Express middleware extracts the bearer token, verifies it, and attaches the claims to the request. Downstream handlers can then read req.user and trust it.

import jwt from "jsonwebtoken";

export function authenticate(req, res, next) {
  const header = req.headers.authorization ?? "";
  const [scheme, token] = header.split(" ");

  if (scheme !== "Bearer" || !token) {
    return res.status(401).json({ error: "Missing bearer token" });
  }

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ["HS256"],
      issuer: "devcraftly.com",
    });
    next();
  } catch (err) {
    const status = err instanceof jwt.TokenExpiredError ? 401 : 403;
    res.status(status).json({ error: err.message });
  }
}

// Usage
// app.get("/me", authenticate, (req, res) => res.json(req.user));

For role checks, compose a second middleware that runs after authenticate and inspects req.user.role, returning 403 when the claim does not grant access.

Best Practices

  • Load the signing secret or private key from environment variables (see dotenv) and use a long, random secret of at least 32 bytes.
  • Always set a short expiresIn for access tokens and use a separate, longer-lived refresh token to obtain new ones.
  • Pass an explicit algorithms allow-list to jwt.verify so the header alone cannot dictate how the token is validated.
  • Verify issuer and audience so a token minted for another service or tenant cannot be replayed against yours.
  • Keep payloads minimal and free of sensitive data, since the contents are readable by anyone holding the token.
  • For logout or compromise, track a jti and maintain a short-lived denylist, since stateless tokens cannot otherwise be revoked before they expire.
Last updated June 14, 2026
Was this helpful?