🔐 Next.js Authentication: The Castle Guards Story
Imagine your Next.js app is a magical castle. Inside are treasure rooms (protected pages), secret passages (routes), and visitors who want to enter. But you need guards to check who’s allowed in!
Today, we’ll learn how to set up the four types of castle guards that protect your Next.js kingdom.
🏰 The Big Picture
graph TD A["Visitor Arrives"] --> B{Has Magic Cookie?} B -->|Yes| C["Server Component Guard"] B -->|No| D["Redirect to Login"] C --> E["Show Protected Page"] D --> F["User Logs In"] F --> G["Server Action Creates Cookie"] G --> H["Route Proxy Watches All Doors"]
Think of it like this:
- Cookies = Magic wristbands that prove you’re allowed in
- Server Components = Guards who check wristbands at each room
- Server Actions = The office that gives/takes wristbands
- Route Protection = The main gate that checks everyone
🍪 Part 1: Cookies for Sessions (The Magic Wristband)
What is a Cookie?
A cookie is like a magic wristband at a theme park. Once you get it, you can enter all the rides without buying a new ticket each time!
// How to give someone a wristband
import { cookies } from 'next/headers';
// Create a session cookie
const cookieStore = await cookies();
cookieStore.set('session', 'user123', {
httpOnly: true, // Can't be stolen by sneaky scripts
secure: true, // Only works on safe connections
maxAge: 60 * 60, // Lasts 1 hour (3600 seconds)
path: '/' // Works everywhere in the castle
});
Reading the Wristband
// Check if someone has a valid wristband
const cookieStore = await cookies();
const session = cookieStore.get('session');
if (session) {
console.log('Welcome back!', session.value);
} else {
console.log('No wristband found!');
}
Why httpOnly Matters
| Setting | What It Does | Like… |
|---|---|---|
httpOnly: true |
Only server can read | Invisible ink wristband |
secure: true |
Only works on HTTPS | Waterproof wristband |
sameSite: 'lax' |
Blocks some attacks | Wristband only works inside park |
🎯 Key Point: Always use
httpOnlycookies for sessions. This stops bad guys from stealing wristbands using JavaScript tricks!
🏛️ Part 2: Auth in Server Components (Room Guards)
What are Server Components?
Server Components are like guards standing at each room. Before you enter, they check your wristband!
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
// The guard checks your wristband
const cookieStore = await cookies();
const session = cookieStore.get('session');
// No wristband? Go to the entrance!
if (!session) {
redirect('/login');
}
// Wristband valid? Come on in!
return (
<div>
<h1>Welcome to the Dashboard!</h1>
<p>Only special people can see this.</p>
</div>
);
}
Getting User Info in Server Components
// lib/auth.ts
import { cookies } from 'next/headers';
export async function getUser() {
const cookieStore = await cookies();
const session = cookieStore.get('session');
if (!session) return null;
// Decode the wristband to see who it belongs to
const user = await verifySession(session.value);
return user;
}
// app/profile/page.tsx
import { getUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function Profile() {
const user = await getUser();
if (!user) redirect('/login');
return <h1>Hello, {user.name}!</h1>;
}
Why Server Components are Special
graph TD A["User Requests Page"] --> B["Server Component Runs"] B --> C["Checks Cookie on Server"] C --> D{Valid Session?} D -->|Yes| E["Sends Protected HTML"] D -->|No| F["Redirects to Login"] E --> G["User Sees Page"]
🔒 Security Win: The user never sees the checking code. Everything happens on the server, safe from hackers!
⚡ Part 3: Auth in Server Actions (The Wristband Office)
What are Server Actions?
Server Actions are like the main office where wristbands are created, checked, and destroyed. This is where login and logout happen!
Creating a Login Action
// app/actions/auth.ts
'use server'; // This magic word makes it run on server!
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function login(formData: FormData) {
const email = formData.get('email');
const password = formData.get('password');
// Check if username/password are correct
const user = await checkCredentials(email, password);
if (!user) {
return { error: 'Wrong email or password!' };
}
// Give them a wristband!
const cookieStore = await cookies();
cookieStore.set('session', user.id, {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 // 1 day
});
redirect('/dashboard');
}
Creating a Logout Action
// app/actions/auth.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function logout() {
// Take back the wristband!
const cookieStore = await cookies();
cookieStore.delete('session');
redirect('/login');
}
Using Actions in Your Form
// app/login/page.tsx
import { login } from '@/app/actions/auth';
export default function LoginPage() {
return (
<form action={login}>
<input
name="email"
type="email"
placeholder="Email"
/>
<input
name="password"
type="password"
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
The Flow
graph TD A["User Fills Form"] --> B["Clicks Submit"] B --> C["Server Action Runs"] C --> D{Credentials OK?} D -->|Yes| E["Create Session Cookie"] D -->|No| F["Return Error"] E --> G["Redirect to Dashboard"] F --> H["Show Error Message"]
🚧 Part 4: Route Protection with Middleware (The Main Gate)
What is Middleware?
Middleware is the main gate guard who checks EVERYONE before they even reach the castle doors. It runs before any page loads!
Setting Up the Gate
// middleware.ts (in your project root!)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check for the magic wristband
const session = request.cookies.get('session');
// List of protected paths (treasure rooms)
const protectedPaths = ['/dashboard', '/profile', '/settings'];
const isProtectedPath = protectedPaths.some(
path => request.nextUrl.pathname.startsWith(path)
);
// No wristband + trying to enter protected area?
if (isProtectedPath && !session) {
// Send them to the login page!
return NextResponse.redirect(
new URL('/login', request.url)
);
}
// Already logged in + going to login page?
if (session && request.nextUrl.pathname === '/login') {
// Send them to dashboard instead!
return NextResponse.redirect(
new URL('/dashboard', request.url)
);
}
// Everything else? Let them through!
return NextResponse.next();
}
// Tell Next.js which paths to guard
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/settings/:path*',
'/login'
]
};
Why Middleware is Powerful
| Feature | Benefit |
|---|---|
| Runs before page loads | Faster redirects |
| Checks every request | No page can be skipped |
| Works at the edge | Super fast globally |
| Single file | Easy to manage |
The Matcher Pattern
// Match specific paths
matcher: ['/dashboard'] // Only /dashboard
// Match with wildcards
matcher: ['/dashboard/:path*'] // /dashboard and all sub-paths
// Match multiple paths
matcher: ['/dashboard/:path*', '/profile/:path*']
🎯 Putting It All Together
Here’s how all four guards work together:
graph TD A["User Visits /dashboard"] --> B["Middleware Checks Cookie"] B -->|No Cookie| C["Redirect to /login"] B -->|Has Cookie| D["Server Component Runs"] D --> E["Double-Check Session"] E -->|Invalid| C E -->|Valid| F["Render Dashboard"] G["User Clicks Logout"] --> H["Server Action Runs"] H --> I["Delete Cookie"] I --> C
Real-World Example: Complete Auth Setup
// 1. lib/auth.ts - Helper functions
import { cookies } from 'next/headers';
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(
process.env.JWT_SECRET
);
export async function createSession(userId: string) {
const token = await new SignJWT({ userId })
.setExpirationTime('1d')
.sign(secret);
const cookieStore = await cookies();
cookieStore.set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24
});
}
export async function getSession() {
const cookieStore = await cookies();
const token = cookieStore.get('session')?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, secret);
return payload;
} catch {
return null;
}
}
💡 Quick Tips
Do’s ✅
- Always use
httpOnlyfor session cookies - Check auth in Server Components AND Middleware
- Use Server Actions for login/logout
- Keep session duration short (hours, not days)
Don’ts ❌
- Never store passwords in cookies
- Don’t trust client-side checks alone
- Avoid storing sensitive data in regular cookies
- Don’t forget to handle expired sessions
🎮 Summary
| Guard Type | When It Works | What It Does |
|---|---|---|
| Cookies | Always | Stores the “proof of entry” |
| Server Components | Page Load | Checks auth before rendering |
| Server Actions | Form Submit | Handles login/logout |
| Middleware | Before Anything | Guards all routes at once |
You now have four powerful guards protecting your Next.js castle! Each one has a special job, and together, they make your app super secure.
🏆 You did it! You now understand Next.js authentication. Go build something amazing and keep those bad guys out! 🚀
