Careeroza
Sessions vs JWT
Two Ways to Remember a Logged-in User
| Server sessions | JWT (JSON Web Token) | |
|---|---|---|
| What the client holds | A random session id in a cookie | A signed token with user data (claims) |
| Where state lives | Server (Redis / DB) | In the token itself |
| Checking a request | Look up the session store | Verify the signature, no lookup |
| Logout / revoke | Easy: delete the session | Hard: token valid until it expires |
| Good for | Classic web apps | APIs used by mobile apps, SPAs, other services |
Most NestJS APIs use short-lived access tokens (JWT, 15 minutes) plus long-lived refresh tokens (7–30 days) that can be revoked. That combines the speed of JWT with the control of sessions.
What is Inside a JWT
A JWT is header.payload.signature, each part base64url-encoded. The payload is readable by anyone; the signature only proves it was not changed. Never put passwords or secrets in it.
Example
Signing and verifying an HS256 JWT with only node:crypto. This is what @nestjs/jwt (via the jsonwebtoken library) does for you:
import { createHmac, timingSafeEqual } from 'node:crypto';
const b64 = (obj: object) => Buffer.from(JSON.stringify(obj)).toString('base64url');
function sign(payload: Record<string, unknown>, secret: string, expiresInSec: number, now: number): string {
const body = { ...payload, iat: now, exp: now + expiresInSec };
const data = b64({ alg: 'HS256', typ: 'JWT' }) + '.' + b64(body);
return data + '.' + createHmac('sha256', secret).update(data).digest('base64url');
}
function verify(token: string, secret: string, now: number): Record<string, unknown> {
const [h, p, s] = token.split('.');
const expected = createHmac('sha256', secret).update(h + '.' + p).digest();
const given = Buffer.from(s, 'base64url');
if (given.length !== expected.length || !timingSafeEqual(given, expected)) throw new Error('invalid signature');
const payload = JSON.parse(Buffer.from(p, 'base64url').toString());
if (payload.exp <= now) throw new Error('jwt expired');
return payload;
}
const NOW = 1768471200; // 2026-01-15T10:00:00Z in seconds
const token = sign({ sub: 12, email: '[email protected]', roles: ['customer'] }, 'dev-secret', 900, NOW);
console.log('token parts:', token.split('.').map((x) => x.length));
console.log('verified:', verify(token, 'dev-secret', NOW + 60));
const tampered = token.split('.');
tampered[1] = b64({ sub: 12, email: '[email protected]', roles: ['admin'], iat: NOW, exp: NOW + 900 });
for (const [label, t, at] of [['tampered', tampered.join('.'), NOW], ['expired', token, NOW + 901]] as const) {
try { verify(t, 'dev-secret', at); } catch (e) { console.log(label + ':', (e as Error).message); }
}Output
token parts: [ 36, 123, 43 ]
verified: {
sub: 12,
email: '[email protected]',
roles: [ 'customer' ],
iat: 1768471200,
exp: 1768472100
}
tampered: invalid signature
expired: jwt expired