---
title: "09 — Authentication — JWT and Passport.js"
uid: authentication-tokens
tags: ["auth", "security", "nodejs", "passport", "authentication", "jwt", "roadmap:nodejs"]
excerpt: "JWT is the signed, stateless what-you-carry; Passport is the how-you-checked-it middleware. Not alternatives — two halves of one flow."
date: 2026-08-13T03:27:56+0000
source: https://www.aveshina.my.id/en/blog/authentication-tokens
---

"Just log them in" was my auth mental model, and it fused two different jobs. The model that finally stuck splits into two pieces: **JWT is a signed, stateless token for proving identity across requests, and Passport.js is the pluggable middleware that handles the messy parts of actually verifying that identity.** They are not alternatives — JWT is the what-you-carry, Passport is the how-you-checked-it [1].

The framing that landed for me is the stateless-then-stateful split: a token is a self-contained credential, and the verification step is where the real auth logic lives.

## The problem: HTTP is stateless

Every HTTP request stands alone — the server remembers nothing between requests by default. So if a user logs in on request one, the server needs a way to know, on request two, that this is still them. Two broad strategies exist:

- **Server-side sessions** — the server stores a record keyed by a random id, sends the id in a cookie, and looks it up on each request. The server keeps state; revoking is trivial.
- **Tokens** — the server hands the client a signed credential (the token) that the client includes on every request. The server verifies the signature and reads the claims; no lookup, no stored state.

JWT is the dominant token format. The trade-off versus sessions is real: tokens are self-contained (good for distributed systems), but they cannot be cleanly revoked before they expire (bad for "log out everywhere"), because the server does not track them. Picking the model is a statelessness-versus-revocation question.

## JWT: anatomy of a token

A JSON Web Token is three base64-encoded parts joined by dots — header.payload.signature [1][2]. The **header** names the algorithm. The **payload** is a set of **claims**: statements about the subject (user id, roles, expiry). The **signature** is the header and payload signed with a secret or private key, so any tampering with the first two parts invalidates the third.

```figure
<svg viewBox="0 0 740 200" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="JWT structure: three base64 parts joined by dots. Header (alg and typ), Payload (claims like sub, role, exp), and Signature (HMAC of header.payload with the secret). A tampered payload would mismatch the signature.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <rect x="20" y="40" width="200" height="100" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="62" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">HEADER</text>
    <text x="120" y="82" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">{ "alg": "HS256",</text>
    <text x="120" y="98" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">  "typ": "JWT" }</text>
    <text x="120" y="124" font-size="9" font-style="italic" fill="#475569" text-anchor="middle">base64-encoded</text>

    <text x="230" y="92" font-size="20" font-weight="700" fill="#64748b" text-anchor="middle">.</text>

    <rect x="245" y="40" width="240" height="100" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="365" y="62" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">PAYLOAD (claims)</text>
    <text x="365" y="82" font-size="10" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">{ "sub": "42",</text>
    <text x="365" y="98" font-size="10" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">  "role": "admin",</text>
    <text x="365" y="114" font-size="10" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">  "exp": 1735689600 }</text>

    <text x="495" y="92" font-size="20" font-weight="700" fill="#64748b" text-anchor="middle">.</text>

    <rect x="510" y="40" width="210" height="100" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="615" y="62" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">SIGNATURE</text>
    <text x="615" y="86" font-size="10" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">HMAC-SHA256(</text>
    <text x="615" y="102" font-size="10" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">  base64(header) + "."</text>
    <text x="615" y="118" font-size="10" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">  + base64(payload), secret)</text>

    <text x="370" y="170" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">verify = re-sign header.payload with the secret and compare</text>
  </g>
</svg>
```

Two properties follow from that structure. **The token is not encrypted** — anyone with the token can base64-decode the payload and read it. The signature only guarantees *integrity* (it was not tampered with) and *origin* (it was signed by whoever holds the secret), not confidentiality. Sensitive data does not go in the payload. And **the token is verifiable without a lookup** — the server recomputes the signature from header + payload using its secret and checks that it matches. No database hit, which is the whole appeal for distributed systems [1].

## Issuing and verifying with jsonwebtoken

The jsonwebtoken package is the standard library for this [3].

```
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET;

// issue — after verifying credentials
const token = jwt.sign({ sub: '42', role: 'admin' }, SECRET, { expiresIn: '1h' });

// verify — on each authenticated request
try {
  const payload = jwt.verify(token, SECRET);
  // payload.sub === '42', payload.exp checked automatically
} catch (err) {
  // invalid signature or expired
}
```

The pattern: on login, issue a short-lived token; on each request, verify it in middleware and attach the decoded claims to the request. The exp claim is what makes tokens self-expire — jwt.verify rejects an expired token automatically. The discipline I keep: short expiry (minutes to an hour), keep the secret in process.env (never in code), and never put anything in the payload I would not want a curious user to read.

## Passport.js: pluggable verification

JWT handles the *carry a signed credential* half. The other half — *how do I verify the user actually is who they claim at login time* — is messier, because it involves checking passwords, talking to OAuth providers, validating one-time codes, and so on. **Passport.js** is authentication middleware for Express that abstracts this into **strategies** [4][5].

A strategy is a self-contained module that knows one way to authenticate: passport-local for username/password, passport-google-oauth20 for Google OAuth, passport-jwt for extracting and verifying a JWT on protected routes. Each strategy implements a verification function; Passport calls it and hands me the result.

```
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';

passport.use(new LocalStrategy(
  async (username, password, done) => {
    const user = await User.findOne({ username });
    if (!user || !(await user.checkPassword(password))) {
      return done(null, false);   // credentials wrong
    }
    return done(null, user);      // success
  }
));

// in the route
app.post('/login', passport.authenticate('local'), (req, res) => {
  const token = jwt.sign({ sub: req.user.id }, SECRET, { expiresIn: '1h' });
  res.json({ token });
});
```

The value of Passport is not any single strategy — it is the uniform interface. "Authenticate with Google" and "authenticate with a password" plug into the same passport.authenticate(...) call, and swapping one for the other is a configuration change rather than a rewrite. For a project with multiple login methods, that modularity pays for itself.

## How I use this

The model I keep is the stateless-then-stateful split, and it drives a few habits. I default to JWT for stateless APIs (and short expiry, since revocation is hard); I reach for server-side sessions when "log out everywhere" or instant revocation is a real requirement. I treat the JWT payload as readable by the client — no secrets in it — and rely on the signature for integrity. For the verification half, I use Passport when there is more than one auth method, because the strategy abstraction makes adding a provider a small change. And the secret always lives in process.env, rotated periodically, because a leaked secret means every token ever issued with it can be forged. The split — token is the carrier, Passport is the checker — is what keeps auth code organized.

## References

[1] "What is JWT?," Akana Blog. [Online]. Available: [https://www.akana.com/blog/what-is-jwt](https://www.akana.com/blog/what-is-jwt)

[2] "jsonwebtoken," npm. [Online]. Available: [https://www.npmjs.com/package/jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)

[3] "JWT Implementation," YouTube. [Online]. Available: [https://www.youtube.com/watch?v=mbsmsi7l3r4](https://www.youtube.com/watch?v=mbsmsi7l3r4)

[4] "Passport.js," passportjs.org. [Online]. Available: [https://www.passportjs.org/](https://www.passportjs.org/)

[5] "Passport.js Documentation," passportjs.org. [Online]. Available: [https://www.passportjs.org/docs/](https://www.passportjs.org/docs/)

```quiz
Q: A JWT's signature provides…
- confidentiality — it encrypts the payload so only the server can read it
- integrity and origin — proof the token was signed by the secret-holder and was not tampered with
correct: 1
explain: JWTs are not encrypted; the payload is base64 and readable by anyone. The signature guarantees the token came from the secret-holder and was not modified, not that its contents are private.

Q: The main advantage of JWT over server-side sessions is…
- it is more secure
- the server verifies the token without a database lookup, which suits distributed systems
correct: 1
explain: JWTs are self-contained — the server recomputes the signature from header+payload and needs no stored state. The trade-off is that revocation before expiry is hard because the server does not track tokens.

Q: In Passport.js, a "strategy" is…
- a configuration file for the whole app
- a self-contained module that implements one way to authenticate (local password, Google OAuth, JWT, etc.)
correct: 1
explain: Strategies are pluggable authentication modules with a uniform interface. passport.authenticate('google') and passport.authenticate('local') plug into the same call, making it easy to support multiple login methods.

Q: The JWT exp (expiry) claim exists so that…
- tokens are smaller
- tokens self-expire and jwt.verify rejects them after the expiry, limiting the blast radius of a stolen token
correct: 1
explain: Because JWTs cannot be cleanly revoked before they expire, keeping expiry short limits how long a stolen token remains useful. jwt.verify checks exp automatically.

Q: Where should the JWT signing secret live?
- Hardcoded in the source, committed to git
- In process.env, loaded from the environment, never committed
correct: 1
explain: A leaked secret lets anyone forge valid tokens. The secret is environment configuration — it lives in process.env, is never committed, and should be rotated periodically.
```
