Next.js
8 minutes readAuthentication Is Not Authorization: Securing a Next.js App
Build a security-first mental model for sessions, roles, server actions, route handlers, and database access in a real application.

Authentication answers “who is making this request?” Authorization answers “is this identity allowed to perform this action on this resource?” A signed-in user is not automatically allowed to read every record or call every mutation.
Authentication answers “who is making this request?” Authorization answers “is this identity allowed to perform this action on this resource?” A signed-in user is not automatically allowed to read every record or call every mutation.
Session handling
Use a maintained authentication library for session creation, rotation, expiration, and cookie handling. Keep session cookies HttpOnly, Secure in production, and choose a SameSite policy that fits your login flows. Never put long-lived secrets in localStorage or expose server-only keys to client bundles.
Enforce authorization on the server
Enforce access at the server boundary that performs the operation. Hiding an admin link or disabling a button improves the interface, but it is not an authorization check. Every Server Action and Route Handler must validate the current session and the requested resource.
Create a reusable guard
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
export async function requireAdmin() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect('/login');
if (session.user.role !== 'admin') redirect('/unauthorized');
return session.user;
}A reusable guard makes the policy explicit. The exact session API and role model depend on your auth setup; treat this snippet as a pattern and adapt it to your installed Better Auth version and your application types.
Protect every mutation
'use server';
export async function deleteUser(userId: string) {
const actor = await requireAdmin(); // Never trust a role sent by the browser
if (actor.id === userId) throw new Error('Cannot delete your own account');
await db.user.delete({ where: { id: userId } });
return { success: true };
}Call the guard inside each sensitive mutation, not only in the page that links to it. A user can invoke a Server Action directly. Also validate the target resource: role checks alone do not prevent a user from editing another user’s record if ownership rules are missing.
Use database invariants
Use database constraints for invariants that must hold even if application code has a bug: unique emails, foreign keys, and valid relationships. For multi-tenant applications, scope queries by tenant and resource ownership rather than fetching globally and hoping the UI filters correctly.
Protect the surrounding flows
Review CSRF protections, rate limits for login and OTP endpoints, password-reset token expiry, and generic error messages that do not disclose whether an account exists. Log security-relevant events without logging passwords, raw tokens, or full session cookies.
A decision checklist
Test anonymous access, a normal user attempting an admin action, one user requesting another user’s resource, expired sessions, and repeated requests. Security is a property of the server-side data path, not the appearance of the interface.