Careeroza
Sessions vs JWT
Two Session Strategies
| JWT session | Database session | |
|---|---|---|
| Stored in | Encrypted cookie on the browser | Sessions table; cookie has only an id |
| DB lookup per request | No | Yes |
| Revoke instantly (logout everywhere) | Hard (wait for expiry) | Easy (delete the row) |
| Works in middleware/edge | Yes | Needs a DB call |
| Auth.js default | Without an adapter | With a database adapter (Prisma) |
How a Signed Token Works
Example
A minimal HS256 JWT with node:crypto. Auth.js actually encrypts its token (JWE), but the trust idea is the same: the server can detect any change.
import { createHmac } from 'node:crypto';
const SECRET = 'dev-secret-change-me';
const b64url = (s: string) => Buffer.from(s).toString('base64url');
function sign(payload: object): string {
const head = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
const body = b64url(JSON.stringify(payload));
const sig = createHmac('sha256', SECRET).update(head + '.' + body).digest('base64url');
return head + '.' + body + '.' + sig;
}
function verify(token: string, now: number): string {
const [head, body, sig] = token.split('.');
const expected = createHmac('sha256', SECRET).update(head + '.' + body).digest('base64url');
if (sig !== expected) return 'invalid signature';
const claims = JSON.parse(Buffer.from(body, 'base64url').toString()) as { sub: string; role: string; exp: number };
if (now >= claims.exp) return 'expired';
return 'ok: user ' + claims.sub + ' (' + claims.role + ')';
}
const token = sign({ sub: 'u_42', role: 'customer', exp: 1_800_000_000 });
console.log(token.split('.').map((p) => p.slice(0, 12) + '…').join('.'));
console.log(verify(token, 1_700_000_000));
const forged = token.split('.')[0] + '.' + b64url(JSON.stringify({ sub: 'u_42', role: 'admin', exp: 1_800_000_000 })) + '.' + token.split('.')[2];
console.log(verify(forged, 1_700_000_000));
console.log(verify(token, 1_900_000_000));Output
eyJhbGciOiJI….eyJzdWIiOiJ1….buNx7XiNcM9I… ok: user u_42 (customer) invalid signature expired
Adding the Role to the Session
// auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
session: { strategy: 'jwt' },
providers: [/* Google, Credentials as above */],
callbacks: {
jwt({ token, user }) {
if (user) token.role = (user as { role: string }).role; // only on sign-in
return token;
},
session({ session, token }) {
session.user.id = token.sub!;
session.user.role = token.role as 'customer' | 'admin';
return session;
},
},
});// types/next-auth.d.ts — tell TypeScript about the new fields
import 'next-auth';
declare module 'next-auth' {
interface Session {
user: { id: string; role: 'customer' | 'admin'; name?: string | null; email?: string | null; image?: string | null };
}
}