Claude-skill-registry clerk-security-basics
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clerk-security-basics" ~/.claude/skills/majiayu000-claude-skill-registry-clerk-security-basics && rm -rf "$T"
manifest:
skills/data/clerk-security-basics/SKILL.mdsource content
Clerk Security Basics
Overview
Implement security best practices for Clerk authentication in your application.
Prerequisites
- Clerk SDK installed and configured
- Understanding of authentication security concepts
- Production deployment planned or active
Instructions
Step 1: Secure Environment Variables
# .env.local (development) NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... # .env.production (production - use secrets manager) # NEVER commit production keys to git # Use Vercel/Railway/AWS Secrets Manager # .gitignore .env.local .env.production .env*.local
// lib/env.ts - Validate environment at startup const requiredEnvVars = [ 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY' ] export function validateEnv() { for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { throw new Error(`Missing required environment variable: ${envVar}`) } } // Validate key format const pk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY! if (!pk.startsWith('pk_test_') && !pk.startsWith('pk_live_')) { throw new Error('Invalid publishable key format') } }
Step 2: Secure Middleware Configuration
// middleware.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' const isPublicRoute = createRouteMatcher([ '/', '/sign-in(.*)', '/sign-up(.*)', '/api/webhooks(.*)' ]) const isAdminRoute = createRouteMatcher(['/admin(.*)']) const isSensitiveRoute = createRouteMatcher(['/api/admin(.*)', '/api/billing(.*)']) export default clerkMiddleware(async (auth, request) => { const { userId, orgRole } = await auth() // Security headers const response = NextResponse.next() response.headers.set('X-Frame-Options', 'DENY') response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') // Protect routes if (!isPublicRoute(request)) { if (!userId) { return NextResponse.redirect(new URL('/sign-in', request.url)) } } // Admin routes require admin role if (isAdminRoute(request) && orgRole !== 'org:admin') { return NextResponse.redirect(new URL('/unauthorized', request.url)) } // Log sensitive route access if (isSensitiveRoute(request)) { console.log('Sensitive route accessed:', { path: request.nextUrl.pathname, userId, timestamp: new Date().toISOString() }) } return response })
Step 3: Secure API Routes
// app/api/protected/route.ts import { auth } from '@clerk/nextjs/server' import { headers } from 'next/headers' export async function POST(request: Request) { // 1. Verify authentication const { userId, sessionId } = await auth() if (!userId) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } // 2. Validate request origin (CSRF protection) const headersList = await headers() const origin = headersList.get('origin') const allowedOrigins = [ process.env.NEXT_PUBLIC_APP_URL, 'https://yourdomain.com' ] if (origin && !allowedOrigins.includes(origin)) { return Response.json({ error: 'Invalid origin' }, { status: 403 }) } // 3. Validate content type const contentType = headersList.get('content-type') if (!contentType?.includes('application/json')) { return Response.json({ error: 'Invalid content type' }, { status: 400 }) } // 4. Parse and validate body let body try { body = await request.json() } catch { return Response.json({ error: 'Invalid JSON' }, { status: 400 }) } // 5. Process request return Response.json({ success: true }) }
Step 4: Secure Webhook Handling
// app/api/webhooks/clerk/route.ts import { Webhook } from 'svix' import { headers } from 'next/headers' import { WebhookEvent } from '@clerk/nextjs/server' export async function POST(req: Request) { const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET if (!WEBHOOK_SECRET) { console.error('CLERK_WEBHOOK_SECRET not configured') return Response.json({ error: 'Configuration error' }, { status: 500 }) } // Get headers const headerPayload = await headers() const svix_id = headerPayload.get('svix-id') const svix_timestamp = headerPayload.get('svix-timestamp') const svix_signature = headerPayload.get('svix-signature') // Validate required headers if (!svix_id || !svix_timestamp || !svix_signature) { return Response.json({ error: 'Missing svix headers' }, { status: 400 }) } // Verify webhook const body = await req.text() const wh = new Webhook(WEBHOOK_SECRET) let evt: WebhookEvent try { evt = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature }) as WebhookEvent } catch (err) { console.error('Webhook verification failed:', err) return Response.json({ error: 'Invalid signature' }, { status: 400 }) } // Process verified event const eventType = evt.type // Idempotency check (prevent replay attacks) const processed = await checkIfProcessed(svix_id) if (processed) { return Response.json({ message: 'Already processed' }) } // Handle event await processWebhookEvent(evt) // Mark as processed await markAsProcessed(svix_id) return Response.json({ success: true }) }
Step 5: Session Security
// lib/session-security.ts import { auth } from '@clerk/nextjs/server' export async function validateSession() { const { userId, sessionClaims } = await auth() if (!userId) { throw new Error('No session') } // Check session age const issuedAt = sessionClaims?.iat const maxAge = 60 * 60 // 1 hour in seconds if (issuedAt && Date.now() / 1000 - issuedAt > maxAge) { throw new Error('Session too old, please re-authenticate') } return { userId, sessionClaims } } // Force re-authentication for sensitive operations export async function requireFreshAuth() { const { userId, sessionClaims } = await auth() if (!userId) { throw new Error('Not authenticated') } const issuedAt = sessionClaims?.iat const freshThreshold = 5 * 60 // 5 minutes if (issuedAt && Date.now() / 1000 - issuedAt > freshThreshold) { throw new Error('Please re-authenticate for this action') } return { userId } }
Output
- Secure environment configuration
- Hardened middleware
- Protected API routes
- Verified webhook handling
Security Checklist
- Production keys stored in secrets manager
- Environment variables validated at startup
- Middleware protects all sensitive routes
- API routes validate authentication
- Webhooks verified with svix
- Security headers configured
- HTTPS enforced in production
- Session timeouts configured
Resources
Next Steps
Proceed to
clerk-prod-checklist for production readiness.