Skip to content

Module 8: Authentication

Authentication is critical. Use a proven solution - don't roll your own.

Authentication Options

SolutionProsConsBest For
ClerkBest DX, components, webhooksPricing at scaleStartups, rapid dev
Supabase AuthFree, integrated with DBLess polished UISupabase users
NextAuth/Auth.jsFree, flexibleMore setup workCustom requirements
Auth0Enterprise featuresComplex, expensiveEnterprise

Setup

bash
npm install @clerk/nextjs
typescript
// .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

ApproachJWTSession
StorageCookie/localStorageServer-side + cookie ID
ValidationCryptographicDatabase lookup
RevocationHard (need blocklist)Easy (delete session)
PayloadCan include claimsMinimal (just ID)
ScaleStatelessRequires shared store

Most auth providers use JWTs with short expiry + refresh tokens.

Security Best Practices

  1. HTTPS everywhere - Never send auth over HTTP
  2. HttpOnly cookies - Prevent XSS from stealing tokens
  3. CSRF protection - Use SameSite cookies
  4. Rate limiting - Prevent brute force
  5. Secure password reset - Time-limited, single-use tokens
  6. MFA support - Offer 2FA for sensitive apps

Summary

  1. Don't roll your own auth - Use Clerk, Supabase, or NextAuth
  2. Clerk - Best DX, managed solution
  3. Supabase Auth - Free, integrated with Supabase
  4. Webhooks - Sync auth events to your database
  5. Middleware - Protect routes at the edge
  6. Security - HTTPS, HttpOnly cookies, rate limiting

Next: Module 9: Payments & Billing →