Clerk Next.js

authentication-libraryJavaScriptNext.jsJWTOAuthsession-managementReact

Authentication Library

Clerk Next.js

Overview

Clerk Next.js is a modern authentication and user management library designed specifically for Next.js applications. As of 2025, it provides full support for both App Router and Pages Router, and is widely adopted as the most developer-friendly authentication solution. It offers comprehensive authentication features including JWT, OAuth 2.0, session management, and multi-factor authentication (MFA), allowing developers to implement a complete user authentication system with just a few lines of code. It's fully integrated with React's hook patterns and provides intuitive APIs such as useAuth, useUser, and useClerk.

Details

Clerk Next.js is a library that comprehensively provides authentication features required by modern web applications. Key features include:

  • Next.js Optimization: Complete integration with App Router, Pages Router, API Routes, and middleware
  • JWT Integration: Automatic JWT token management and session claim processing
  • OAuth Providers: Support for 20+ external authentication providers including Google, GitHub, Discord, Apple, Microsoft
  • Session Management: Unified session processing on both server-side and client-side
  • Reactive Hooks: Declarative authentication state management with useAuth, useUser, and useClerk
  • Protected Routes: Automatic route protection and redirect functionality based on middleware
  • Customizable: Complete customization of authentication flows, UI components, and redirect destinations

Pros and Cons

Pros

  • Seamless integration and high performance through Next.js-specific optimization
  • Dramatic improvement in development speed with complete authentication system in just a few lines of code
  • Enterprise-level security features including JWT, OAuth, MFA, and RBAC
  • Intuitive and declarative API design through React hooks patterns
  • Consistent development experience across both App Router and Pages Router
  • Rich documentation and active community support

Cons

  • Cannot be used with frameworks other than Next.js (not portable to React, Vue, Angular, etc.)
  • Subject to impact when Clerk service is down due to external service dependency
  • Constraints within Clerk's ecosystem for advanced customization
  • Potentially high usage fees for large-scale applications
  • Risk of vendor lock-in when migrating from proprietary authentication systems

Reference Pages

Code Examples

Basic Setup and Installation

# Install Clerk Next.js package
npm install @clerk/nextjs

# Environment variable setup (.env.local)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

# Optional: Redirect URL configuration
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

Basic Authentication Setup with App Router

// app/layout.tsx - Wrap entire app with ClerkProvider
import { ClerkProvider } from '@clerk/nextjs'
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

// middleware.ts - Route protection configuration
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/admin(.*)',
  '/profile(.*)'
])

const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/public(.*)'
])

export default clerkMiddleware((auth, request) => {
  // Skip authentication for public routes
  if (isPublicRoute(request)) {
    return NextResponse.next()
  }

  // Require authentication for protected routes
  if (isProtectedRoute(request)) {
    auth().protect()
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
}

Sign-in and Sign-up Page Implementation

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn
        appearance={{
          elements: {
            formButtonPrimary: 'bg-blue-600 hover:bg-blue-700 text-white',
            card: 'shadow-lg'
          }
        }}
        routing="hash"
        signUpUrl="/sign-up"
        redirectUrl="/dashboard"
      />
    </div>
  )
}

// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'

export default function SignUpPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUp
        appearance={{
          elements: {
            formButtonPrimary: 'bg-green-600 hover:bg-green-700 text-white',
            card: 'shadow-lg'
          }
        }}
        routing="hash"
        signInUrl="/sign-in"
        redirectUrl="/dashboard"
      />
    </div>
  )
}

Conditional Rendering Based on Authentication State

// app/page.tsx - Authentication state display on homepage
import { 
  SignedIn, 
  SignedOut, 
  SignInButton, 
  UserButton,
  useUser 
} from '@clerk/nextjs'
import Link from 'next/link'

export default function HomePage() {
  return (
    <div className="container mx-auto px-4 py-8">
      <nav className="flex justify-between items-center mb-8">
        <h1 className="text-2xl font-bold">My App</h1>
        
        <div className="flex items-center space-x-4">
          <SignedOut>
            <SignInButton mode="modal">
              <button className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
                Sign In
              </button>
            </SignInButton>
            <Link href="/sign-up">
              <button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
                Sign Up
              </button>
            </Link>
          </SignedOut>
          
          <SignedIn>
            <Link href="/dashboard">
              <button className="text-blue-600 hover:text-blue-800">
                Dashboard
              </button>
            </Link>
            <UserButton 
              afterSignOutUrl="/"
              appearance={{
                elements: {
                  avatarBox: 'w-10 h-10'
                }
              }}
            />
          </SignedIn>
        </div>
      </nav>

      <main>
        <SignedOut>
          <div className="text-center">
            <h2 className="text-4xl font-bold mb-4">Welcome to My App</h2>
            <p className="text-xl text-gray-600 mb-8">
              Sign in to get started
            </p>
          </div>
        </SignedOut>
        
        <SignedIn>
          <WelcomeMessage />
        </SignedIn>
      </main>
    </div>
  )
}

