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>.
Magic link
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 .