Toggle theme D

When a user logs into an app, the server needs to remember who they are on subsequent requests. The old way was session IDs stored on the server—each request sent the session ID, the server looked it up, and knew who you were. JWT flips this around: instead of the server keeping track, the user holds a signed token that proves their identity. No server-side session storage needed.

What JWT Actually Is

JWT (JSON Web Token) is a compact, URL-safe way to represent claims between two parties. In authentication, it lets the server verify "this user is who they claim to be" without needing to store session data.

The token is a string that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsZXgiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It has three parts separated by dots: header, payload, and signature.

The Three Parts

Header - Contains the algorithm used to sign the token and the token type:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload - Contains the actual data (called "claims"). For authentication, you'd typically store the user ID here:

{
  "sub": "123",
  "name": "Alex",
  "role": "user",
  "iat": 1516239022
}

Signature - Takes the encoded header, encoded payload, and a secret key, then runs them through the algorithm specified in the header. This proves the token wasn't tampered with.

The magic: anyone can read the header and payload (they're just base64 encoded), but only the server can create valid signatures because only the server knows the secret.

The Login Flow

  1. User sends credentials (username/password) to the server

  2. Server validates them

  3. Server creates a JWT with the user info

  4. Server sends the token back to the client

const jwt = require('jsonwebtoken');

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  if (validateUser(username, password)) {
    const token = jwt.sign(
      { userId: 123, username },
      'your-secret-key',
      { expiresIn: '24h' }
    );
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

The client stores this token (usually in localStorage or a cookie) and sends it with every request.

Sending the Token

Clients typically send the token in the Authorization header:

fetch('/api/protected', {
  headers: {
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  }
});

Protecting Routes

On the server, you verify the token before allowing access:

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401);

  jwt.verify(token, 'your-secret-key', (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};

app.get('/api/protected', authenticateToken, (req, res) => {
  res.json({ message: 'You have access!', user: req.user });
});

If the token is missing or invalid, the request gets rejected. No database lookup needed—the signature verification proves the token is legitimate.

Why It Works

The server doesn't store anything. Every request carries everything needed to verify identity. This is called stateless authentication. Scale becomes easier because any server can verify any token—no session store needed, no sticky sessions required.

The tradeoff: you can't "log out" a JWT server-side because there's no central record. That's why short expiration times and token refresh mechanisms exist.


JWT isn't magic—just a signed JSON object that proves the user is who the payload says they are. The server signs it, the client holds it, and every request validates the signature. Once the flow clicks, building token-based auth is straightforward.