Auth patterns cheatsheet. Pair with Cheatsheet 06 — Authentication .

JWT vs DB sessions

JWT (stateless):

  • ✅ No DB hit per request.
  • ✅ Easy multi-instance.
  • ❌ Can’t revoke until expiry.
  • ❌ Permissions stale until refresh.

DB sessions:

  • ✅ Revoke instantly.
  • ✅ Per-session metadata.
  • ❌ DB hit per request (mitigate with Redis).

Hybrid: JWT with short TTL + DB for refresh tokens.

Sliding session refresh

async function getSession() {
  const token = (await cookies()).get("session")?.value;
  if (!token) return null;
  
  const payload = await verify(token);
  if (!payload) return null;
  
  // Refresh if expiring soon
  if (payload.exp - Date.now() / 1000 < 60 * 60 * 24) {
    const newToken = await sign({ userId: payload.userId });
    (await cookies()).set("session", newToken, { httpOnly: true });
  }
  
  return payload;
}

Route protection — layout

// app/dashboard/layout.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function Layout({ children }) {
  const session = await auth();
  if (!session) redirect(`/login?from=/dashboard`);
  return children;
}

Applies to all dashboard/* pages.

Route protection — middleware

// middleware.ts
import { auth } from "@/auth";

export default auth((req) => {
  const path = req.nextUrl.pathname;
  if (path.startsWith("/dashboard") && !req.auth) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
});

export const config = {
  matcher: ["/((?!api|_next|favicon).*)"],
};

Middleware runs at edge, fast but limited (no DB calls usually).

RBAC (role-based access)

type Role = "admin" | "editor" | "viewer";

function canEdit(role: Role) { return role === "admin" || role === "editor"; }
function canDelete(role: Role) { return role === "admin"; }

// In server component
async function Page() {
  const session = await auth();
  if (!session) redirect("/login");
  if (!canEdit(session.user.role)) return <p>No access</p>;
  return <EditForm />;
}

// In server action
"use server";
export async function deletePost(id: number) {
  const session = await auth();
  if (!session || !canDelete(session.user.role)) {
    throw new Error("Forbidden");
  }
  await db.post.delete({ where: { id } });
}

Permission helpers

// lib/perms.ts
export async function requireAuth() {
  const session = await auth();
  if (!session) throw new Error("Unauthorized");
  return session;
}

export async function requireRole(role: Role) {
  const session = await requireAuth();
  if (session.user.role !== role) throw new Error("Forbidden");
  return session;
}
"use server";
export async function adminAction() {
  await requireRole("admin");
  // ...
}

OAuth provider

// Auth.js
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

providers: [
  GitHub({ clientId: ..., clientSecret: ... }),
  Google({ clientId: ..., clientSecret: ... }),
],

Set redirect URLs in provider dashboards: https://yourdomain/api/auth/callback/<provider>.

import Email from "next-auth/providers/email";

Email({
  server: { host: ..., port: ..., auth: { user: ..., pass: ... } },
  from: "[email protected]",
})

User enters email, gets clickable link, lands authenticated.

CSRF

Auth.js handles CSRF for sign in/out. For your own forms with cookies:

// Compare cookie token with header token, or use sameSite: "lax" cookies

sameSite: "lax" covers most CSRF.

Password hashing

import bcrypt from "bcryptjs";

const hash = await bcrypt.hash(plain, 12);
const ok = await bcrypt.compare(plain, hash);

Or Argon2:

import argon2 from "argon2";
const hash = await argon2.hash(plain);
const ok = await argon2.verify(hash, plain);

Argon2 > bcrypt for new projects.

Rate-limit auth

Brute-force protection. See middleware cheatsheet for Upstash example.

const { success } = await rl.limit(`login:${email}`);
if (!success) throw new Error("Too many attempts");

Audit log

async function login(email: string, password: string) {
  // ... verify ...
  await db.auditLog.create({
    data: {
      event: "login",
      userId: user.id,
      ip: req.headers.get("x-forwarded-for"),
      ua: req.headers.get("user-agent"),
    },
  });
}

Useful for compliance + incident investigation.

Session in client

"use client";
import { useSession } from "next-auth/react";

const { data: session, status } = useSession();

Re-validates on focus by default.

Logout everywhere

DB sessions: delete all session rows for user.

JWT: rotate signing key (invalidates all tokens), or maintain a per-user tokenVersion:

async function verify(token: string) {
  const payload = await jwtVerify(token, key);
  const user = await db.user.find(payload.userId);
  if (user.tokenVersion !== payload.version) return null;
  return payload;
}

To invalidate, increment tokenVersion.

Common mistakes

  • Trusting client-side role checks alone — always re-verify on server.
  • Long-lived JWTs without revocation strategy.
  • Storing session token in localStorage (XSS-readable). Use httpOnly cookies.
  • Plaintext passwords or weak hash (MD5, SHA1).
  • Missing rate limit on login → brute force.

Read this next

If you want my auth + RBAC starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .