
Testing JWT Implementation in Node.js APIs for Common Pitfalls
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, andaudare 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
Bearerwithout 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.
| Layer | What to test | Typical bug |
|---|---|---|
| Auth middleware | Signature, exp, iss, aud | Token accepted too broadly |
| Route handler | Role and ownership | Any authenticated user can read another user’s data |
| Admin endpoints | Elevated permissions | UI 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:
- revoked tokens stop working when they should
- refresh tokens cannot be reused forever
- 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
404or403, 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, andaud - 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.


