Module 8: Authentication
Authentication is critical. Use a proven solution - don't roll your own.
Authentication Options
| Solution | Pros | Cons | Best For |
|---|---|---|---|
| Clerk | Best DX, components, webhooks | Pricing at scale | Startups, rapid dev |
| Supabase Auth | Free, integrated with DB | Less polished UI | Supabase users |
| NextAuth/Auth.js | Free, flexible | More setup work | Custom requirements |
| Auth0 | Enterprise features | Complex, expensive | Enterprise |
Clerk (Recommended)
Setup
bash
npm install @clerk/nextjstypescript
// .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...Middleware (Protect Routes)
typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/',
'/pricing',
'/blog(.*)',
'/api/webhooks(.*)',
])
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect()
}
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}Layout Provider
tsx
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}Sign In/Up Pages
tsx
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return (
<div className="flex justify-center py-24">
<SignIn />
</div>
)
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function SignUpPage() {
return (
<div className="flex justify-center py-24">
<SignUp />
</div>
)
}Using Auth in Components
tsx
// Server Component
import { auth, currentUser } from '@clerk/nextjs/server'
export default async function DashboardPage() {
const { userId } = auth()
const user = await currentUser()
if (!userId) {
redirect('/sign-in')
}
return <h1>Welcome, {user?.firstName}</h1>
}tsx
// Client Component
'use client'
import { useUser, useAuth, UserButton } from '@clerk/nextjs'
export function Header() {
const { user, isLoaded } = useUser()
const { signOut } = useAuth()
if (!isLoaded) return null
return (
<header className="flex justify-between p-4">
<h1>My App</h1>
{user ? (
<UserButton afterSignOutUrl="/" />
) : (
<a href="/sign-in">Sign In</a>
)}
</header>
)
}Webhooks (Sync Users to DB)
typescript
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
import { db } from '@/lib/db'
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!
const headerPayload = headers()
const svix_id = headerPayload.get('svix-id')
const svix_timestamp = headerPayload.get('svix-timestamp')
const svix_signature = headerPayload.get('svix-signature')
const body = await req.text()
const wh = new Webhook(WEBHOOK_SECRET)
let evt: WebhookEvent
try {
evt = wh.verify(body, {
'svix-id': svix_id!,
'svix-timestamp': svix_timestamp!,
'svix-signature': svix_signature!,
}) as WebhookEvent
} catch (err) {
return new Response('Webhook verification failed', { status: 400 })
}
if (evt.type === 'user.created') {
await db.user.create({
data: {
id: evt.data.id,
email: evt.data.email_addresses[0]?.email_address,
name: `${evt.data.first_name} ${evt.data.last_name}`.trim(),
},
})
}
if (evt.type === 'user.updated') {
await db.user.update({
where: { id: evt.data.id },
data: {
email: evt.data.email_addresses[0]?.email_address,
name: `${evt.data.first_name} ${evt.data.last_name}`.trim(),
},
})
}
if (evt.type === 'user.deleted') {
await db.user.delete({
where: { id: evt.data.id },
})
}
return new Response('OK', { status: 200 })
}Supabase Auth
Setup
typescript
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}Sign Up
typescript
// app/auth/signup/action.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function signUp(formData: FormData) {
const supabase = createClient()
const { error } = await supabase.auth.signUp({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) {
return { error: error.message }
}
redirect('/auth/verify')
}Sign In
typescript
// app/auth/signin/action.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function signIn(formData: FormData) {
const supabase = createClient()
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}OAuth (Social Login)
typescript
// Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${origin}/auth/callback`,
},
})
// Callback handler
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(`${origin}/dashboard`)
}Middleware Protection
typescript
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options) {
response.cookies.set({ name, value, ...options })
},
remove(name: string, options) {
response.cookies.set({ name, value: '', ...options })
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}Session Management
JWT vs Session Tokens
| Approach | JWT | Session |
|---|---|---|
| Storage | Cookie/localStorage | Server-side + cookie ID |
| Validation | Cryptographic | Database lookup |
| Revocation | Hard (need blocklist) | Easy (delete session) |
| Payload | Can include claims | Minimal (just ID) |
| Scale | Stateless | Requires shared store |
Most auth providers use JWTs with short expiry + refresh tokens.
Security Best Practices
- HTTPS everywhere - Never send auth over HTTP
- HttpOnly cookies - Prevent XSS from stealing tokens
- CSRF protection - Use SameSite cookies
- Rate limiting - Prevent brute force
- Secure password reset - Time-limited, single-use tokens
- MFA support - Offer 2FA for sensitive apps
Summary
- Don't roll your own auth - Use Clerk, Supabase, or NextAuth
- Clerk - Best DX, managed solution
- Supabase Auth - Free, integrated with Supabase
- Webhooks - Sync auth events to your database
- Middleware - Protect routes at the edge
- Security - HTTPS, HttpOnly cookies, rate limiting