Menu

JWT in Node.js: Sign, Verify and Protect Express Routes

A JWT is a signed token of three base64url parts that a server issues at login and checks on every request. In Node.js you sign and verify one with the jose or jsonwebtoken package and a secret key.

This page includes runnable editors. Edit, run, and see output instantly.

A JWT (JSON Web Token) is a string of three base64url parts, header.payload.signature, that a server gives a user at login and checks on every later request. The signature proves the server issued the token and nobody changed it, but the payload is only encoded, not encrypted, so anyone can read it. In Node.js you create one with new SignJWT(payload).sign(secret) from the jose package and check it with jwtVerify(token, secret).

It prints 3 parts, then user-42 admin: jwtVerify checked the signature and the expiry and returned the payload. Change the secret passed to jwtVerify by one character and run it again: verification fails with signature verification failed.

What is inside a JWT

The header and the payload are JSON, encoded with base64url. Decoding them needs no key, which is why a JWT must never hold a password or anything else secret.

The first two lines print {"alg":"HS256"} and {"name":"Ada","role":"editor"}, readable by anyone holding the token. The signature (43 characters for HS256) is what stops them from editing it: change editor to admin and the signature no longer matches.

PartContainsSecret?
HeaderThe algorithm, such as HS256No, readable
PayloadClaims: sub (user id), exp (expiry), iat (issued at), your own fieldsNo, readable
SignatureHMAC or RSA signature of the first two partsNeeds the key to create

Expiry and failed verification

setExpirationTime('15m') writes an exp claim, and jwtVerify rejects a token past it. Every rejection is an error with a code, so you can tell an expired token from a forged one.

The first check fails with ERR_JWT_EXPIRED and the second, whose signature was changed, with ERR_JWS_SIGNATURE_VERIFICATION_FAILED. Both mean the same to your API (answer 401), but logging the code helps when debugging.

Protect an Express route with a Bearer token

The client logs in once, keeps the token and sends it in the Authorization header as Bearer <token>. A middleware verifies it and puts the payload on req.user for the route.

The first request, GET /me with no token, gets 401 and Missing token. Send POST /login with the body {"username": "ada", "password": "lovelace"}, copy the token from the response, then send GET /me again with the header Authorization: Bearer <token>: the answer is {"id":"user-1","name":"ada"}.

The jsonwebtoken package

jsonwebtoken (npm install jsonwebtoken) is the other common choice. Its sign and verify are synchronous (with optional callbacks) instead of returning promises:

const jwt = require('jsonwebtoken');

const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
    subject: 'user-42',
    expiresIn: '1h',
});

const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid

Use jose for new code: it has no dependencies and also runs in edge runtimes and browsers. Tokens from either library are standard JWTs, so one can verify what the other signed.

Common mistakes

Using decode instead of verify. jwt.decode and decodeJwt read the payload without checking the signature, so a forged token passes. Only trust what verify returns.

A short or committed secret. A guessable HS256 secret lets attackers sign their own tokens. Use at least 32 random bytes, read it from process.env, and never commit it.

Tokens that never expire. Without exp, a leaked token works forever. Always set an expiry.

Frequently Asked Questions

Where should the client store a JWT?

In a browser app, an HttpOnly, Secure cookie is safest, because page scripts cannot read it. localStorage is simpler but any script on the page, including an injected one, can read the token. Mobile apps use the platform's secure storage.

How do I log a user out with JWTs?

The server cannot cancel a token it already signed; it stays valid until it expires. Delete it on the client, keep access tokens short-lived, and if you need instant logout, keep a server-side list of revoked token ids (jti) and check it on every request.

HS256 or RS256?

HS256 uses one shared secret for signing and verifying, which suits a single backend. RS256 signs with a private key and verifies with a public key, so other services can check tokens without being able to create them.

How long should a JWT be valid?

Access tokens are usually valid for 5 to 60 minutes. For longer sessions, pair them with a refresh token stored more carefully, which the client exchanges for a new access token.

Coddy programming languages illustration

Learn to code with Coddy

Get started