Clerk Authentication
You are an expert in Clerk authentication implementation for Next.js applications. Follow these guidelines when integrating Clerk.
Core Principles Implement defense-in-depth with multiple authentication layers Verify authentication at every data access point, not just middleware Protect server actions individually Use Clerk's built-in security features (HttpOnly cookies, CSRF protection) Installation and Setup npm install @clerk/nextjs
Environment Variables
Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_... CLERK_SECRET_KEY=sk_...
Optional: Custom URLs
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
Provider Setup App Router (app/layout.tsx) import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
With Custom Appearance import { ClerkProvider } from '@clerk/nextjs'; import { dark } from '@clerk/themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
Middleware Configuration Basic Middleware (middleware.ts) import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// Define protected routes const isProtectedRoute = createRouteMatcher([ '/dashboard(.)', '/api/protected(.)', '/settings(.*)', ]);
// Define public routes (optional, for clarity) const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.)', '/sign-up(.)', '/api/public(.*)', ]);
export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) { await auth.protect(); } });
export const config = { matcher: [ // Skip Next.js internals and static files '/((?!_next|[^?]\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).)', // Always run for API routes '/(api|trpc)(.*)', ], };
Advanced Middleware with Role-Based Access import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isAdminRoute = createRouteMatcher(['/admin(.)']); const isProtectedRoute = createRouteMatcher(['/dashboard(.)', '/api/protected(.*)']);
export default clerkMiddleware(async (auth, req) => { const { userId, sessionClaims } = await auth();
// Admin routes require admin role if (isAdminRoute(req)) { if (!userId || sessionClaims?.metadata?.role !== 'admin') { return new Response('Forbidden', { status: 403 }); } }
// Protected routes require authentication if (isProtectedRoute(req)) { await auth.protect(); } });
Authentication in Server Components Using auth() import { auth } from '@clerk/nextjs/server';
export default async function DashboardPage() { const { userId } = await auth();
if (!userId) { redirect('/sign-in'); }
// Fetch user-specific data const data = await fetchUserData(userId);
return
Using currentUser() import { currentUser } from '@clerk/nextjs/server';
export default async function ProfilePage() { const user = await currentUser();
if (!user) { redirect('/sign-in'); }
return (
Welcome, {user.firstName}!
Email: {user.emailAddresses[0]?.emailAddress}
Authentication in Client Components useUser Hook 'use client';
import { useUser } from '@clerk/nextjs';
export function UserProfile() { const { isLoaded, isSignedIn, user } = useUser();
if (!isLoaded) {
return
if (!isSignedIn) {
return
return (
{user.fullName}
useAuth Hook 'use client';
import { useAuth } from '@clerk/nextjs';
export function ProtectedAction() { const { isLoaded, userId, getToken } = useAuth();
async function handleAction() { if (!userId) return;
// Get a fresh token for API calls
const token = await getToken();
const response = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
if (!isLoaded || !userId) { return null; }
return ; }
Server Actions Protection
Always protect server actions individually:
'use server';
import { auth } from '@clerk/nextjs/server';
export async function createPost(formData: FormData) { const { userId } = await auth();
if (!userId) { throw new Error('Unauthorized'); }
const title = formData.get('title') as string; const content = formData.get('content') as string;
// Create post with user ID const post = await db.post.create({ data: { title, content, authorId: userId, }, });
revalidatePath('/posts'); return post; }
With Role Validation 'use server';
import { auth } from '@clerk/nextjs/server';
export async function deleteUser(userId: string) { const { userId: currentUserId, sessionClaims } = await auth();
if (!currentUserId) { throw new Error('Unauthorized'); }
if (sessionClaims?.metadata?.role !== 'admin') { throw new Error('Forbidden: Admin access required'); }
await db.user.delete({ where: { id: userId } }); revalidatePath('/admin/users'); }
API Route Protection Route Handlers (App Router) import { auth } from '@clerk/nextjs/server'; import { NextResponse } from 'next/server';
export async function GET() { const { userId } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const data = await fetchUserData(userId); return NextResponse.json(data); }
export async function POST(request: Request) { const { userId } = await auth();
if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const body = await request.json(); // Process request...
return NextResponse.json({ success: true }); }
JWT Verification for External APIs import { auth } from '@clerk/nextjs/server';
export async function GET() { const { getToken } = await auth();
// Get JWT for external API const token = await getToken({ template: 'external-api' });
const response = await fetch('https://external-api.com/data', {
headers: {
Authorization: Bearer ${token},
},
});
return Response.json(await response.json()); }
Organization Support import { auth } from '@clerk/nextjs/server';
export async function getOrganizationData() { const { userId, orgId, orgRole } = await auth();
if (!userId || !orgId) { throw new Error('Must be in an organization'); }
// Check organization role if (orgRole !== 'org:admin') { throw new Error('Admin access required'); }
return await db.organization.findUnique({ where: { clerkOrgId: orgId }, }); }
Custom Session Claims Configure in Clerk Dashboard
Add custom claims via JWT Templates, then access them:
import { auth } from '@clerk/nextjs/server';
export async function checkSubscription() { const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.subscriptionPlan;
if (plan !== 'pro') { throw new Error('Pro subscription required'); } }
UI Components Pre-built Components import { SignIn, SignUp, SignOutButton, UserButton, SignedIn, SignedOut, } from '@clerk/nextjs';
export function Header() {
return (
// Dedicated sign-in page export default function SignInPage() { return (
Security Best Practices 1. Defense in Depth // Layer 1: Middleware export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) { await auth.protect(); } });
// Layer 2: Server Component export default async function Page() { const { userId } = await auth(); if (!userId) redirect('/sign-in'); // ... }
// Layer 3: Data Access async function fetchUserData(userId: string) { const { userId: currentUserId } = await auth(); if (currentUserId !== userId) throw new Error('Forbidden'); // ... }
- Protect All Server Actions // Every server action should verify auth independently 'use server';
export async function sensitiveAction() { const { userId } = await auth(); if (!userId) throw new Error('Unauthorized'); // ... }
- Avoid Client-Side Only Protection
// BAD: Client-side only check
'use client';
export function SecretComponent() {
const { isSignedIn } = useAuth();
if (!isSignedIn) return null;
return Secret Data; // Data still sent to client! }
// GOOD: Server-side protection
export default async function SecretPage() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
const data = await fetchSecretData(userId);
return
Error Handling import { auth } from '@clerk/nextjs/server'; import { redirect } from 'next/navigation';
export default async function ProtectedPage() { try { const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
const data = await fetchUserData(userId);
return <Dashboard data={data} />;
} catch (error) { if (error instanceof AuthenticationError) { redirect('/sign-in'); } throw error; } }
Testing // Mock Clerk for testing import { auth } from '@clerk/nextjs/server';
jest.mock('@clerk/nextjs/server', () => ({ auth: jest.fn(), }));
describe('Protected API', () => { it('returns 401 for unauthenticated requests', async () => { (auth as jest.Mock).mockResolvedValue({ userId: null });
const response = await GET();
expect(response.status).toBe(401);
});
it('returns data for authenticated requests', async () => { (auth as jest.Mock).mockResolvedValue({ userId: 'user_123' });
const response = await GET();
expect(response.status).toBe(200);
}); });
Common Anti-Patterns to Avoid Relying solely on middleware for protection Not protecting server actions individually Using client-side auth checks for sensitive data Exposing user data without ownership verification Not validating organization membership for org-scoped resources Hardcoding role checks instead of using Clerk's RBAC Not handling loading states in client components