function WelcomeMessage() {
  const { user } = useUser()
  
  return (
    <div className="text-center">
      <h2 className="text-4xl font-bold mb-4">
        Hello, {user?.firstName || user?.username}!
      </h2>
      <p className="text-xl text-gray-600">
        Authentication completed. Enjoy the app.
      </p>
    </div>
  )
}

Protected Dashboard Page

// app/dashboard/page.tsx - Authentication-required page
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
import UserProfile from '@/components/UserProfile'
import DashboardStats from '@/components/DashboardStats'

export default async function DashboardPage() {
  // Server-side authentication check
  const { userId } = await auth()
  
  if (!userId) {
    redirect('/sign-in')
  }

  // Get current user information
  const user = await currentUser()
  
  if (!user) {
    redirect('/sign-in')
  }

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <h1 className="text-3xl font-bold mb-2">Dashboard</h1>
        <p className="text-gray-600">
          Welcome, {user.firstName || user.username}
        </p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <UserProfile user={user} />
        <DashboardStats userId={userId} />
        
        <div className="bg-white p-6 rounded-lg shadow">
          <h3 className="text-lg font-semibold mb-4">Account Information</h3>
          <div className="space-y-2">
            <p><strong>Email:</strong> {user.primaryEmailAddress?.emailAddress}</p>
            <p><strong>Sign-up Date:</strong> {user.createdAt?.toLocaleDateString('en-US')}</p>
            <p><strong>Last Sign-in:</strong> {user.lastSignInAt?.toLocaleDateString('en-US')}</p>
          </div>
        </div>
      </div>
    </div>
  )
}

API Routes with Authentication and JWT Processing

// app/api/profile/route.ts - Protected API
import { auth, currentUser } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

export async function GET() {
  try {
    // Get authentication state and JWT claims
    const { userId, sessionClaims, getToken } = await auth()
    
    if (!userId) {
      return new NextResponse(
        JSON.stringify({ error: 'Authentication required' }), 
        { status: 401 }
      )
    }

    // Get JWT token (for external API calls)
    const token = await getToken()
    
    // Get current user information
    const user = await currentUser()
    
    // Detailed session claims information
    const userMetadata = {
      userId: userId,
      sessionId: sessionClaims?.sid,
      email: user?.primaryEmailAddress?.emailAddress,
      roles: sessionClaims?.metadata?.roles || [],
      permissions: sessionClaims?.metadata?.permissions || []
    }

    return NextResponse.json({
      success: true,
      user: userMetadata,
      tokenAvailable: !!token
    })
    
  } catch (error) {
    console.error('API error:', error)
    return new NextResponse(
      JSON.stringify({ error: 'Server error' }), 
      { status: 500 }
    )
  }
}

export async function PUT(request: Request) {
  try {
    const { userId } = await auth()
    
    if (!userId) {
      return new NextResponse(
        JSON.stringify({ error: 'Authentication required' }), 
        { status: 401 }
      )
    }

    const updateData = await request.json()
    
    // Profile update logic
    // userService.updateProfile(userId, updateData)
    
    return NextResponse.json({
      success: true,
      message: 'Profile updated successfully'
    })
    
  } catch (error) {
    return new NextResponse(
      JSON.stringify({ error: 'Profile update failed' }), 
      { status: 500 }
    )
  }
}

External API Authentication and Token Management

// components/ExternalDataFetcher.tsx - External API integration
'use client'

import { useAuth } from '@clerk/nextjs'
import { useState, useEffect } from 'react'

interface ExternalApiData {
  id: string
  name: string
  data: any
}

