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:
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxx
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxThe 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:
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
const isPublicRoute = createRouteMatcher(["/auth(.*)"])
export const middleware = clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) await auth.protect()
})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:
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:
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.
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:
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:
"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 stateuseAuth()- Authentication state and sign-out functionuseClerk()- 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:
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:
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.