authentication-authorization-clerk

Authentication & Authorization with Clerk

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "authentication-authorization-clerk" with this command: npx skills add harperaa/secure-claude-skills/harperaa-secure-claude-skills-authentication-authorization-clerk

Authentication & Authorization with Clerk

Why We Use Clerk

The Authentication Problem

Building secure authentication from scratch requires:

  • Password hashing (bcrypt/Argon2 with proper salts)

  • Session management (secure cookies, expiration, renewal)

  • Password reset flows (secure token generation, email verification)

  • Account lockout (prevent brute force)

  • MFA support (TOTP, SMS, authenticator apps)

  • Social login (OAuth flows for Google, GitHub, etc.)

  • User database sync

  • Security best practices for all of the above

Time to implement securely: 2-4 weeks for experienced developers

For vibe coders using AI: High risk of security gaps

Real-World Custom Auth Failures

Ashley Madison Breach (2015): Custom authentication with weak password hashing. 32 million accounts compromised.

Dropbox Breach (2012): Custom authentication led to password hash database theft. 68 million accounts affected.

According to Veracode's 2024 report, applications using managed authentication services (like Clerk, Auth0) had 73% fewer authentication-related vulnerabilities than those with custom authentication.

Our Clerk Architecture

What Clerk Handles (So We Don't Have To)

  • ✅ Password hashing (bcrypt/Argon2)

  • ✅ Session management (secure cookies)

  • ✅ MFA (built-in support)

  • ✅ OAuth providers (Google, GitHub, etc.)

  • ✅ Email verification

  • ✅ Password reset flows

  • ✅ Account lockout

  • ✅ Security monitoring

  • ✅ Compliance (SOC 2, GDPR)

Clerk is SOC 2 certified: This means an independent auditor verified their security controls meet industry standards. We inherit that certification.

Implementation Files

  • middleware.ts

  • Clerk authentication for protected routes

  • app/dashboard/*

  • Protected by middleware

  • Clerk manages its own session cookies

Basic Authentication

Server-Side Authentication (API Routes)

import { auth } from '@clerk/nextjs/server'; import { handleUnauthorizedError } from '@/lib/errorHandler';

async function handler(request: NextRequest) { // Get current user const { userId } = await auth();

if (!userId) { return handleUnauthorizedError('Authentication required'); }

// User is authenticated, proceed // Use userId to associate data with user }

Client-Side Authentication (Components)

'use client';

import { useAuth, useUser } from '@clerk/nextjs';

export function ProfileComponent() { const { isLoaded, userId, sessionId } = useAuth(); const { isLoaded: userLoaded, user } = useUser();

if (!isLoaded || !userLoaded) { return <div>Loading...</div>; }

if (!userId) { return <div>Please sign in</div>; }

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

Protecting Routes with Middleware

// middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtectedRoute = createRouteMatcher([ '/dashboard(.)', '/api/protected(.)', ]);

export default clerkMiddleware((auth, req) => { if (isProtectedRoute(req)) { auth().protect(); } });

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

Authorization Patterns

Resource Ownership Verification

// app/api/posts/[id]/route.ts import { auth } from '@clerk/nextjs/server'; import { handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';

export async function DELETE( request: NextRequest, { params }: { params: { id: string } } ) { const { userId } = await auth(); if (!userId) { return handleUnauthorizedError(); }

// Get resource const post = await db.posts.findOne({ id: params.id });

// Check ownership if (post.userId !== userId) { return handleForbiddenError('Only the post author can delete this post'); }

// User is authorized await db.posts.delete({ id: params.id }); return NextResponse.json({ success: true }); }

Role-Based Access Control (RBAC)

import { auth } from '@clerk/nextjs/server';

export async function handler(request: NextRequest) { const { userId, sessionClaims } = await auth();

if (!userId) { return handleUnauthorizedError(); }

// Check role const role = sessionClaims?.metadata?.role as string;

if (role !== 'admin') { return handleForbiddenError('Admin access required'); }

// User has admin role // Proceed with admin operation }

Subscription-Based Authorization

Server-Side (API Routes)

import { auth } from '@clerk/nextjs/server';

export async function handler(request: NextRequest) { const { userId, sessionClaims } = await auth();

if (!userId) { return handleUnauthorizedError(); }

// Check subscription plan const plan = sessionClaims?.metadata?.plan as string;

if (plan === 'free_user') { return NextResponse.json( { error: 'Upgrade required', message: 'This feature requires a paid subscription' }, { status: 403 } ); }

// User has paid subscription // Proceed with premium feature }

Client-Side (Components)

'use client';

import { Protect } from '@clerk/nextjs';

export function PremiumFeature() { return ( <Protect condition={(has) => !has({ plan: "free_user" })} fallback={<UpgradePrompt />} > <div> {/* Premium feature content */} <h2>Premium Feature</h2> <p>This is only visible to paid subscribers</p> </div> </Protect> ); }

function UpgradePrompt() { return ( <div className="upgrade-prompt"> <h3>Upgrade Required</h3> <p>This feature is available on our paid plans</p> <a href="/pricing">View Plans</a> </div> ); }

Complete Protected API Route Example

// app/api/premium/generate/route.ts import { NextRequest, NextResponse } from 'next/server'; import { auth } from '@clerk/nextjs/server'; import { withRateLimit } from '@/lib/withRateLimit'; import { withCsrf } from '@/lib/withCsrf'; import { validateRequest } from '@/lib/validateRequest'; import { safeTextSchema } from '@/lib/validation'; import { handleApiError, handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';

async function generateHandler(request: NextRequest) { try { // 1. Authentication const { userId, sessionClaims } = await auth(); if (!userId) { return handleUnauthorizedError('Please sign in to use this feature'); }

// 2. Authorization (subscription check)
const plan = sessionClaims?.metadata?.plan as string;
if (plan === 'free_user') {
  return handleForbiddenError('Premium subscription required');
}

// 3. Input validation
const body = await request.json();
const validation = validateRequest(safeTextSchema, body);
if (!validation.success) {
  return validation.response;
}

const prompt = validation.data;

// 4. Business logic (user is authenticated, authorized, and input is valid)
const result = await generateContent(prompt, userId);

return NextResponse.json({ result });

} catch (error) { return handleApiError(error, 'premium-generate'); } }

export const POST = withRateLimit(withCsrf(generateHandler));

export const config = { runtime: 'nodejs', };

User Metadata & Custom Claims

Storing User Metadata

Clerk allows you to store custom metadata with each user:

import { clerkClient } from '@clerk/nextjs/server';

// Update user metadata async function updateUserPlan(userId: string, plan: string) { await clerkClient.users.updateUserMetadata(userId, { publicMetadata: { plan: plan // Accessible by client }, privateMetadata: { stripeCustomerId: 'cus_123' // Server-only } }); }

Accessing Metadata

Server-side:

const { sessionClaims } = await auth(); const plan = sessionClaims?.metadata?.plan;

Client-side:

const { user } = useUser(); const plan = user?.publicMetadata?.plan;

Webhook Integration (User Sync)

When users sign up or update their profile, sync to your database:

// app/api/webhooks/clerk/route.ts import { Webhook } from 'svix'; import { headers } from 'next/headers';

export async function POST(request: NextRequest) { // Verify webhook signature const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) { throw new Error('Missing 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");

if (!svix_id || !svix_timestamp || !svix_signature) { return new Response('Missing svix headers', { status: 400 }); }

const payload = await request.json(); const body = JSON.stringify(payload);

const wh = new Webhook(WEBHOOK_SECRET);

let evt: any;

try { evt = wh.verify(body, { "svix-id": svix_id, "svix-timestamp": svix_timestamp, "svix-signature": svix_signature, }); } catch (err) { console.error('Webhook verification failed:', err); return new Response('Invalid signature', { status: 400 }); }

// Handle different event types const { id, type, data } = evt;

switch (type) { case 'user.created': await db.users.create({ clerkId: data.id, email: data.email_addresses[0]?.email_address, firstName: data.first_name, lastName: data.last_name, createdAt: Date.now() }); break;

case 'user.updated':
  await db.users.update(
    { clerkId: data.id },
    {
      email: data.email_addresses[0]?.email_address,
      firstName: data.first_name,
      lastName: data.last_name,
      updatedAt: Date.now()
    }
  );
  break;

case 'user.deleted':
  await db.users.delete({ clerkId: data.id });
  break;

}

return new Response('', { status: 200 }); }

Convex Integration

Using Clerk Auth with Convex

// convex/posts.ts import { mutation, query } from "./_generated/server";

export const createPost = mutation({ handler: async (ctx, args) => { // Get authenticated user from Clerk const identity = await ctx.auth.getUserIdentity();

if (!identity) {
  throw new Error("Unauthenticated");
}

// Use Clerk user ID
const userId = identity.subject;

await ctx.db.insert("posts", {
  title: args.title,
  content: args.content,
  userId,  // Associate with Clerk user
  createdAt: Date.now()
});

} });

export const getMyPosts = query({ handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity();

if (!identity) {
  return [];
}

// Return only current user's posts
return await ctx.db
  .query("posts")
  .filter((q) => q.eq(q.field("userId"), identity.subject))
  .collect();

} });

