AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Authentication Authorization Clerk

skill-harperaa-secure-claude-skills-auth-security · by harperaa

Implement secure authentication and authorization using Clerk. Use this skill when you need to authenticate users, protect routes, check permissions, implement subscription-based access control, or integrate Clerk with your application. Triggers include "authentication", "auth", "authorization", "Clerk", "protect route", "check user", "sign in", "session", "permissions", "subscription access".

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-harperaa-secure-claude-skills-auth-security

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-harperaa-secure-claude-skills-auth-security)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
7mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Authentication Authorization Clerk? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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 Loading...;
  }

  if (!userId) {
    return Please sign in;
  }

  return (
    
      Welcome, {user.firstName}!
      Email: {user.primaryEmailAddress?.emailAddress}
    
  );
}

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 (
     !has({ plan: "free_user" })}
      fallback={}
    >
      
        {/* Premium feature content */}
        Premium Feature
        This is only visible to paid subscribers
      
    
  );
}

function UpgradePrompt() {
  return (
    
      Upgrade Required
      This feature is available on our paid plans
      View Plans
    
  );
}

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 (
    
      
    
  );
}

Basic Sign-Up Page

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

export default function SignUpPage() {
  return (
    
      
    
  );
}

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 (
    
      
        Home
        {isSignedIn ? (
          <>
            Dashboard
            
          
        ) : (
          <>
            Sign In
            Sign Up
          
        )}
      
    
  );
}

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 Access denied;
// 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
}

2. 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);
}

3. 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();
  }
});

4. 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 Loading...;
  }

  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 checksDON'T trust client-side auth state for securityDON'T forget to check resource ownershipDON'T expose sensitive data based on authentication aloneDON'T hardcode auth logic (use Clerk's utilities)DON'T forget to handle session expiration

DO use auth() on server for every protected operationDO verify resource ownership before allowing accessDO protect routes with middlewareDO use subscription/role checks for premium featuresDO sync users to your database via webhooksDO handle auth errors gracefully

References

  • Clerk Documentation: https://clerk.com/docs
  • Clerk Security: https://clerk.com/docs/security
  • Clerk Next.js Integration: https://clerk.com/docs/references/nextjs/overview
  • OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/AuthenticationCheatSheet.html

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 Rou

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.