Skip to content

JSON Web Tokens

A JSON Web Token (JWT) is a compact, self-contained way to securely transmit information between parties. The Bulletin API issues a JWT on login; the client includes it in every subsequent request.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWxpY2UiLCJpYXQiOjE3MDUzNDU2MDAsImV4cCI6MTcwNTQzMjAwMH0.abc123signature

Three parts separated by dots: header.payload.signature

Header (base64-encoded JSON):

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

The signing algorithm.

Payload (base64-encoded JSON):

{ "userId": 1, "username": "alice", "iat": 1705345600, "exp": 1705432000 }

The claims — data about the user. iat = issued at, exp = expiry timestamp.

Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

The signature is created by hashing the header+payload with a secret key. This prevents tampering — if anyone changes the payload, the signature won’t match.

Terminal window
npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken
src/utils/jwt.ts
import jwt from 'jsonwebtoken'
import { config } from '../config.js'
export interface TokenPayload {
userId: number
username: string
}
export function signToken(payload: TokenPayload): string {
return jwt.sign(payload, config.jwtSecret, { expiresIn: '24h' })
}

expiresIn accepts strings like '24h', '7d', '1h' or a number of seconds.

export function verifyToken(token: string): TokenPayload {
return jwt.verify(token, config.jwtSecret) as TokenPayload
}

jwt.verify throws if:

  • The token is malformed
  • The signature doesn’t match (tampered token)
  • The token has expired

The standard way to send a JWT in an API request is the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Extract it:

const authHeader = req.headers.authorization // 'Bearer eyJ...'
const token = authHeader?.split(' ')[1] // 'eyJ...'

Short-lived tokens are more secure — if a token is stolen, it’s only valid for a limited time:

  • '15m' — very short, requires frequent re-auth (used with refresh tokens)
  • '24h' — one day — good default for learning/development
  • '7d' — one week — reasonable for many consumer apps

The Bulletin API uses '24h'. After 24 hours, users log in again.

The payload is readable by anyone (it’s base64-encoded, not encrypted). Never include sensitive data like passwords.

Good payload for Bulletin:

{ userId: 1, username: 'alice' }

This is enough for the API to know who’s making each request without a database lookup.

  1. Create src/utils/jwt.ts with signToken and verifyToken.
  2. Add JWT_SECRET to your .env file (any long random string).
  3. Write a quick test: sign a token, decode it with jwt.decode() to inspect the payload, then verify it.
  4. What happens when you call verifyToken with an expired token? (Set expiresIn: '1s' and wait a second.)
  • A JWT is header + payload + signature — verifiable without a database.
  • jwt.sign(payload, secret, { expiresIn }) creates a signed token.
  • jwt.verify(token, secret) returns the payload or throws if invalid/expired.
  • Send the token in Authorization: Bearer <token> headers.
  • Never put sensitive data in the payload — it’s base64 but not encrypted.