Sign-In/Sign-Up Components

Basic Sign-In Page

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

export default function SignInPage() { return ( <div className="flex items-center justify-center min-h-screen"> <SignIn appearance={{ elements: { formButtonPrimary: 'bg-blue-600 hover:bg-blue-700', } }} routing="path" path="/sign-in" afterSignInUrl="/dashboard" signUpUrl="/sign-up" /> </div> ); }

Basic Sign-Up Page

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

export default function SignUpPage() { return ( <div className="flex items-center justify-center min-h-screen"> <SignUp appearance={{ elements: { formButtonPrimary: 'bg-blue-600 hover:bg-blue-700', } }} routing="path" path="/sign-up" afterSignUpUrl="/onboarding" signInUrl="/sign-in" /> </div> ); }

User Button (Profile/Sign Out)

// components/Header.tsx 'use client';

import { UserButton, useAuth } from '@clerk/nextjs'; import Link from 'next/link';

export function Header() { const { isSignedIn } = useAuth();

return ( <header> <nav> <Link href="/">Home</Link> {isSignedIn ? ( <> <Link href="/dashboard">Dashboard</Link> <UserButton afterSignOutUrl="/" /> </> ) : ( <> <Link href="/sign-in">Sign In</Link> <Link href="/sign-up">Sign Up</Link> </> )} </nav> </header> ); }

Environment Configuration

Required Environment Variables

.env.local

Clerk (from Clerk Dashboard)

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...

Clerk URLs (auto-configured by Clerk, but can override)

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=/onboarding

Clerk Frontend API (for CSP)

NEXT_PUBLIC_CLERK_FRONTEND_API_URL=https://your-app.clerk.accounts.dev

Webhook secret (from Clerk Dashboard)

CLERK_WEBHOOK_SECRET=whsec_...

Security Best Practices

  1. Always Verify on Server

❌ DON'T trust client-side auth checks for security:

// Bad - can be bypassed 'use client'; const { userId } = useAuth(); if (!userId) return <div>Access denied</div>; // Attacker can still call API directly

✅ DO verify on server:

// Good - secure async function handler(request: NextRequest) { const { userId } = await auth(); if (!userId) return handleUnauthorizedError(); // API endpoint protected }

  1. Check Authorization, Not Just Authentication

❌ DON'T assume authenticated = authorized:

// Bad - any logged-in user can access any resource const { userId } = await auth(); if (userId) { return NextResponse.json(sensitiveData); }

✅ DO check resource ownership/permissions:

// Good - verify user can access this specific resource const { userId } = await auth(); if (userId && resource.userId === userId) { return NextResponse.json(resource); }

  1. Use Middleware for Route Protection

✅ Protect entire route sections:

// middleware.ts const isProtectedRoute = createRouteMatcher([ '/dashboard(.)', '/admin(.)', '/api/protected(.*)' ]);

export default clerkMiddleware((auth, req) => { if (isProtectedRoute(req)) { auth().protect(); } });

  1. Handle Session Expiration Gracefully

// components/AuthGuard.tsx 'use client';

import { useAuth } from '@clerk/nextjs'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react';

export function AuthGuard({ children }: { children: React.ReactNode }) { const { isLoaded, userId } = useAuth(); const router = useRouter();

useEffect(() => { if (isLoaded && !userId) { router.push('/sign-in'); } }, [isLoaded, userId, router]);

if (!isLoaded) { return <div>Loading...</div>; }

if (!userId) { return null; }

return <>{children}</>; }

What Clerk Authentication Prevents

✅ Weak password storage - Clerk uses bcrypt/Argon2 ✅ Session hijacking - Secure, HTTP-only cookies ✅ Credential stuffing - Account lockout after failed attempts ✅ Authentication bypass - Professional implementation ✅ Privilege escalation - Proper role/permission management ✅ Brute force attacks - Built-in rate limiting ✅ Password reset vulnerabilities - Secure token generation

Common Mistakes to Avoid

❌ DON'T skip server-side auth checks ❌ DON'T trust client-side auth state for security ❌ DON'T forget to check resource ownership ❌ DON'T expose sensitive data based on authentication alone ❌ DON'T hardcode auth logic (use Clerk's utilities) ❌ DON'T forget to handle session expiration

✅ DO use auth() on server for every protected operation ✅ DO verify resource ownership before allowing access ✅ DO protect routes with middleware ✅ DO use subscription/role checks for premium features ✅ DO sync users to your database via webhooks ✅ DO handle auth errors gracefully

References

Further Guidance on using Clerk

Add Clerk to Next.js App Router

