Next.js

Next.js Authentication👨‍💻

Authentication in Next.js sits at the intersection of server and client rendering, which makes it both powerful and tricky to get right. You have proxy (middleware) that can intercept requests before they hit your routes, server components that can check sessions without shipping auth logic to the browser, and server actions that need protection too. The question is never "can I add auth?" but "where should each check live?"

The short answer: proxy handles coarse-grained redirects (is the user logged in?), server components handle fine-grained authorization (does this user have the right role?), and Auth.js (NextAuth v5) ties it all together with OAuth providers, session management, and callbacks. If you're rolling your own auth, you'll use JWTs with the jose library in proxy and cookies() in server components. Either way, the principle is the same: never trust the client, always verify server-side, and keep your auth checks close to where data is accessed.

Key Takeaways

  • 1Proxy (middleware) is your first line of defense -- it intercepts requests before routes render, so unauthenticated users never see a flash of protected content
  • 2Auth.js (NextAuth v5) is the standard authentication library for Next.js, handling OAuth flows, session management, CSRF protection, and secure cookies out of the box
  • 3The `auth()` function from Auth.js works in server components, route handlers, and server actions -- use it instead of the deprecated `getServerSession()`
  • 4JWT strategy stores everything in the cookie (stateless, fast, but not revocable); database strategy stores sessions server-side (revocable, but adds a DB query per request)
  • 5Server actions need explicit auth checks -- they're callable via POST requests, so proxy alone won't protect them. Always call `auth()` or verify the session at the top of every server action
  • 6Role-based access control (RBAC) should happen at multiple layers: proxy for route-level gating, server components for UI-level decisions, and route handlers/server actions for data-level enforcement

Master next.js authentication

Take the Next.js Full-Stack course with hands-on lessons and challenges.

Examples

Proxy-based route protection with JWT verification

typescript

Proxy intercepts requests before they reach your routes. This pattern verifies the JWT signature with jose, passes the decoded user info via headers (so server components don't re-verify), and redirects to login with a return URL. The matcher ensures proxy only runs on protected paths -- not on static assets or public pages.

Auth.js (NextAuth v5) setup with GitHub OAuth

typescript

Auth.js v5 exports four things: handlers (for the route handler), auth (to check sessions), signIn, and signOut. The jwt callback fires first and adds custom fields to the token. The session callback then exposes those fields to your app. The Prisma adapter stores users and accounts in your database, enabling account linking and server-side session revocation.

Checking the session in a server component

typescript

The auth() function from Auth.js works directly in server components -- no client-side JavaScript needed. This runs entirely on the server, so session data never leaks to the browser bundle. Use conditional rendering to show different UI based on roles. The redirect() function from next/navigation throws internally to halt rendering, so no content is rendered for unauthenticated users.

Protecting a server action

typescript

Server actions are reachable via POST requests, so proxy-level auth alone won't protect them. Always call auth() at the top of every server action. This example checks authentication first, then verifies the user owns the resource (or is an admin) before deleting. This is defense in depth -- even if someone bypasses the UI, the server action enforces the rules.

Role-based access control in proxy

typescript

Auth.js v5 lets you wrap your proxy/middleware function with auth(), which attaches the session to the request object. This avoids manually verifying tokens. The ROUTE_ROLES map centralizes your access rules -- if a user's role isn't in the allowed list for that route, they get redirected to an unauthorized page. Check routes from most specific to least specific so /admin matches before /dashboard.

Client-side auth with useSession and sign-in/out

typescript

Client components use useSession() from next-auth/react, which requires a SessionProvider wrapper in your root layout. Always handle the three states: loading, unauthenticated, and authenticated. The signIn() function accepts a provider ID to skip the default sign-in page. The signOut() function accepts a callbackUrl to control where users land after logging out. Prefer server-side auth checks for security -- use client-side only for UI rendering decisions.

Common Mistakes

Mistake:

Relying on proxy (middleware) as the only auth layer -- assuming that because proxy redirects unauthenticated users, server actions and API routes are automatically protected

Fix:

Proxy only intercepts matched routes. Server actions are callable via POST, and API routes can be hit directly. Always call `auth()` or verify the session at the top of every server action and route handler. Auth should be checked close to where data is accessed, not just at the routing layer.

Mistake:

Storing JWTs in localStorage instead of HttpOnly cookies, exposing them to XSS attacks

Fix:

Always store tokens in HttpOnly, Secure, SameSite cookies. HttpOnly prevents JavaScript from reading the cookie (blocking XSS token theft), Secure ensures transmission only over HTTPS, and SameSite='lax' provides basic CSRF protection. Auth.js does this correctly by default.

Mistake:

Using long-lived JWTs (days or weeks) without a refresh token mechanism, making it impossible to revoke access after a password change or security incident

Fix:

Keep access tokens short-lived (15 minutes) and use refresh tokens (7 days, stored server-side) for silent renewal. When you need to revoke access, delete the refresh token from the database. Alternatively, use Auth.js with a database session strategy where sessions are revocable by design.

Mistake:

Checking `session.user.role` only in the UI (client component) to hide admin buttons, without enforcing the same check server-side in route handlers or server actions

Fix:

Client-side role checks are a UX convenience, not a security boundary. Anyone can call your API directly or invoke server actions with crafted requests. Always enforce RBAC in server components, route handlers, and server actions. Think of client-side checks as hiding buttons, server-side checks as locking doors.

Best Practices

  • Use Auth.js (NextAuth v5) for OAuth and social login -- it handles the OAuth state parameter, PKCE, token refresh, CSRF protection, and secure cookie flags correctly, which are all easy to get wrong when rolling your own
  • Check auth close to the data: proxy for routing decisions, `auth()` in server components for page-level access, and `auth()` again inside every server action and route handler that mutates or reads sensitive data
  • Cache your session check with React's `cache()` function when calling `auth()` in multiple server components during the same request -- this deduplicates the database/JWT verification to a single call
  • Extend the Auth.js session via the `jwt` and `session` callbacks to include user ID and role, rather than making a separate database query in every component that needs user info
  • Choose your session strategy deliberately: JWT for stateless, horizontally-scalable apps where instant revocation is not critical; database sessions when you need instant logout, account linking, or frequently changing permissions

Summary

Next.js authentication works best as a layered system. Proxy (middleware) handles the first gate -- redirecting unauthenticated users before pages render. Auth.js (NextAuth v5) provides the authentication infrastructure: OAuth providers, session management, JWT or database strategies, and callbacks for customization. Server components use `auth()` for session checks without client-side JavaScript. Server actions and route handlers each need their own auth verification because they're independently callable. For RBAC, centralize your role definitions, check permissions at every layer, and remember that client-side checks are cosmetic -- the real enforcement happens on the server.

Practice Next.js with hands-on challenges

Learn next.js authentication hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Next.js with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.