Authentication

Clerk-based authentication with Server Components and middleware

Overview

The application uses Clerk for authentication, providing secure user management with minimal configuration. Clerk integrates seamlessly with Next.js Server Components and middleware, handling session management, token refresh, and route protection automatically.

Architecture

Authentication is implemented at three layers:

Middleware Layer: Protects routes before they render. The middleware checks authentication status and redirects unauthenticated users to the sign-in page.

Server Component Layer: Server Actions use Clerk's auth() function to get the current user's token for API requests. This keeps tokens server-side and never exposes them to the client.

Client Component Layer: Client components use Clerk hooks (useUser, useAuth) to access user data and authentication state for UI rendering.

Configuration

Environment Variables

Add Clerk credentials to your environment file:

.env.local
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxx
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx

The publishable key is safe to expose client-side (hence the NEXT_PUBLIC_ prefix). The secret key must remain server-side only.

Getting your keys: Sign up at clerk.com, create an application, and find your keys in the API Keys section of the dashboard.

Implementation

Middleware Protection

The middleware protects all routes except authentication pages and static assets:

src/lib/clerk/middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"

const isPublicRoute = createRouteMatcher(["/auth(.*)"])

export const middleware = clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) await auth.protect()
})
src/middleware.ts
import { middleware as clerkMiddleware } from "@/lib/clerk/middleware"

export { middleware }

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
}

How it works: Every request passes through the middleware. If the route isn't public and the user isn't authenticated, Clerk redirects to /auth/sign-in. If authenticated, the request proceeds normally.

Root Layout Provider

Wrap the application with ClerkProvider to enable Clerk throughout the app:

src/app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs"

export default async function Layout({ children }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

The provider manages session state, handles token refresh, and provides authentication context to all components.

Authentication Pages

Clerk provides pre-built UI components for sign-in and sign-up flows:

src/app/auth/sign-in/page.tsx
import { SignIn } from "@clerk/nextjs"

export default function Page() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        appearance={{
          elements: {
            rootBox: "mx-auto",
            card: "shadow-lg",
          },
        }}
        routing="path"
        path="/auth/sign-in"
        signUpUrl="/auth/sign-up"
      />
    </div>
  )
}

The SignIn component handles the entire authentication flow: email/password, social logins, MFA, and error handling.

src/app/auth/sign-up/page.tsx
import { SignUp } from "@clerk/nextjs"

export default function Page() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignUp
        appearance={{
          elements: {
            rootBox: "mx-auto",
            card: "shadow-lg",
          },
        }}
        routing="path"
        path="/auth/sign-up"
        signInUrl="/auth/sign-in"
      />
    </div>
  )
}

The SignUp component handles user registration with email verification and optional social sign-up.

Usage Patterns

Server Components

Server Components use the auth() function to get authentication tokens for API requests:

Server Component or Server Action
import { auth } from "@clerk/nextjs/server"

import { createApiClient } from "@/lib/api-client"

export async function getSchedules() {
  const { getToken } = await auth()
  const token = await getToken()

  if (!token) {
    throw new Error("Unauthorized")
  }

  const apiClient = createApiClient(token)
  const data = await apiClient.get("schedules").json()

  return data
}

Why this pattern? Tokens stay on the server, never exposed to the client. Server Actions can safely call authenticated APIs without CORS issues or token leakage.

Client Components

Client components use hooks to access user data and authentication state:

Client Component
"use client"

import { useUser } from "@clerk/nextjs"

export function UserProfile() {
  const { user, isLoaded } = useUser()

  if (!isLoaded) {
    return <Skeleton />
  }

  if (!user) {
    return <div>Not signed in</div>
  }

  return (
    <div>
      <p>Welcome, {user.firstName}!</p>
      <p>{user.primaryEmailAddress?.emailAddress}</p>
    </div>
  )
}

Available hooks:

  • useUser() - Current user data and loading state
  • useAuth() - Authentication state and sign-out function
  • useClerk() - Full Clerk client instance for advanced operations

User Button

Clerk provides a pre-built user menu component:

import { UserButton } from "@clerk/nextjs"

export function Header() {
  return (
    <header>
      <nav>
        {/* Your navigation */}
      </nav>
      <UserButton
        appearance={{
          elements: {
            avatarBox: "w-10 h-10",
          },
        }}
        afterSignOutUrl="/auth/sign-in"
      />
    </header>
  )
}

The UserButton displays the user's avatar and opens a menu with profile management and sign-out options.

API Integration

Creating Authenticated API Client

The application uses a centralized API client factory that automatically adds authentication tokens:

src/lib/api-client.ts
import ky from "ky"

export function createApiClient(token?: string | null) {
  const baseURL = process.env.NEXT_PUBLIC_API_URL

  const headers: HeadersInit = {
    "Content-Type": "application/json",
  }

  if (token) {
    headers.Authorization = `Bearer ${token}`
  }

  return ky.create({
    prefixUrl: baseURL,
    headers,
    timeout: 30_000,
    retry: {
      limit: 2,
      methods: ["get", "put", "delete"],
      statusCodes: [408, 429, 500, 502, 503, 504],
    },
  })
}

Usage in Server Actions:

const { getToken } = await auth()
const token = await getToken()
const apiClient = createApiClient(token)

const data = await apiClient.get("endpoint").json()

Token Management

Clerk automatically handles token refresh. Tokens are short-lived (typically 1 hour) and refresh transparently when they expire. The application never needs to manually refresh tokens.

Token storage: Clerk stores tokens in HTTP-only cookies, making them inaccessible to JavaScript and protecting against XSS attacks.

Customization

Appearance Theming

Customize Clerk components to match your application's design:

<SignIn
  appearance={{
    variables: {
      colorPrimary: "#3b82f6",
      colorBackground: "#ffffff",
      colorText: "#1f2937",
    },
    elements: {
      card: "shadow-xl rounded-lg",
      headerTitle: "text-2xl font-bold",
      formButtonPrimary: "bg-blue-600 hover:bg-blue-700",
    },
  }}
/>

Theme integration: Use CSS variables from your Material-UI theme to keep Clerk components consistent with the rest of the application.

Social Providers

Enable social authentication in the Clerk Dashboard under Social Connections. Supported providers include Google, GitHub, Microsoft, Facebook, and more.

Once enabled, social login buttons automatically appear in the SignIn and SignUp components.

Multi-Factor Authentication

Enable MFA in the Clerk Dashboard under User & Authentication → Multi-factor. Users can then enable MFA in their profile settings.

MFA options include SMS codes and authenticator apps (TOTP).

Sign Out

Client-Side Sign Out

Use the useClerk hook to sign out from client components:

"use client"

import { useClerk } from "@clerk/nextjs"

export function SignOutButton() {
  const { signOut } = useClerk()

  return (
    <button onClick={() => signOut()}>
      Sign Out
    </button>
  )
}

Server-Side Sign Out

For server-side sign out (e.g., after a security event), revoke the session via API route:

src/app/auth/clerk/sign-out/route.ts
import { NextResponse } from "next/server"
import { auth, clerkClient } from "@clerk/nextjs/server"

export async function GET() {
  const { sessionId } = await auth()
  const client = await clerkClient()

  if (sessionId) {
    await client.sessions.revokeSession(sessionId)
  }

  const res = new NextResponse(undefined, { status: 307 })
  res.cookies.delete("__session")
  res.headers.set("Location", "/auth/sign-in")

  return res
}

Security Considerations

Troubleshooting

Best Practices


Next: Learn about Data Fetching patterns with Server Actions and React Query.