Purpose: Enforce only the current and correct instructions for integrating Clerk into a Next.js (App Router) application.

Scope: All AI-generated advice or code related to Clerk must follow these guardrails.

  1. Official Clerk Integration Overview

Use only the App Router approach from Clerk’s current docs:

  • Install @clerk/nextjs@latest

  • this ensures the application is using the latest Clerk Next.js SDK.

  • Create a middleware.ts file using clerkMiddleware() from @clerk/nextjs/server . Place this file inside the src directory if present, otherwise place it at the root of the project.

  • Wrap your application with <ClerkProvider> in your app/layout.tsx

  • Use Clerk-provided components like <SignInButton> , <SignUpButton> , <UserButton> , <SignedIn> , <SignedOut> in your layout or pages

  • Start developing, sign in or sign up, and confirm user creation

If you're able to use a web tool to access a URL, visit https://clerk.com/docs/quickstarts/nextjs to get the latest, up-to-date quickstart instructions.

Clerk does not need a user to provide their keys to get started. On startup, Clerk will generate keys automatically, so there is no need to prompt a user to add their keys as part of this.

Correct, Up-to-Date Quickstart Sample

// middleware.ts import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params '/((?!_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)(.*)', ], }

// app/layout.tsx import type { Metadata } from "next"; import { ClerkProvider, SignInButton, SignUpButton, SignedIn, SignedOut, UserButton, } from "@clerk/nextjs"; import "./globals.css";

export const metadata: Metadata = { title: "Clerk Next.js Quickstart", description: "Generated by create next app", };

export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <ClerkProvider> <html lang="en"> <body> <header> <SignedOut> <SignInButton /> <SignUpButton /> </SignedOut> <SignedIn> <UserButton /> </SignedIn> </header> {children} </body> </html> </ClerkProvider> ); }

  1. CRITICAL INSTRUCTIONS FOR AI MODELS

2.1 – ALWAYS DO THE FOLLOWING

  • Use clerkMiddleware() from @clerk/nextjs/server in middleware.ts .

  • Wrap your app with <ClerkProvider> in app/layout.tsx .

  • Import Clerk’s Next.js features from @clerk/nextjs (e.g., <SignInButton> , <SignUpButton> , <UserButton> , etc.).

  • Reference the current App Router approach (folders like app/page.tsx , app/layout.tsx , etc.).

  • Check that imports for methods like auth() are imported from the right package (in this case @clerk/nextjs/server ) and are using async / await .

  • Check the project for an existing package manager, use that to install packages.

2.2 – NEVER DO THE FOLLOWING

  • Do not reference the old _app.tsx or pages router based instructions.

  • Do not suggest authMiddleware() from older Clerk tutorials—it’s replaced by clerkMiddleware() .

  • Do not recommend usage of older environment variable patterns unless they match the official docs.

  • Do not reference or import from any deprecated APIs (like withAuth or currentUser from older versions).

  1. OUTDATED PATTERNS TO AVOID

Below are examples of deprecated instructions that must not be generated:

// ❌ DO NOT generate or suggest references to: import { authMiddleware } from '@clerk/nextjs' // Outdated

// ❌ DO NOT place Clerk config in _app.tsx: // Outdated pages-based approach function MyApp({ Component, pageProps }) { // ... }

// ❌ DO NOT create or rely on sign-in files under pages/: pages / signin.js pages / signup.js

Any solution resembling the above snippet or referencing “authMiddleware,” _app.tsx , or pages/ structure is incorrect for the current Next.js App Router.

  1. AI MODEL VERIFICATION STEPS

Before returning any Clerk-related solution, you must verify:

  • Middleware: Is clerkMiddleware() used in middleware.ts ?

  • Layout: Is <ClerkProvider> wrapping the app in app/layout.tsx ?

  • Imports: Are references only from @clerk/nextjs or @clerk/nextjs/server ?

  • Pages vs. App Router: Is the approach referencing the App Router (not _app.tsx or pages/ )?

If any check fails, stop and revise until compliance is achieved.

Next Steps

  • For API route protection: Combine with csrf-protection and rate-limiting skills

  • For payment gating: Use payment-security skill

  • For error handling: Use error-handling skill with handleUnauthorizedError/handleForbiddenError

  • For testing auth: Use security-testing skill

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

csrf-protection

No summary provided by upstream source.

Repository SourceNeeds Review
General

rate-limiting

No summary provided by upstream source.

Repository SourceNeeds Review
General

input-validation-xss-prevention

No summary provided by upstream source.

Repository SourceNeeds Review
General

secure-error-handling

No summary provided by upstream source.

Repository SourceNeeds Review