Tutorial

Authentication with JWTs, Step by Step

Tutorial · Security

Cover: Authentication with JWTs, Step by Step

A JSON Web Token is a signed, self-contained piece of data a server hands a client after login, and trusts on every later request — without needing to check a session store each time.

What's actually inside a JWT

A JWT is three base64url-encoded segments joined by dots: header.payload.signature. The payload holds claims like the user's ID and an expiry time — readable by anyone, but not forgeable, because the signature is computed with a secret only the server knows.

{
  "sub": "user_42",
  "role": "editor",
  "exp": 1735689600
}

Issuing a token at login

const jwt = require('jsonwebtoken')

app.post('/login', (req, res) => {
  const user = authenticate(req.body.email, req.body.password)
  if (!user) return res.status(401).json({ error: 'Invalid credentials' })

  const token = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  )
  res.json({ token })
})

The secret must never be checked into source control or hardcoded — it should live in an environment variable, and rotating it invalidates every previously issued token at once.

Protecting a route

function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization
  if (!authHeader) return res.status(401).json({ error: 'No token' })

  try {
    const payload = jwt.verify(authHeader.split(' ')[1], process.env.JWT_SECRET)
    req.user = payload
    next()
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' })
  }
}

app.get('/api/profile', requireAuth, (req, res) => {
  res.json({ userId: req.user.sub })
})

jwt.verify checks the signature and expiry in one call, throwing if either fails — which is why it's wrapped in try/catch.

Never store sensitive data in the payload. The payload is only base64-encoded, not encrypted — anyone with the token can decode and read it. Passwords, full card numbers, or anything truly secret has no business inside a JWT.

Where the token lives on the client

A JWT stored in localStorage is readable by any JavaScript on the page, including an injected XSS script. An httpOnly cookie is safer against that specific attack but needs CSRF protection instead. Neither option is risk-free — pick based on which threat matters more for your app, and don't treat JWTs as inherently secure by default.

Comments