export default function ExternalDataFetcher() {
  const { getToken, isLoaded, isSignedIn } = useAuth()
  const [data, setData] = useState<ExternalApiData[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const fetchExternalData = async () => {
    try {
      setLoading(true)
      setError(null)
      
      // Get JWT token from Clerk
      const token = await getToken()
      
      if (!token) {
        throw new Error('Unable to get authentication token')
      }

      // Request to external API using JWT token
      const response = await fetch('/api/external-data', {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      })

      if (!response.ok) {
        throw new Error(`API Error: ${response.status}`)
      }

      const result = await response.json()
      setData(result.data)
      
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
      console.error('External API fetch error:', err)
    } finally {
      setLoading(false)
    }
  }

  // Auto-fetch on mount
  useEffect(() => {
    if (isLoaded && isSignedIn) {
      fetchExternalData()
    }
  }, [isLoaded, isSignedIn])

  if (!isLoaded) {
    return <div className="animate-pulse">Loading...</div>
  }

  if (!isSignedIn) {
    return <div className="text-red-600">Authentication required</div>
  }

  return (
    <div className="space-y-4">
      <div className="flex justify-between items-center">
        <h3 className="text-lg font-semibold">External Data</h3>
        <button
          onClick={fetchExternalData}
          disabled={loading}
          className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {loading ? 'Updating...' : 'Refresh'}
        </button>
      </div>

      {error && (
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
          Error: {error}
        </div>
      )}

      <div className="grid gap-4">
        {data.map((item) => (
          <div key={item.id} className="bg-white p-4 rounded-lg shadow">
            <h4 className="font-medium">{item.name}</h4>
            <pre className="mt-2 text-sm text-gray-600">
              {JSON.stringify(item.data, null, 2)}
            </pre>
          </div>
        ))}
      </div>
      
      {data.length === 0 && !loading && !error && (
        <div className="text-center text-gray-500 py-8">
          No data available
        </div>
      )}
    </div>
  )
}

Advanced Authentication Hooks and Customization

// hooks/useAuthStatus.ts - Custom authentication hook
import { useAuth, useUser, useClerk } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import { useCallback } from 'react'

export function useAuthStatus() {
  const { 
    isLoaded: authLoaded, 
    isSignedIn, 
    userId, 
    sessionId,
    getToken 
  } = useAuth()
  
  const { 
    isLoaded: userLoaded, 
    user 
  } = useUser()
  
  const { signOut, openSignIn } = useClerk()
  const router = useRouter()

  // Custom sign-out processing
  const handleSignOut = useCallback(async () => {
    try {
      await signOut()
      router.push('/')
    } catch (error) {
      console.error('Sign out error:', error)
    }
  }, [signOut, router])

  // Token acquisition and refresh
  const getAuthToken = useCallback(async () => {
    try {
      return await getToken()
    } catch (error) {
      console.error('Token fetch error:', error)
      return null
    }
  }, [getToken])

  // Authentication-required action
  const requireAuth = useCallback((action: () => void) => {
    if (!isSignedIn) {
      openSignIn()
      return
    }
    action()
  }, [isSignedIn, openSignIn])

  return {
    // Basic state
    isLoaded: authLoaded && userLoaded,
    isSignedIn,
    user,
    userId,
    sessionId,
    
    // Extended functionality
    getAuthToken,
    handleSignOut,
    requireAuth,
    
    // User information
    userEmail: user?.primaryEmailAddress?.emailAddress,
    userName: user?.firstName || user?.username || 'Unknown',
    userRoles: user?.publicMetadata?.roles as string[] || [],
    
    // State checks
    isAdmin: (user?.publicMetadata?.roles as string[])?.includes('admin') || false,
    isVerified: user?.primaryEmailAddress?.verification?.status === 'verified'
  }
}

// components/AuthGuard.tsx - Authentication guard component
import { ReactNode } from 'react'
import { useAuthStatus } from '@/hooks/useAuthStatus'
import { SignInButton } from '@clerk/nextjs'

interface AuthGuardProps {
  children: ReactNode
  fallback?: ReactNode
  requireAdmin?: boolean
  requireVerified?: boolean
}

export default function AuthGuard({ 
  children, 
  fallback, 
  requireAdmin = false,
  requireVerified = false 
}: AuthGuardProps) {
  const { 
    isLoaded, 
    isSignedIn, 
    isAdmin, 
    isVerified 
  } = useAuthStatus()

  // Loading state
  if (!isLoaded) {
    return (
      <div className="flex justify-center items-center min-h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
      </div>
    )
  }

  // Unauthenticated
  if (!isSignedIn) {
    return fallback || (
      <div className="flex flex-col items-center justify-center min-h-screen space-y-4">
        <h2 className="text-2xl font-bold">Authentication Required</h2>
        <p className="text-gray-600">Please sign in to access this page</p>
        <SignInButton mode="modal">
          <button className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
            Sign In
          </button>
        </SignInButton>
      </div>
    )
  }

  // Admin privilege check
  if (requireAdmin && !isAdmin) {
    return (
      <div className="flex flex-col items-center justify-center min-h-screen space-y-4">
        <h2 className="text-2xl font-bold text-red-600">Access Denied</h2>
        <p className="text-gray-600">You don't have permission to access this page</p>
      </div>
    )
  }

  // Email verification check
  if (requireVerified && !isVerified) {
    return (
      <div className="flex flex-col items-center justify-center min-h-screen space-y-4">
        <h2 className="text-2xl font-bold text-yellow-600">Email Verification Required</h2>
        <p className="text-gray-600">Please verify your email address to access this page</p>
      </div>
    )
  }

  return <>{children}</>
}