JWT Authentication in Express

JWT authentication is a stateless way to prove who a user is: the server signs a token at login, the client sends it back on every request, and a middleware verifies the signature before letting the request reach a protected route.

Learn JWT Authentication in Express in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll sign and verify tokens with jsonwebtoken, hash passwords with bcrypt, build a reusable auth middleware that reads the Bearer header, protect routes, and understand expiry and refresh tokens — the exact pattern real Express APIs use to lock things down.

What You'll Learn in This Lesson

1️⃣ Signing & Verifying Tokens

Authentication with JWTs comes down to two functions. jwt.sign(payload, secret, options) takes the data you trust (a user id, a role), bundles it with a header and an expiry, and produces a signed string. jwt.verify(token, secret) does the reverse: it checks the signature against your secret and the expiry, and either returns the decoded payload or throws.

The magic is the signature . Because it's computed from the payload and your secret, nobody can change the payload (say, bump their role to admin ) without invalidating the token. That's why JWTs are stateless — the server doesn't store sessions; it just re-checks the signature on every request.

2️⃣ Hashing Passwords with bcrypt

Before you can issue a token at login, you have to verify the password — and you must never store passwords in plain text. bcrypt turns a password into a slow, salted hash. You store the hash, and at login you call bcrypt.compare(candidate, storedHash) , which returns true or false .

The second argument to hash is the cost factor . A value of 10 means bcrypt does 2¹⁰ rounds of work — slow enough to make brute-forcing painful, fast enough for a login. Notice the wrong password returns false without you ever decrypting anything.

3️⃣ Login + an Auth Middleware

Now put it together. A login function checks credentials and signs a token. An auth middleware runs before protected routes: it reads the Authorization: Bearer <token> header, verifies the token, and attaches the decoded claims to the request. The runnable version below uses plain objects so you can see the logic without a server.

Here's the same idea wired into a real Express app, with registration, login, a reusable requireAuth middleware, and a protected /me route. This is the shape you'll copy into your own projects:

Tokens don't last forever, and that's the point. expiresIn stamps an expiry into the token; once it passes, verify throws a TokenExpiredError . A token signed with the wrong secret throws invalid signature . Short-lived access tokens limit the damage of a stolen one — and a longer-lived refresh token lets the client quietly get a fresh access token without re-entering a password.

Your turn. Fill in the two ___ blanks, follow the 👉 hints, then run it.

No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output against the example in the comments. This ties together signing, the Bearer header, and verification in one short program.

📋 Quick Reference — JWT & bcrypt

Practice quiz

Is a standard JWT encrypted or signed?

  • Encrypted, so the payload is hidden
  • Both encrypted and compressed
  • Signed, so anyone can read the payload
  • Neither; it is plain text only

Answer: Signed, so anyone can read the payload. A JWT is signed, not encrypted. Anyone with the token can decode and read the payload, so never put secrets in it.

What does jwt.sign(payload, secret, options) return?

  • A compact URL-safe token string
  • A boolean
  • The decoded payload object
  • A Buffer of random bytes

Answer: A compact URL-safe token string. sign() produces a compact, URL-safe string made of three base64url parts separated by dots.

What does jwt.verify(token, secret) do?

  • Only decodes without checking anything
  • Generates a new token
  • Hashes a password
  • Checks the signature and expiry, returning the payload or throwing

Answer: Checks the signature and expiry, returning the payload or throwing. verify() validates the signature and expiry; it throws if the token was tampered with or has expired.

How many dot-separated parts does a JWT have?

  • Two: header and body
  • Three: header, payload, signature
  • One single block
  • Four parts

Answer: Three: header, payload, signature. A JWT has three base64url parts: header, payload, and signature, separated by dots.

Why hash passwords with bcrypt instead of storing them plainly?

  • A leak would expose every account, and bcrypt is slow and salted
  • bcrypt makes login faster
  • It encrypts the database file
  • Plain passwords are fine if short

Answer: A leak would expose every account, and bcrypt is slow and salted. Never store plain passwords. bcrypt is deliberately slow and adds a per-password salt, making guesses expensive.

What does bcrypt.compare(candidate, storedHash) return?

  • The original password
  • A new hash
  • Whether the candidate matches the stored hash
  • The salt used

Answer: Whether the candidate matches the stored hash. compare() re-hashes the candidate with the stored salt and tells you if it matches; never use === on passwords.

In an Authorization header, what scheme prefixes the token?

  • Basic
  • Bearer
  • Token
  • JWT

Answer: Bearer. The common pattern is 'Authorization: Bearer <token>', so the middleware splits off the Bearer scheme.

What error name does jwt.verify throw for an expired token?

  • ExpiredJwtException
  • AuthError
  • TimeoutError
  • TokenExpiredError

Answer: TokenExpiredError. An expired token causes verify() to throw a TokenExpiredError with message 'jwt expired'.

What happens if a token is signed with a different secret than verify uses?

  • It still verifies fine
  • Verification fails with an invalid signature error
  • The payload is returned anyway
  • Node crashes

Answer: Verification fails with an invalid signature error. A wrong secret produces 'invalid signature', which is what stops attackers from forging tokens.

Why use short-lived access tokens plus a refresh token?

  • It removes the need for any secret
  • It makes tokens unencrypted
  • A stolen access token is useful only briefly, and refresh tokens can be revoked
  • It avoids using HTTPS

Answer: A stolen access token is useful only briefly, and refresh tokens can be revoked. Short-lived access tokens limit damage if stolen; a separately stored refresh token mints new ones and can be revoked.