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.
JWT structure
Section titled “JWT structure”A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWxpY2UiLCJpYXQiOjE3MDUzNDU2MDAsImV4cCI6MTcwNTQzMjAwMH0.abc123signatureThree 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.
Installing jsonwebtoken
Section titled “Installing jsonwebtoken”npm install jsonwebtokennpm install --save-dev @types/jsonwebtokenSigning a token
Section titled “Signing a token”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.
Verifying a token
Section titled “Verifying a token”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
Reading the token from a request
Section titled “Reading the token from a request”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...'Token expiry
Section titled “Token expiry”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.
What to put in the payload
Section titled “What to put in the payload”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.
Exercise
Section titled “Exercise”- Create
src/utils/jwt.tswithsignTokenandverifyToken. - Add
JWT_SECRETto your.envfile (any long random string). - Write a quick test: sign a token, decode it with
jwt.decode()to inspect the payload, then verify it. - What happens when you call
verifyTokenwith an expired token? (SetexpiresIn: '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.