Authentication bugs rarely look dramatic in local development. A login works, the dashboard loads, and everyone moves on. Then production adds HTTPS, preview domains, server actions, multiple runtimes, cached pages, password reset emails, and users who do strange things at 2 a.m.
This Next.js authentication checklist is the review I want before shipping a real app. It assumes you already know the basics of Next.js and React. If you're still building that foundation, start with a practical Next.js roadmap from zero to production, then come back when auth is no longer a demo feature.
Start with the auth boundary
Before picking a library, write down what counts as protected data in your app. A billing page, admin panel, account settings page, API route, and server action may all need different checks.
Don't treat "logged in" as the whole model. Most production apps need at least two questions:
- Who is this user?
- What is this user allowed to do here?
Authentication answers the first question. Authorization answers the second. Mixing them makes code easier to write at first, then harder to audit later.
For a Next.js app, list every place that accepts or returns private data:
- Server Components that read user-specific data
- Route Handlers under
app/api - Server Actions that mutate data
- Client Components that call internal APIs
- Middleware used for redirects or coarse route checks
- Background jobs or webhooks that touch user records
If a route isn't on the list, nobody will remember to protect it.
Choose a session strategy on purpose
Auth.js documents two common session strategies: JWT sessions and database sessions. Neither is magic.
JWT sessions can be fast because the server doesn't need a database lookup for every request. The tradeoff is revocation. If a token is valid until Friday, the server needs an extra plan to kill it on Wednesday.
Database sessions are easier to revoke because the server can delete or rotate a session record. The cost is a database read, or a cache layer if your traffic needs it.
For admin-heavy apps, billing apps, and anything where account lockout must work quickly, I prefer database sessions. JWT sessions can be fine for simpler apps, but only if expiry, rotation, and token size are reviewed instead of copied from a tutorial.
Checklist:
- Pick JWT or database sessions deliberately.
- Set a short enough session lifetime for your risk level.
- Rotate sessions after sign in, privilege changes, and password changes.
- Revoke all sessions when a password is reset or an account is compromised.
- Store only the minimum data needed in a session.
Lock down cookies before writing UI
MDN's cookie guidance is blunt: cookies are sent automatically with matching requests. That convenience is why cookie settings need a real review.
For session cookies in production, use these defaults unless you have a specific reason not to:
HttpOnlyso JavaScript can't read the session cookie.Secureso browsers send it only over HTTPS.SameSite=Laxfor most app sessions.SameSite=Strictfor very sensitive flows if it doesn't break login redirects.- A narrow
Path, usually/for the app. - No broad
Domainunless subdomains truly need the same session.
Avoid storing access tokens in localStorage. It makes client-side code convenient, but it also gives injected scripts a clean place to steal tokens. Cookies with HttpOnly don't remove every risk, but they close one common door.
Also set cookie names carefully. Prefixes such as __Host- can force safer browser behavior when used correctly: secure origin, no Domain, and path set to /.
Protect routes in the right layer
Next.js gives you several places to check auth. Use them for different jobs.
Middleware is useful for broad redirects. For example, redirect anonymous users away from /dashboard. But middleware should not be your only security check.
Server Components are a good place to load the current user and avoid rendering private UI for anonymous users. Route Handlers and Server Actions still need their own checks because users can call them directly.
A practical pattern:
// lib/authz.ts
export async function requireUser() {
const session = await getSession()
if (!session?.user?.id) {
throw new Error("Unauthorized")
}
return session.user
}Then use that helper inside every data mutation and private read path. It feels repetitive. That's better than trusting that a redirect ran earlier.
Checklist:
- Use middleware for coarse navigation rules only.
- Check auth again in Route Handlers.
- Check auth again in Server Actions.
- Keep permission logic in shared server-only helpers.
- Never trust a user ID sent from the browser when the session already has one.
Separate login, signup, and recovery risks
OWASP's authentication guidance spends a lot of time on the unglamorous parts: error messages, password policy, recovery flows, and brute force protection. That's where many real bugs sit.
Your login form should not reveal whether the email or password was wrong. Use one generic error. It feels less helpful, but it prevents account enumeration.
Password reset deserves the same treatment. Always show the same response after a reset request, whether the account exists or not. The email can contain a short-lived, single-use token. The page that accepts the token should rotate the user's sessions after the password changes.
For passwords, follow current guidance rather than old myths. Allow long passwords. Don't force weird composition rules that push people toward predictable patterns. Check common or breached passwords if your auth provider supports it.
Checklist:
- Use generic login and password reset errors.
- Rate limit login, signup, reset, and verification endpoints.
- Use short-lived, single-use recovery tokens.
- Rotate sessions after password reset.
- Require re-authentication before changing email, password, MFA, or billing settings.
Add CSRF protection where cookies are used
If your session lives in a cookie, the browser may send it automatically on cross-site requests depending on your settings. SameSite=Lax helps, but it shouldn't be your only plan for sensitive state changes.
For form posts, Server Actions, and Route Handlers that mutate data, add CSRF protection when a browser can trigger the request. Auth libraries often include this for their own routes, but your custom endpoints still need review.
Common options include synchronizer tokens, double-submit cookies, or framework-supported server action protections. The exact choice depends on your stack. The point is simpler: don't assume "it uses POST" means it is safe.
Checklist:
- Identify every browser-triggered mutation.
- Confirm CSRF handling for login provider callbacks.
- Add CSRF tokens to custom forms if needed.
- Keep destructive actions behind POST or another non-GET method.
- Recheck CORS settings so another origin can't read private responses.
Watch caching and redirects in the App Router
Auth in the App Router has one trap that still catches people: caching. A page that reads session-specific data must not be treated like public static content.
If a Server Component reads cookies, headers, or a session, make sure the route isn't treated as public static output. Next.js APIs such as cookies() and headers() influence rendering behavior, but your own data layer and fetch calls still deserve attention.
Be careful with shared fetches too. A request for /api/account should not end up cached across users because someone copied a public data-fetching pattern.
Redirects also need review. After login, only redirect to safe internal paths. Don't accept any next parameter and send users there blindly. Open redirects look minor until they are used in phishing flows.
Checklist:
- Mark user-specific pages as uncached when needed.
- Avoid shared caches for account data.
- Use
cache: "no-store"for sensitive fetches when appropriate. - Validate post-login redirect targets.
- Keep public pages and private pages clearly separated.
Treat providers and secrets like production systems
OAuth providers make auth easier, but they add configuration risk. The callback URL must match production exactly. Preview deployments need their own policy. A loose wildcard can turn a testing shortcut into an account takeover path.
Keep provider secrets in environment variables, never in the repository. Rotate them when a developer leaves, when a secret leaks into logs, or when you move between auth providers.
For Next.js deployments, check that the production app uses production secrets. This sounds obvious until a preview value gets copied into production during a late-night deploy.
Checklist:
- Use exact callback URLs for each provider.
- Keep development, preview, and production secrets separate.
- Rotate OAuth client secrets on a schedule or after access changes.
- Disable unused providers in production.
- Review scopes so providers request only what the app uses.
Log enough to investigate, not enough to leak
Auth logs are useful when users are locked out or when an account is attacked. They are also a place where teams accidentally store reset tokens, full cookies, or provider payloads.
Log events, not secrets. A good auth log can record sign in success, sign in failure, password reset requested, password changed, MFA changed, session revoked, and provider linked. It should include a user ID when known, an IP address if your privacy policy allows it, and a timestamp.
Don't log raw tokens. Don't log session cookies. Don't log full request headers. If you need correlation, generate an internal request ID.
Checklist:
- Log auth events with stable event names.
- Redact cookies, tokens, passwords, and provider secrets.
- Alert on repeated failures and suspicious provider linking.
- Keep audit logs separate from noisy application logs.
- Test that errors don't print secrets in production.
Run this before launch
A final production review should be boring.
Use this shorter launch checklist when the app is almost ready:
- Create a fresh user and complete signup.
- Sign out, then confirm private routes redirect correctly.
- Call private Route Handlers without a session and confirm they fail.
- Call private Server Actions without a session and confirm they fail.
- Reset the password and confirm old sessions stop working.
- Change a role or permission and confirm the change applies quickly.
- Try an unsafe post-login redirect and confirm it is rejected.
- Inspect session cookies in the browser and confirm
HttpOnly,Secure, andSameSite. - Check production logs for leaked tokens or cookies.
- Confirm OAuth callbacks match the production domain.
If this list finds bugs, that's good. Better now than after a customer pastes a broken reset link into support chat.



