Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Testing JWT Implementation in Node.js APIs for Common Pitfalls

Testing JWT Implementation in Node.js APIs for Common Pitfalls

pr0h0
node-jsjwtapi-testingauthentication
AI Usage (88%)

JWT bugs in Node.js APIs are rarely about cryptography first. They usually start as boundary mistakes: the server trusts the wrong token, skips one claim check, or treats authentication as if it were authorization. This post shows you how to test JWT implementation in Node.js APIs for those common pitfalls before they become real access-control bugs.

What usually goes wrong with JWT in Node.js APIs

The common failure mode is straightforward: the API accepts a token, verifies something, and then assumes the request is safe. That breaks in a few predictable ways:

  • the token is read from an untrusted place
  • the signature check is incomplete or misconfigured
  • exp, iss, and aud are not enforced
  • the route is protected, but the object inside the route is not

I usually break JWT testing into two layers: token validation and request authorization. If you only test the decoder, you miss the real bug.

Start with the trust boundary

Token source and parsing

First ask where the API gets the token. Header, cookie, query string, or a custom field all create different failure modes.

A safe baseline looks like this:

  • accept the token from one place only
  • reject malformed prefixes like Bearer without a token
  • trim parser quirks before verification
  • never trust a decoded payload before the signature is checked

If the app reads JWTs from both cookies and headers, I check whether one source overrides the other. That kind of ambiguity can turn into accidental privilege selection.

Signature verification and algorithm choice

The critical mistake is decoding before verifying. Decoding shows you claims, but it does not prove who signed them.

In Node.js, I want to see explicit verification with a known secret or public key, plus a pinned algorithm list.

const jwt = require("jsonwebtoken");

function verifyAccessToken(token) {
  return jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
    algorithms: ["RS256"],
    issuer: "https://auth.example.com",
    audience: "api.example.com",
  });
}

If the app accepts multiple algorithms without a clear reason, or accepts whatever the token claims, that is a red flag.

Expiration, issuer, and audience checks

A valid signature is not enough. The token also needs to be valid for this API, right now.

I test for:

  • expired tokens still being accepted
  • tokens issued by another environment working in production
  • tokens minted for a different audience working on the wrong service

These checks are cheap, and they block a lot of real abuse. A token for the mobile app should not automatically work on an internal admin API.

Reproduce the common failure modes safely

Tampered payloads and broken signature checks

A safe test is to change a non-sensitive claim in a copy of a token and see whether the API rejects it.

For example, if the payload has role: "user", change it to role: "admin" and verify that the request fails. The goal is not to bypass anything in production. It is to prove the server is actually verifying the signature before it trusts the payload.

If the route still succeeds after payload tampering, the backend is either decoding without verification or trusting client-provided claims too early.

none algorithm and weak library defaults

Old or careless JWT code sometimes accepts alg: none or fails open when the algorithm is not pinned.

I check for this by confirming the verifier rejects tokens whose header does not match the expected algorithm. This matters especially in projects that wrapped JWT handling in a helper and never revisited it.

⚠️

Never rely on library defaults for algorithm choice. Pin the allowed algorithms and fail closed when the header or key type does not match.

Missing authorization after authentication

This is the bug I see most often. The API verifies the user, then forgets to verify the action.

Example: a valid user token can reach GET /orders/:id, but the handler never checks whether that order belongs to the current account.

That is not a JWT bug alone. It is an authorization bug exposed by JWT.

Test the API routes, not just the token decoder

Role checks and object-level access

Once token verification passes, I move to route behavior. I want to confirm the handler checks both identity and ownership.

LayerWhat to testTypical bug
Auth middlewareSignature, exp, iss, audToken accepted too broadly
Route handlerRole and ownershipAny authenticated user can read another user’s data
Admin endpointsElevated permissionsUI hides the button, API still allows the call

A route that says “authenticated” is not enough. The backend must still compare userId, accountId, or tenant ID against the resource being requested.

Logout, revocation, and refresh behavior

JWT revocation is where teams often hand-wave. If you issue long-lived access tokens, logout may do nothing unless you maintain a denylist or use short TTLs with refresh tokens.

I test three things:

  1. revoked tokens stop working when they should
  2. refresh tokens cannot be reused forever
  3. a logout event actually changes server-side state

If the answer is “the client deleted local storage,” that is not revocation. That is just cleanup in the browser.

A small Node.js test harness for quick verification

Supertest and fixture tokens

A lightweight harness catches regressions fast. I keep a few fixture tokens: valid user, expired user, wrong audience, and tampered payload.

const request = require("supertest");
const app = require("../app");

describe("auth", () => {
  test("rejects expired tokens", async () => {
    const res = await request(app)
      .get("/api/profile")
      .set("Authorization", `Bearer ${fixtures.expiredToken}`);

    expect(res.status).toBe(401);
  });

  test("rejects tampered tokens", async () => {
    const res = await request(app)
      .get("/api/profile")
      .set("Authorization", `Bearer ${fixtures.tamperedToken}`);

    expect(res.status).toBe(401);
  });
});

Assertions that catch auth regressions

The useful assertions are boring and specific:

  • unauthorized requests return 401
  • authenticated but underprivileged requests return 403
  • cross-account reads return 404 or 403, depending on policy
  • expired and wrong-audience tokens never pass

That mix catches more bugs than a single “token valid” test ever will.

Fixes that belong on the backend

The backend fixes are straightforward:

  • verify the signature before reading claims
  • pin allowed algorithms
  • check exp, iss, and aud
  • keep authorization in route handlers, not just middleware
  • validate object ownership on every sensitive read or write
  • use short-lived access tokens with a real refresh strategy
  • keep revocation server-side when logout must matter
💪

If you only have time for one regression test, write the cross-account access test. It catches the difference between “logged in” and “allowed.”

Conclusion

JWT in Node.js is easy to get working and easy to get wrong. The useful tests are the ones that prove the server rejects tampered tokens, enforces claim checks, and still authorizes each route correctly after authentication. If those three layers hold up, the implementation is usually in good shape.

Share this post

More posts

Comments