Home/Learn/Next.js/Authentication Patterns

Authentication Patterns

Advanced
Auth & Security

Reading the session on the server changes the shape of auth: there is no loading state, no flash of the signed-out UI, and no token in JavaScript at all.

Overview

Client-side auth spends most of its complexity on not knowing yet — a checking state, a flash of the wrong UI, a guard that redirects on refresh. A server component sidesteps all of it: `cookies()` is available before anything renders, so the page is built already knowing who the user is. What you trade is that reading cookies makes the route dynamic, so the decision becomes where to draw that boundary — and, if the real auth lives in a separate FastAPI or Spring service, how to reach it from the Next server.

Session on the Server

No loading state, because the answer is available before the first render.

cookies() + cache(), and requireUser
// lib/auth.ts
import { cookies } from 'next/headers'
import { cache } from 'react'

export const getSession = cache(async () => {
  const token = (await cookies()).get('session')?.value
  if (!token) return null
  try {
    return await verifySession(token)          // JWT verify, or a DB lookup
  } catch {
    return null
  }
})

export async function requireUser() {
  const session = await getSession()
  if (!session) redirect('/login')             // throws
  return session.user
}

// A page — no useEffect, no isLoading, no flash
export default async function DashboardPage() {
  const user = await requireUser()
  const stats = await getStats(user.id)
  return <Dashboard user={user} stats={stats} />
}

// cache() means calling getSession in the layout, the page and three
// components costs one verification per request.

// The client still needs the user for interactive bits — pass it down
// as a prop rather than fetching it again:
<UserMenu user={{ id: user.id, name: user.name }} />
// and select the fields, because those props are visible in the HTML.

Signing In and Out

A server action, and the cookie flags that matter.

httpOnly, secure, sameSite — set in an action
'use server'
export async function login(prevState, formData: FormData) {
  const parsed = LoginSchema.safeParse(Object.fromEntries(formData))
  if (!parsed.success) return { error: 'Check your details' }

  const user = await verifyCredentials(parsed.data)
  if (!user) return { error: 'Incorrect email or password' }
  // Deliberately the same message for both cases — a distinct
  // "no such user" tells an attacker which emails are registered.

  const token = await createSession(user.id)
  ;(await cookies()).set('session', token, {
    httpOnly: true,                 // JavaScript cannot read it
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',                // blocks cross-site POST (CSRF)
    path: '/',
    maxAge: 60 * 60 * 12,
  })
  redirect('/dashboard')
}

'use server'
export async function logout() {
  const token = (await cookies()).get('session')?.value
  if (token) await revokeSession(token)        // server-side revocation
  ;(await cookies()).delete('session')
  redirect('/login')
}

// Cookies can only be set in a server action or a route handler —
// not in a server component during render, because the response has
// already begun. That constraint shapes where auth logic lives.

With a Separate Backend

The shape this platform uses, and the choice inside it.

Forward the cookie, or own the session — pick one
// FastAPI owns the session; Next renders. Two workable designs:

// A. The browser talks to the API for auth, and the cookie is
//    same-site (api.aicancode.org). Next forwards it when rendering:
const res = await fetch(`${process.env.API_URL}/me`, {
  headers: { cookie: (await cookies()).toString() },
  cache: 'no-store',
})
// Simple, one source of truth, and the token never touches JS.

// B. Next owns the session and calls the API with a service token.
//    Better isolation, more moving parts, and the API can no longer
//    identify the user on its own.

// Libraries: Auth.js (NextAuth) and Clerk both work well when Next
// owns auth. If a Java or Python service already owns it, adding
// Auth.js usually means two session systems that must agree — which
// is worse than forwarding one cookie.

// Whatever the design, the rules from the Full-Stack track hold:
//   - httpOnly cookie over a token in localStorage
//   - same-site if at all possible; localhost hides the difference
//   - one shared refresh for concurrent 401s
//   - a complete logout clears the server session AND the caches

Key Points to Remember

  • 1Reading cookies in a server component removes the checking state and the flash of signed-out UI entirely
  • 2Wrap the session read in cache() so calling it from several components costs one verification per request
  • 3Cookies can only be set in a server action or route handler, never during a server component render
  • 4Use the same error message for a wrong password and an unknown email, or you leak which accounts exist
  • 5When a separate backend owns auth, forward its cookie rather than running a second session system

Interview Questions

Sign in to ask Aria
1

Why does server-side session reading remove the flash of signed-out UI?

Medium
2

Why can a server component not set a cookie?

Hard
3

How would you handle auth when a separate FastAPI service owns the session?

Hard

Ask Aria about Authentication Patterns

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…