home / skills / jeremylongshore / claude-code-plugins-plus-skills / clerk-core-workflow-b

This skill helps you implement and manage Clerk session, middleware protection, and token handling across routes for secure web apps.

npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill clerk-core-workflow-b

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.0 KB
---
name: clerk-core-workflow-b
description: |
  Implement session management and middleware with Clerk.
  Use when managing user sessions, configuring route protection,
  or implementing token refresh logic.
  Trigger with phrases like "clerk session", "clerk middleware",
  "clerk route protection", "clerk token", "clerk JWT".
allowed-tools: Read, Write, Edit, Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
---

# Clerk Core Workflow B: Session & Middleware

## Overview
Manage user sessions, protect routes with middleware, and handle JWT tokens.

## Prerequisites
- Clerk SDK installed and configured
- Authentication flows implemented
- Understanding of Next.js middleware

## Instructions

### Step 1: Advanced Middleware Configuration
```typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/webhooks(.*)',
  '/api/public(.*)'
])

const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isAPIRoute = createRouteMatcher(['/api/(.*)'])

export default clerkMiddleware(async (auth, request) => {
  const { userId, orgRole, sessionClaims } = await auth()

  // Allow public routes
  if (isPublicRoute(request)) {
    return NextResponse.next()
  }

  // Require authentication for all other routes
  if (!userId) {
    const signInUrl = new URL('/sign-in', request.url)
    signInUrl.searchParams.set('redirect_url', request.url)
    return NextResponse.redirect(signInUrl)
  }

  // Admin route protection
  if (isAdminRoute(request)) {
    if (orgRole !== 'org:admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url))
    }
  }

  // Add custom headers for API routes
  if (isAPIRoute(request)) {
    const response = NextResponse.next()
    response.headers.set('x-user-id', userId)
    return response
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!_next|[^?]*\\.(?:html?|css|js|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)', '/']
}
```

### Step 2: Session Management
```typescript
'use client'
import { useSession, useAuth } from '@clerk/nextjs'

export function SessionManager() {
  const { session, isLoaded } = useSession()
  const { signOut } = useAuth()

  if (!isLoaded) return <div>Loading session...</div>
  if (!session) return <div>No active session</div>

  const handleSignOutAll = async () => {
    // Sign out from all devices
    await signOut({ sessionId: 'all' })
  }

  const handleSignOutCurrent = async () => {
    // Sign out from current session only
    await signOut()
  }

  return (
    <div>
      <h2>Session Info</h2>
      <p>Session ID: {session.id}</p>
      <p>Created: {new Date(session.createdAt).toLocaleString()}</p>
      <p>Last Active: {new Date(session.lastActiveAt).toLocaleString()}</p>
      <p>Expires: {new Date(session.expireAt).toLocaleString()}</p>

      <div className="space-x-2">
        <button onClick={handleSignOutCurrent}>Sign Out</button>
        <button onClick={handleSignOutAll}>Sign Out All Devices</button>
      </div>
    </div>
  )
}
```

### Step 3: Token Management
```typescript
'use client'
import { useAuth } from '@clerk/nextjs'

export function useClerkToken() {
  const { getToken, isLoaded, isSignedIn } = useAuth()

  const fetchWithAuth = async (url: string, options: RequestInit = {}) => {
    if (!isLoaded || !isSignedIn) {
      throw new Error('Not authenticated')
    }

    const token = await getToken()

    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    })
  }

  const fetchWithCustomTemplate = async (url: string, template: string) => {
    const token = await getToken({ template })

    return fetch(url, {
      headers: {
        Authorization: `Bearer ${token}`
      }
    })
  }

  return { fetchWithAuth, fetchWithCustomTemplate, getToken }
}
```

### Step 4: Server-Side Session Validation
```typescript
// app/api/protected/route.ts
import { auth } from '@clerk/nextjs/server'
import { headers } from 'next/headers'

export async function GET() {
  const { userId, sessionId, sessionClaims } = await auth()

  if (!userId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Access session claims
  const email = sessionClaims?.email as string
  const role = sessionClaims?.metadata?.role as string

  // Validate session freshness
  const sessionAge = Date.now() - (sessionClaims?.iat ?? 0) * 1000
  const maxAge = 60 * 60 * 1000 // 1 hour

  if (sessionAge > maxAge) {
    return Response.json({ error: 'Session expired' }, { status: 401 })
  }

  return Response.json({
    userId,
    sessionId,
    email,
    role
  })
}
```

