Skip to main content

auth-patterns

Next.js authentication and authorization patterns. Use when the user asks about "authentication in Next.js", "NextAuth.js", "Auth.js", "Better Auth", "middleware auth guards", "protected routes", "session management", "role-based access", "login page", "signup form", "JWT sessions", "cookies auth", or needs guidance on implementing auth in App Router applications.

Source facts

Repository
Agents-Store/cursor-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
auth-patterns
description
Next.js authentication and authorization patterns. Use when the user asks about "authentication in Next.js", "NextAuth.js", "Auth.js", "Better Auth", "middleware auth guards", "protected routes", "session management", "role-based access", "login page", "signup form", "JWT sessions", "cookies auth", or needs guidance on implementing auth in App Router applications.
# Authentication Patterns Authentication in Next.js spans multiple layers: proxy (middleware), layouts, pages, Server Actions, and Route Handlers. This skill covers where and how to check auth at each layer. ## Choosing an Auth Library | Option | Use when | Notes | |--------|----------|-------| | **Better Auth** | New projects | Recommended path — see "Better Auth (Recommended for New Projects)" below. The Auth.js project is now part of Better Auth | | **Own sessions** (`jose` + cookies, below) | Simple credentials login, full control, no extra dependency | You own password hashing, rotation and CSRF review | | **NextAuth.js v4** (`next-auth@latest`, `4.24.15`) | Existing projects; a credentials login against an existing token API (for example a Directus backend) | A working path with Next.js 16, not a deprecated one: `npm i next-auth` still installs v4. Config, `proxy.ts` guard and its limits: [references/nextauth-and-authjs.md](references/nextauth-and-authjs.md) | | **Auth.js v5** (`next-auth@beta`) | Existing projects already on it | Still a beta (`5.0.0-beta.32`) in maintenance mode — the Better Auth team ships security patches and critical fixes only | The proxy / DAL / Server Action / Route Handler layering in this skill is library-independent: `getSession()` changes, and so does where a role comes from (Better Auth has none by default, see its section). ## Authentication Flow Overview 1. **User submits credentials** → Server Action validates and creates session 2. **Session stored** in encrypted cookie (stateless) or database (stateful) 3. **Proxy/middleware checks** session on every request → redirects if unauthorized 4. **Server Components** read session to render user-specific UI 5. **Server Actions/Route Handlers** verify session before mutations ## Sign-up / Login with Server Actions ### 1. Define Validation Schema ```typescript // lib/schemas.ts import { z } from 'zod' export const SignupSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters').trim(), email: z.string().email('Please enter a valid email').trim(), password: z.string().min(8, 'Password must be at least 8 characters'), }) export const LoginSchema = z.object({ email: z.string().email('Please enter a valid email'), password: z.string().min(1, 'Password is required'), }) ``` ### 2. Server Action for Signup ```typescript // app/actions/auth.ts 'use server' import { SignupSchema } from '@/lib/schemas' import { createSession } from '@/lib/session' import { redirect } from 'next/navigation' import bcrypt from 'bcrypt' export type AuthState = { message: string errors?: Record<string, string[]> } export async function signup(prevState: AuthState, formData: FormData): Promise<AuthState> { const parsed = SignupSchema.safeParse({ name: formData.get('name'), email: formData.get('email'), password: formData.get('password'), }) if (!parsed.success) { return { message: 'Validation failed', errors: parsed.error.flatten().fieldErrors } } // Check if user exists const existingUser = await db.user.findUnique({ where: { email: parsed.data.email } }) if (existingUser) { return { message: 'Email already registered', errors: { email: ['Email already in use'] } } } // Create user const hashedPassword = await bcrypt.hash(parsed.data.password, 10) const user = await db.user.create({ data: { name: parsed.data.name, email: parsed.data.email, password: hashedPassword }, }) // Create session and redirect await createSession(user.id) redirect('/dashboard') } ``` ### 3. Signup Form Component ```tsx // app/(auth)/signup/page.tsx 'use client' import { useActionState } from 'react' import { signup, type AuthState } from '@/app/actions/auth' const initialState: AuthState = { message: '' } export default function SignupPage() { const [state, formAction, pending] = useActionState(signup, initialState) return ( <form action={formAction}> <div> <label htmlFor="name">Name</label> <input id="name" name="name" type="text" required /> {state.errors?.name && <p className="text-red-500">{state.errors.name[0]}</p>} </div> <div> <label htmlFor="email">Email</label> <input id="email" name="email" type="email" required /> {state.errors?.email && <p className="text-red-500">{state.errors.email[0]}</p>} </div> <div> <label htmlFor="password">Password</label> <input id="password" name="password" type="password" required /> {state.errors?.password && <p className="text-red-500">{state.errors.password[0]}</p>} </div> <button type="submit" disabled={pending}> {pending ? 'Creating account...' : 'Sign Up'} </button> {state.message && !state.errors && <p>{state.message}</p>} </form> ) } ``` ## Session Management ### Stateless Sessions (JWT in Cookies) Use `jose` for JWT encryption compatible with Edge Runtime: ```typescript // lib/session.ts import 'server-only' import { SignJWT, jwtVerify } from 'jose' import { cookies } from 'next/headers' const secretKey = process.env.SESSION_SECRET! const encodedKey = new TextEncoder().encode(secretKey) export async function createSession(userId: string) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days const session = await new SignJWT({ userId }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(encodedKey) const cookieStore = await cookies() cookieStore.set('session', session, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', expires: expiresAt, path: '/', }) } export async function getSession() { const cookieStore = await cookies() const session = cookieStore.get('session')?.value if (!session) return null try { const { payload } = await jwtVerify(session, encodedKey, { algorithms: ['HS256'] }) return payload as { userId: string } } catch { return null } } export async function deleteSession() { const cookieStore = await cookies() cookieStore.delete('session') } ``` **Cookie options checklist:** - `httpOnly: true` — prevents client-side JS access - `secure: true` — HTTPS only in production - `sameSite: 'lax'` — CSRF protection - `path: '/'` — available on all routes ### Session Refresh Extend session expiration when the user is active: ```typescript // lib/session.ts export async function refreshSession() { const session = await getSession() if (!session) return // Recreate with fresh expiration await createSession(session.userId) } ``` ## Proxy Auth Guard (Next.js 16) Check session in `proxy.ts` to protect routes before they render: ```typescript // proxy.ts import { NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/session' const protectedRoutes = ['/dashboard', '/settings', '/account'] const authRoutes = ['/login', '/signup'] export async function proxy(request: NextRequest) { const path = request.nextUrl.pathname const isProtected = protectedRoutes.some(route => path.startsWith(route)) const isAuthRoute = authRoutes.some(route => path.startsWith(route)) const session = await getSession() // Redirect unauthenticated users to login if (isProtected && !session) { const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('callbackUrl', path) return NextResponse.redirect(loginUrl) } // Redirect authenticated users away from auth pages if (isAuthRoute && session) { return NextResponse.redirect(new URL('/dashboard', request.url)) } return NextResponse.next() } ``` ## Session in Server Components ```tsx // app/dashboard/page.tsx import { getSession } from '@/lib/session' import { redirect } from 'next/navigation' export default async function DashboardPage() { const session = await getSession() if (!session) { redirect('/login') } const user = await db.user.findUnique({ where: { id: session.userId } }) return <h1>Welcome, {user?.name}</h1> } ``` ## Protecting Server Actions Always verify auth before mutations: ```typescript // app/actions/posts.ts 'use server' import { getSession } from '@/lib/session' export async function deletePost(postId: string) { const session = await getSession() if (!session) { throw new Error('Unauthorized') } // Verify ownership const post = await db.post.findUnique({ where: { id: postId } }) if (post?.authorId !== session.userId) { throw new Error('Forbidden') } await db.post.delete({ where: { id: postId } }) } ``` ## Protecting Route Handlers ```typescript // app/api/posts/route.ts import { getSession } from '@/lib/session' export async function GET() { const session = await getSession() if (!session) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } const posts = await db.post.findMany({ where: { authorId: session.userId } }) return Response.json(posts) } ``` ## Role-Based Access Control (RBAC) Extend session with roles: ```typescript // lib/session.ts export async function createSession(userId: string, role: string) { const session = await new SignJWT({ userId, role }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('7d') .sign(encodedKey) // ... set cookie } ``` ```typescript // lib/dal.ts — Data Access Layer with role checks import 'server-only' import { getSession } from '@/lib/session' export async function requireAdmin() { const session = await getSession() if (!session || session.role !== 'admin') { throw new Error('Forbidden: Admin access required') } return session } export async function requireAuth() { const session = await getSession() if (!session) { throw new Error('Unauthorized') } return session } ``` ```tsx // app/admin/page.tsx import { requireAdmin } from '@/lib/dal' export default async function AdminPage() { const session = await requireAdmin() // Throws if not admin return <h1>Admin Dashboard</h1> } ``` ## Better Auth (Recommended for New Projects) The Auth.js project has joined Better Auth. The Better Auth team continues to handle security patches and critical issues for Auth.js, but for new projects they strongly recommend Better Auth; if an existing Auth.js setup works well there is no urgent need to migrate (migration guide: https://authjs.dev/getting-started/migrate-to-better-auth). ```bash npm install better-auth ``` ```bash # .env — generate the secret with: openssl rand -base64 32 BETTER_AUTH_SECRET=<at least 32 characters of high entropy> BETTER_AUTH_URL=http://localhost:3000 ``` ```typescript // lib/auth.ts import { betterAuth } from 'better-auth' import { nextCookies } from 'better-auth/next-js' import { Pool } from 'pg' export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL }), // or a Drizzle / Prisma adapter emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, }, plugins: [nextCookies()], // lets Server Actions set cookies — must be the last plugin }) ``` Without a `database` option Better Auth runs in stateless-session mode, but most plugins require a database. Generate the schema with the Better Auth CLI (`npx auth@latest generate`; for Prisma or Drizzle it writes the ORM schema, for the built-in database support it writes an SQL file). ```typescript // app/api/auth/[...all]/route.ts import { auth } from '@/lib/auth' import { toNextJsHandler } from 'better-auth/next-js' export const { GET, POST } = toNextJsHandler(auth) ``` ```typescript // lib/auth-client.ts — import from better-auth/react
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub