Sessions vs JWT

Two Session Strategies

JWT sessionDatabase session
Stored inEncrypted cookie on the browserSessions table; cookie has only an id
DB lookup per requestNoYes
Revoke instantly (logout everywhere)Hard (wait for expiry)Easy (delete the row)
Works in middleware/edgeYesNeeds a DB call
Auth.js defaultWithout an adapterWith 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 };
  }
}