### Step 5: Multi-Session Support
```typescript
'use client'
import { useSessionList, useSession } from '@clerk/nextjs'

export function SessionList() {
  const { sessions, isLoaded, setActive } = useSessionList()
  const { session: currentSession } = useSession()

  if (!isLoaded) return <div>Loading sessions...</div>

  return (
    <div>
      <h2>Active Sessions</h2>
      <ul>
        {sessions?.map((session) => (
          <li key={session.id}>
            <span>{session.id}</span>
            <span>{session.id === currentSession?.id ? ' (current)' : ''}</span>
            <button onClick={() => setActive({ session: session.id })}>
              Switch
            </button>
            <button onClick={() => session.remove()}>
              Revoke
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

## Output
- Protected routes with middleware
- Session management UI
- Token refresh handling
- Multi-session support

## Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| Session not found | Expired or revoked | Redirect to sign-in |
| Token expired | JWT lifetime exceeded | Call getToken() for fresh token |
| Middleware loop | Incorrect matcher | Check matcher regex excludes static files |
| Headers already sent | Response already started | Check middleware order |

## Examples

### Rate-Limited Middleware
```typescript
import { clerkMiddleware } from '@clerk/nextjs/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s')
})

export default clerkMiddleware(async (auth, request) => {
  const { userId } = await auth()

  if (userId) {
    const { success } = await ratelimit.limit(userId)
    if (!success) {
      return Response.json({ error: 'Rate limited' }, { status: 429 })
    }
  }
})
```

## Resources
- [Middleware Guide](https://clerk.com/docs/references/nextjs/clerk-middleware)
- [Session Management](https://clerk.com/docs/authentication/configuration/session-options)
- [JWT Templates](https://clerk.com/docs/backend-requests/making/jwt-templates)

## Next Steps
Proceed to `clerk-common-errors` for troubleshooting common issues.

Overview

This skill implements session management and route protection using Clerk. It provides middleware patterns for public, admin, and API routes, client-side session UI components, token handling helpers, and server-side session validation. Use it to add secure, scalable authentication flows and multi-session support to Next.js apps.

How this skill works

Middleware inspects incoming requests and Clerk auth state to allow public routes, enforce sign-in, restrict admin routes, and inject user headers for API endpoints. Client hooks manage session state, sign-out actions, session lists, and token retrieval. Server-side handlers validate session claims, check freshness, and return structured errors for unauthorized or expired sessions.

When to use it

  • Protect pages or API endpoints and redirect unauthenticated users to sign-in
  • Enforce organization admin access for sensitive routes
  • Attach user identity headers for backend APIs
  • Implement client flows to sign out current or all sessions
  • Fetch backend resources with up-to-date JWTs using getToken

Best practices

  • Keep a clear public route matcher to avoid middleware loops or blocking static assets
  • Always call getToken() right before protected fetches to refresh JWTs
  • Validate session age and claims server-side to prevent replay or long-lived sessions
  • Set short JWT lifetimes and rely on refresh via Clerk to limit exposure
  • Provide explicit user feedback for session expiration and sign-in redirects

Example use cases

  • Redirect anonymous visitors to /sign-in with a redirect_url back to the original page
  • Limit /admin routes to users with org:admin role and redirect unauthorized users
  • Add x-user-id header on API routes so downstream services can identify callers
  • Render a session list UI allowing users to revoke or switch between active sessions
  • Wrap fetch calls with fetchWithAuth to automatically include fresh Bearer tokens

FAQ

What if middleware keeps redirecting my pages?

Confirm the matcher excludes _next and static assets and that public routes are listed correctly to avoid redirect loops.

How do I handle expired tokens on the client?

Call getToken() before requests; if it fails, prompt re-authentication or redirect to sign-in.

Can users manage multiple sessions?

Yes. Provide a session list UI to switch active sessions or revoke individual sessions and support sign-out from all devices.