Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:03 +08:00
commit d3ec204941
27 changed files with 4067 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "clerk-auth",
"description": "Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# clerk-auth
Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.

420
SKILL.md Normal file
View File

@@ -0,0 +1,420 @@
---
name: clerk-auth
description: |
Clerk auth with API version 2025-11-10 breaking changes (billing endpoints, payment_source→payment_method), Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense. Use when: troubleshooting "Missing Clerk Secret Key", JWKS errors, authorizedParties CSRF, JWT size limits (1.2KB), 431 header errors (Vite dev mode), or testing with 424242 OTP.
license: MIT
metadata:
version: 2.0.0
last_verified: 2025-11-22
sdk_versions:
nextjs: 6.35.4
backend: 2.23.2
clerk_react: 5.56.2
testing: 1.13.18
token_savings: ~52%
errors_prevented: 11
breaking_changes: Nov 2025 - API version 2025-11-10 (billing endpoints), Oct 2024 - Next.js v6 async auth()
keywords:
- clerk
- clerk auth
- api version 2025-11-10
- billing api breaking changes
- commerce to billing migration
- payment_source to payment_method
- "@clerk/nextjs"
- "@clerk/backend"
- "@clerk/clerk-react"
- next.js v6 async auth
- next.js 16 support
- pkce custom oauth
- credential stuffing defense
- client trust
- verifyToken
- authorizedParties csrf
- JWT template
- JWT size limit 1.2kb
- 431 request header too large
- vite dev mode clerk
- "@clerk/testing"
- 424242 OTP
- test credentials
- session token script
- "Missing Clerk Secret Key"
- JWKS cache race condition
- core 2 migration
---
# Clerk Auth - Breaking Changes & Error Prevention Guide
**Package Versions**: @clerk/nextjs@6.35.4, @clerk/backend@2.23.2, @clerk/clerk-react@5.56.2, @clerk/testing@1.13.18
**Breaking Changes**: Nov 2025 - API version 2025-11-10, Oct 2024 - Next.js v6 async auth()
**Last Updated**: 2025-11-22
---
## What's New in v6.35.x & API 2025-11-10 (Nov 2025)
### 1. API Version 2025-11-10 (Nov 10, 2025) - BREAKING CHANGES ⚠️
**Affects:** Applications using Clerk Billing/Commerce APIs
**Critical Changes:**
- **Endpoint URLs:** `/commerce/``/billing/` (30+ endpoints)
```
GET /v1/commerce/plans → GET /v1/billing/plans
GET /v1/commerce/statements → GET /v1/billing/statements
POST /v1/me/commerce/checkouts → POST /v1/me/billing/checkouts
```
- **Field Terminology:** `payment_source` → `payment_method`
```typescript
// OLD (deprecated)
{ payment_source_id: "...", payment_source: {...} }
// NEW (required)
{ payment_method_id: "...", payment_method: {...} }
```
- **Removed Fields:** Plans responses no longer include:
- `amount`, `amount_formatted` (use `fee.amount` instead)
- `currency`, `currency_symbol` (use fee objects)
- `payer_type` (use `for_payer_type`)
- `annual_monthly_amount`, `annual_amount`
- **Removed Endpoints:**
- Invoices endpoint (use statements)
- Products endpoint
- **Null Handling:** Explicit rules - `null` means "doesn't exist", omitted means "not asserting existence"
**Migration:** Update SDK to v6.35.0+ which includes support for API version 2025-11-10.
**Official Guide:** https://clerk.com/docs/guides/development/upgrading/upgrade-guides/2025-11-10
### 2. Next.js v6 Async auth() (Oct 2024) - BREAKING CHANGE ⚠️
**Affects:** All Next.js Server Components using `auth()`
```typescript
// ❌ OLD (v5 - synchronous)
const { userId } = auth()
// ✅ NEW (v6 - asynchronous)
const { userId } = await auth()
```
**Also affects:** `auth.protect()` is now async in middleware
```typescript
// ❌ OLD (v5)
auth.protect()
// ✅ NEW (v6)
await auth.protect()
```
**Compatibility:** Next.js 15, 16 supported. Static rendering by default.
### 3. PKCE Support for Custom OAuth (Nov 12, 2025)
Custom OIDC providers and social connections now support PKCE (Proof Key for Code Exchange) for enhanced security in native/mobile applications where client secrets cannot be safely stored.
**Use case:** Mobile apps, native apps, public clients that can't securely store secrets.
### 4. Client Trust: Credential Stuffing Defense (Nov 14, 2025)
Automatic secondary authentication when users sign in from unrecognized devices:
- Activates for users with valid passwords but no 2FA
- No configuration required
- Included in all Clerk plans
**How it works:** Clerk automatically prompts for additional verification (email code, backup code) when detecting sign-in from new device.
### 5. Next.js 16 Support (Nov 2025)
**@clerk/nextjs v6.35.2+** includes cache invalidation improvements for Next.js 16 during sign-out.
---
## Critical Patterns & Error Prevention
### Next.js v6: Async auth() Helper
**Pattern:**
```typescript
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { userId } = await auth() // ← Must await
if (!userId) {
return <div>Unauthorized</div>
}
return <div>User ID: {userId}</div>
}
```
### Cloudflare Workers: authorizedParties (CSRF Prevention)
**CRITICAL:** Always set `authorizedParties` to prevent CSRF attacks
```typescript
import { verifyToken } from '@clerk/backend'
const { data, error } = await verifyToken(token, {
secretKey: c.env.CLERK_SECRET_KEY,
// REQUIRED: Prevent CSRF attacks
authorizedParties: ['https://yourdomain.com'],
})
```
**Why:** Without `authorizedParties`, attackers can use valid tokens from other domains.
**Source:** https://clerk.com/docs/reference/backend/verify-token
---
## JWT Templates - Size Limits & Shortcodes
### JWT Size Limitation: 1.2KB for Custom Claims ⚠️
**Problem**: Browser cookies limited to 4KB. Clerk's default claims consume ~2.8KB, leaving **1.2KB for custom claims**.
**⚠️ Development Note**: When testing custom JWT claims in Vite dev mode, you may encounter **"431 Request Header Fields Too Large"** error. This is caused by Clerk's handshake token in the URL exceeding Vite's 8KB limit. See [Issue #11](#issue-11-431-request-header-fields-too-large-vite-dev-mode) for solution.
**Solution:**
```json
// ✅ GOOD: Minimal claims
{
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role}}"
}
// ❌ BAD: Exceeds limit
{
"bio": "{{user.public_metadata.bio}}", // 6KB field
"all_metadata": "{{user.public_metadata}}" // Entire object
}
```
**Best Practice**: Store large data in database, include only identifiers/roles in JWT.
### Available Shortcodes Reference
| Category | Shortcodes | Example |
|----------|-----------|---------|
| **User ID & Name** | `{{user.id}}`, `{{user.first_name}}`, `{{user.last_name}}`, `{{user.full_name}}` | `"John Doe"` |
| **Contact** | `{{user.primary_email_address}}`, `{{user.primary_phone_address}}` | `"john@example.com"` |
| **Profile** | `{{user.image_url}}`, `{{user.username}}`, `{{user.created_at}}` | `"https://..."` |
| **Verification** | `{{user.email_verified}}`, `{{user.phone_number_verified}}` | `true` |
| **Metadata** | `{{user.public_metadata}}`, `{{user.public_metadata.FIELD}}` | `{"role": "admin"}` |
| **Organization** | `org_id`, `org_slug`, `org_role` (in sessionClaims) | `"org:admin"` |
**Advanced Features:**
- **String Interpolation**: `"{{user.last_name}} {{user.first_name}}"`
- **Conditional Fallbacks**: `"{{user.public_metadata.role || 'user'}}"`
- **Nested Metadata**: `"{{user.public_metadata.profile.interests}}"`
**Official Docs**: https://clerk.com/docs/guides/sessions/jwt-templates
---
## Testing with Clerk
### Test Credentials (Fixed OTP: 424242)
**Test Emails** (no emails sent, fixed OTP):
```
john+clerk_test@example.com
jane+clerk_test@gmail.com
```
**Test Phone Numbers** (no SMS sent, fixed OTP):
```
+12015550100
+19735550133
```
**Fixed OTP Code**: `424242` (works for all test credentials)
### Generate Session Tokens (60-second lifetime)
**Script** (`scripts/generate-session-token.js`):
```bash
# Generate token
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js
# Create new test user
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --create-user
# Auto-refresh token every 50 seconds
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --refresh
```
**Manual Flow**:
1. Create user: `POST /v1/users`
2. Create session: `POST /v1/sessions`
3. Generate token: `POST /v1/sessions/{session_id}/tokens`
4. Use in header: `Authorization: Bearer <token>`
### E2E Testing with Playwright
Install `@clerk/testing` for automatic Testing Token management:
```bash
npm install -D @clerk/testing
```
**Global Setup** (`global.setup.ts`):
```typescript
import { clerkSetup } from '@clerk/testing/playwright'
import { test as setup } from '@playwright/test'
setup('global setup', async ({}) => {
await clerkSetup()
})
```
**Test File** (`auth.spec.ts`):
```typescript
import { setupClerkTestingToken } from '@clerk/testing/playwright'
import { test } from '@playwright/test'
test('sign up', async ({ page }) => {
await setupClerkTestingToken({ page })
await page.goto('/sign-up')
await page.fill('input[name="emailAddress"]', 'test+clerk_test@example.com')
await page.fill('input[name="password"]', 'TestPassword123!')
await page.click('button[type="submit"]')
// Verify with fixed OTP
await page.fill('input[name="code"]', '424242')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
```
**Official Docs**: https://clerk.com/docs/guides/development/testing/overview
---
## Known Issues Prevention
This skill prevents **11 documented issues**:
### Issue #1: Missing Clerk Secret Key
**Error**: "Missing Clerk Secret Key or API Key"
**Source**: https://stackoverflow.com/questions/77620604
**Prevention**: Always set in `.env.local` or via `wrangler secret put`
### Issue #2: API Key → Secret Key Migration
**Error**: "apiKey is deprecated, use secretKey"
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
**Prevention**: Replace `apiKey` with `secretKey` in all calls
### Issue #3: JWKS Cache Race Condition
**Error**: "No JWK available"
**Source**: https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md
**Prevention**: Use @clerk/backend@2.17.2 or later (fixed)
### Issue #4: Missing authorizedParties (CSRF)
**Error**: No error, but CSRF vulnerability
**Source**: https://clerk.com/docs/reference/backend/verify-token
**Prevention**: Always set `authorizedParties: ['https://yourdomain.com']`
### Issue #5: Import Path Changes (Core 2)
**Error**: "Cannot find module"
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
**Prevention**: Update import paths for Core 2
### Issue #6: JWT Size Limit Exceeded
**Error**: Token exceeds size limit
**Source**: https://clerk.com/docs/backend-requests/making/custom-session-token
**Prevention**: Keep custom claims under 1.2KB
### Issue #7: Deprecated API Version v1
**Error**: "API version v1 is deprecated"
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
**Prevention**: Use latest SDK versions (API v2025-11-10)
### Issue #8: ClerkProvider JSX Component Error
**Error**: "cannot be used as a JSX component"
**Source**: https://stackoverflow.com/questions/79265537
**Prevention**: Ensure React 19 compatibility with @clerk/clerk-react@5.56.2+
### Issue #9: Async auth() Helper Confusion
**Error**: "auth() is not a function"
**Source**: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
**Prevention**: Always await: `const { userId } = await auth()`
### Issue #10: Environment Variable Misconfiguration
**Error**: "Missing Publishable Key" or secret leaked
**Prevention**: Use correct prefixes (`NEXT_PUBLIC_`, `VITE_`), never commit secrets
### Issue #11: 431 Request Header Fields Too Large (Vite Dev Mode)
**Error**: "431 Request Header Fields Too Large" when signing in
**Source**: Common in Vite dev mode when testing custom JWT claims
**Cause**: Clerk's `__clerk_handshake` token in URL exceeds Vite's 8KB header limit
**Prevention**:
Add to `package.json`:
```json
{
"scripts": {
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite"
}
}
```
**Temporary Workaround**: Clear browser cache, sign out, sign back in
**Why**: Clerk dev tokens are larger than production; custom JWT claims increase handshake token size
**Note**: This is different from Issue #6 (session token size). Issue #6 is about cookies (1.2KB), this is about URL parameters in dev mode (8KB → 32KB).
---
## Official Documentation
- **Clerk Docs**: https://clerk.com/docs
- **Next.js Guide**: https://clerk.com/docs/references/nextjs/overview
- **React Guide**: https://clerk.com/docs/references/react/overview
- **Backend SDK**: https://clerk.com/docs/reference/backend/overview
- **JWT Templates**: https://clerk.com/docs/guides/sessions/jwt-templates
- **API Version 2025-11-10 Upgrade**: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/2025-11-10
- **Testing Guide**: https://clerk.com/docs/guides/development/testing/overview
- **Context7 Library ID**: `/clerk/clerk-docs`
---
## Package Versions
**Latest (Nov 22, 2025):**
```json
{
"dependencies": {
"@clerk/nextjs": "^6.35.4",
"@clerk/clerk-react": "^5.56.2",
"@clerk/backend": "^2.23.2",
"@clerk/testing": "^1.13.18"
}
}
```
---
**Token Efficiency**:
- **Without skill**: ~5,200 tokens (setup tutorials, JWT templates, testing setup)
- **With skill**: ~2,500 tokens (breaking changes + critical patterns + error prevention)
- **Savings**: ~52% (~2,700 tokens)
**Errors prevented**: 11 documented issues with exact solutions
**Key value**: API 2025-11-10 breaking changes, Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense, JWT size limits, 431 header error workaround
---
**Last verified**: 2025-11-22 | **Skill version**: 2.0.0 | **Changes**: Added API version 2025-11-10 breaking changes (billing endpoints), PKCE support, Client Trust defense, Next.js 16 support. Removed tutorials (~480 lines). Updated SDK versions. Focused on breaking changes + error prevention + critical patterns.

View File

@@ -0,0 +1,14 @@
[TODO: Example Template File]
[TODO: This directory contains files that will be used in the OUTPUT that Claude produces.]
[TODO: Examples:]
- Templates (.html, .tsx, .md)
- Images (.png, .svg)
- Fonts (.ttf, .woff)
- Boilerplate code
- Configuration file templates
[TODO: Delete this file and add your actual assets]
These files are NOT loaded into context. They are copied or used directly in the final output.

137
plugin.lock.json Normal file
View File

@@ -0,0 +1,137 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jezweb/claude-skills:skills/clerk-auth",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "9fce0ffec647b8d8c61c76bd4fca8d99d2baff35",
"treeHash": "f72a84de09ac89d9f601331c56be2456523750f8902e426b3e15417d871693f4",
"generatedAt": "2025-11-28T10:19:03.105129Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "clerk-auth",
"description": "Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "6b38d36c1223919554d05e8990a2a555011915ea6e07f880993a1b652336dec4"
},
{
"path": "SKILL.md",
"sha256": "777b54890367198bc182e4c08e1b7dec0ee9b19e8e6c788ae90d5febbe46a354"
},
{
"path": "references/example-reference.md",
"sha256": "77c788d727d05d6479a61d6652b132e43882ffc67c145bb46ba880567d83f7f8"
},
{
"path": "references/jwt-claims-guide.md",
"sha256": "e62be80eb192068532466ee545b4230090983eaaea42bb2def0ca94b7caab724"
},
{
"path": "references/common-errors.md",
"sha256": "026bbaefa69b87ad3cd9099d33e5d8403d8e39aa6164f33d9ef018b4483614c0"
},
{
"path": "references/testing-guide.md",
"sha256": "01d0a6216c98fc371218eb25399cc6c1402b36f8c187dcb10229062dcc5b5897"
},
{
"path": "scripts/example-script.sh",
"sha256": "83d2b09d044811608e17cbd8e66d993b1e9998c7bd3379a42ab81fbdba973e0e"
},
{
"path": "scripts/generate-session-token.js",
"sha256": "535155e99263a2e8d4417409a1092ebf2af12fbb1ff8cb082e4f0e42db420ecc"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "7374c496926cea0a8057b7723a2f5ad8c88e03e71616c8312d34f83b43c265e0"
},
{
"path": "templates/jwt/supabase-template.json",
"sha256": "a3b554eb7dcb51759bdf9beca6db74bf8d4511d5750fb598ca8eda3a537b5d16"
},
{
"path": "templates/jwt/grafbase-template.json",
"sha256": "4b3a9d3f55e01c4400313caaf05360873221e32c2e077a96c31c1c79139526ac"
},
{
"path": "templates/jwt/basic-template.json",
"sha256": "333acef2a560adede6151e6a9afe0f4f84fefa801211a09d23f9ddd9c6ce2937"
},
{
"path": "templates/jwt/advanced-template.json",
"sha256": "f81ed4716ca32f2a105563e4bfff35967d35706efd40aaf0b641e446e6f8e49a"
},
{
"path": "templates/typescript/custom-jwt-types.d.ts",
"sha256": "c1bd761f82dc93ccfef8dac718df7113f0027839e0bcc53df575db9c49acdb3c"
},
{
"path": "templates/cloudflare/wrangler.jsonc",
"sha256": "0361caad0f82846078b605ddb6e3b88eb56ab854109b0e4dbb1fbbd84341d7b3"
},
{
"path": "templates/cloudflare/worker-auth.ts",
"sha256": "6abc3321967c0bed597204eef3fc36ef7caefdec8e8a26b237fd7717e9e2037b"
},
{
"path": "templates/vite/package.json",
"sha256": "b4b496d3c457af1247768c4a6c725b01e2c2f82ac7648bbf24d69911e60e4d00"
},
{
"path": "templates/nextjs/middleware.ts",
"sha256": "866f24cef11c0dcd59fcdd12f429fe54b3ec77626e7b7705726776362e15daea"
},
{
"path": "templates/nextjs/server-component-example.tsx",
"sha256": "235dc29fb7fceb29621dced8671467ad5b7a0dc2012f704c2ef73a18b574ffb5"
},
{
"path": "templates/nextjs/app-layout.tsx",
"sha256": "efdeba1d20c15771a7e15974bfbb418708dc80d163f41f4946b49bb41423a9ec"
},
{
"path": "templates/env-examples/.dev.vars.example",
"sha256": "1e3d178480a4c771fbbde5e87e33bfc6b4779a3a319fad4c45c7552a3140b8b0"
},
{
"path": "templates/env-examples/.env.local.example",
"sha256": "fc0f7ec352213a16c72973d5462aa35a41d1c78f5c9108974fba5bec82947475"
},
{
"path": "templates/env-examples/.env.local.vite.example",
"sha256": "077bf978ef4d088934e1bbf7811dad25e1bd80d94c319d452ff5c522e790bb9c"
},
{
"path": "templates/react/App.tsx",
"sha256": "b7cf39eb6d8381348442f360f32ed01cf75b992ac9816413b36f7d88bae3618f"
},
{
"path": "templates/react/main.tsx",
"sha256": "3184f8908c096402d5519497ceb31c6d0e157741429c2bc830a9c7aaf518d02f"
},
{
"path": "assets/example-template.txt",
"sha256": "3f725c80d70847fd8272bf1400515ba753f12f98f3b294d09e50b54b4c1b024a"
}
],
"dirSha256": "f72a84de09ac89d9f601331c56be2456523750f8902e426b3e15417d871693f4"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

642
references/common-errors.md Normal file
View File

@@ -0,0 +1,642 @@
# Clerk Authentication - Common Errors & Solutions
**Last Updated**: 2025-10-22
This document provides detailed troubleshooting for all 10 documented Clerk authentication issues.
---
## Error #1: Missing Clerk Secret Key
### Symptoms
\`\`\`
Error: Missing Clerk Secret Key or API Key
\`\`\`
### Why It Happens
- \`CLERK_SECRET_KEY\` environment variable not set
- Environment file not loaded
- Wrong environment file name
### Solutions
**Next.js**:
1. Create \`.env.local\` in project root
2. Add: \`CLERK_SECRET_KEY=sk_test_...\`
3. Restart dev server
**Cloudflare Workers**:
1. Local: Create \`.dev.vars\` with \`CLERK_SECRET_KEY=sk_test_...\`
2. Production: Run \`wrangler secret put CLERK_SECRET_KEY\`
### Prevention
- Always set secret key before running app
- Verify with: \`echo $CLERK_SECRET_KEY\` (should be empty - it's private!)
- Check environment is loading: add \`console.log(!!process.env.CLERK_SECRET_KEY)\`
**Source**: https://stackoverflow.com/questions/77620604
---
## Error #2: API Key → Secret Key Migration (Core 2)
### Symptoms
\`\`\`
Warning: apiKey is deprecated, use secretKey instead
TypeError: Cannot read properties of undefined
\`\`\`
### Why It Happens
- Upgrading from @clerk/backend v1 to v2 (Core 2)
- Old code uses \`apiKey\` parameter
- Breaking change in Core 2
### Solutions
**Before (v1)**:
\`\`\`typescript
const clerk = createClerkClient({
apiKey: process.env.CLERK_API_KEY // ❌ Deprecated
})
\`\`\`
**After (v2)**:
\`\`\`typescript
const clerk = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY // ✅ Correct
})
\`\`\`
### Prevention
- Use \`secretKey\` in all new code
- Search codebase for \`apiKey:\` and replace with \`secretKey:\`
- Update environment variable name from \`CLERK_API_KEY\` to \`CLERK_SECRET_KEY\`
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
---
## Error #3: JWKS Cache Race Condition
### Symptoms
\`\`\`
Error: No JWK available
Token verification fails intermittently
\`\`\`
### Why It Happens
- Race condition in older @clerk/backend versions
- JWKS cache not populated before verification
- Fixed in recent versions
### Solutions
**Update Package**:
\`\`\`bash
npm install @clerk/backend@latest
# Ensure version is 2.17.2 or later
\`\`\`
**Verify Version**:
\`\`\`bash
npm list @clerk/backend
\`\`\`
### Prevention
- Always use latest stable @clerk/backend version
- This issue is fixed in modern versions
**Source**: https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md
---
## Error #4: Missing authorizedParties (CSRF Vulnerability)
### Symptoms
- No error, but security vulnerability
- Tokens from other domains accepted
- CSRF attacks possible
### Why It Happens
- \`authorizedParties\` not set in \`verifyToken()\`
- Clerk accepts tokens from any domain by default
### Solutions
**Always Set authorizedParties**:
\`\`\`typescript
const { data, error } = await verifyToken(token, {
secretKey: env.CLERK_SECRET_KEY,
authorizedParties: [
'http://localhost:5173', // Development
'https://yourdomain.com', // Production
], // ✅ Required for security
})
\`\`\`
### Prevention
- Always set \`authorizedParties\` in production
- List all domains that should be able to make requests
- Never use wildcard or leave empty
**Source**: https://clerk.com/docs/reference/backend/verify-token
---
## Error #5: Import Path Changes (Core 2 Upgrade)
### Symptoms
\`\`\`
Error: Cannot find module '@clerk/backend'
Module not found: Can't resolve '@clerk/backend/errors'
\`\`\`
### Why It Happens
- Import paths changed in Core 2
- Old imports don't work in new version
### Solutions
**Update Imports**:
\`\`\`typescript
// Before (v1)
import { TokenVerificationError } from '@clerk/backend'
// After (v2)
import { TokenVerificationError } from '@clerk/backend/errors'
\`\`\`
### Common Path Changes
| Old Path | New Path |
|----------|----------|
| \`@clerk/backend\` (errors) | \`@clerk/backend/errors\` |
| \`@clerk/backend\` (types) | \`@clerk/backend/types\` |
### Prevention
- Follow official migration guide
- Update all imports when upgrading
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
---
## Error #6: JWT Size Limit Exceeded
### Symptoms
\`\`\`
Error: Token exceeds maximum size
Session token too large
\`\`\`
### Why It Happens
- Custom JWT claims exceed 1.2KB limit
- Too much data in \`publicMetadata\`
- Clerk's default claims + your claims > 1.2KB
### Solutions
**Reduce Custom Claims**:
\`\`\`json
{
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role}}"
}
\`\`\`
**Store Large Data in Database**:
- Use JWT for minimal claims (user ID, role)
- Store profile data, preferences in your database
- Fetch after authentication
### Prevention
- Keep custom claims minimal
- Use database for large/complex data
- Monitor token size during development
**Source**: https://clerk.com/docs/backend-requests/making/custom-session-token
---
## Error #7: Deprecated API Version v1
### Symptoms
\`\`\`
Warning: API version v1 is deprecated
Please upgrade to API version 2025-04-10
\`\`\`
### Why It Happens
- Using old SDK version
- API version 1 deprecated April 2025
### Solutions
**Update SDKs**:
\`\`\`bash
npm install @clerk/nextjs@latest
npm install @clerk/backend@latest
npm install @clerk/clerk-react@latest
\`\`\`
**Verify Versions**:
- @clerk/nextjs: 6.0.0+
- @clerk/backend: 2.0.0+
- @clerk/clerk-react: 5.0.0+
### Prevention
- Keep SDKs updated
- Use latest stable versions
- Follow deprecation warnings
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
---
## Error #8: ClerkProvider JSX Component Error
### Symptoms
\`\`\`
Error: 'ClerkProvider' cannot be used as a JSX component
Its element type 'ReactElement<any, any> | Component<...>' is not a valid JSX element
\`\`\`
### Why It Happens
- React version mismatch
- @clerk/clerk-react not compatible with React version
- TypeScript type conflicts
### Solutions
**Update Packages**:
\`\`\`bash
npm install @clerk/clerk-react@latest react@latest react-dom@latest
\`\`\`
**Ensure Compatibility**:
- React 19: Use @clerk/clerk-react@5.51.0+
- React 18: Use @clerk/clerk-react@5.0.0+
### Prevention
- Keep React and Clerk packages in sync
- Use compatible versions
**Source**: https://stackoverflow.com/questions/79265537
---
## Error #9: Async auth() Helper Confusion
### Symptoms
\`\`\`
Error: auth() is not a function
TypeError: Cannot read properties of undefined
\`\`\`
### Why It Happens
- @clerk/nextjs v6 made \`auth()\` async (breaking change)
- Old code doesn't await \`auth()\`
### Solutions
**Before (v5)**:
\`\`\`typescript
const { userId } = auth() // ❌ Sync in v5
\`\`\`
**After (v6)**:
\`\`\`typescript
const { userId } = await auth() // ✅ Async in v6
\`\`\`
### All Affected Code
- \`const { userId } = await auth()\`
- \`const user = await currentUser()\`
- \`await auth.protect()\` (in middleware)
### Prevention
- Always await auth-related functions in v6+
- Update all instances when upgrading
**Source**: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
---
## Error #10: Environment Variable Misconfiguration
### Symptoms
\`\`\`
Error: Missing Publishable Key
Warning: Secret key exposed to client
\`\`\`
### Why It Happens
- Wrong environment variable prefix
- Secrets committed to version control
- Environment file not loaded
### Solutions
**Next.js Prefix Rules**:
- Client variables: \`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY\`
- Server variables: \`CLERK_SECRET_KEY\` (no prefix!)
**Vite Prefix Rules**:
- Client variables: \`VITE_CLERK_PUBLISHABLE_KEY\`
**Cloudflare Workers**:
- Local: \`.dev.vars\` file
- Production: \`wrangler secret put CLERK_SECRET_KEY\`
### Prevention Checklist
- [ ] Use correct prefix for framework
- [ ] Never use \`NEXT_PUBLIC_\` or \`VITE_\` for secrets
- [ ] Add \`.env.local\` and \`.dev.vars\` to \`.gitignore\`
- [ ] Use different keys for dev and production
- [ ] Restart dev server after changing env vars
**Source**: General best practices
---
## Error #11: 431 Request Header Fields Too Large (Vite Dev Mode)
### Symptoms
```
431 Request Header Fields Too Large
Failed to load resource: the server responded with a status of 431 ()
```
Happens when:
- Signing in to your Vite app in development mode
- After adding custom JWT claims
- URL contains very long `__clerk_handshake=...` parameter
### Why It Happens
- Clerk's authentication handshake passes a JWT token in the URL as `__clerk_handshake=...`
- Vite dev server (Node.js http) has default 8KB request header limit
- Clerk development tokens are larger than production tokens
- Custom JWT claims increase handshake token size
- Long URLs with JWT tokens exceed Vite's header limit
### Solutions
**Solution 1: Increase Node.js Header Limit (Recommended)**
Update `package.json`:
```json
{
"scripts": {
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
"build": "vite build"
}
}
```
This increases the limit from 8KB to 32KB.
**For Windows PowerShell**:
```json
{
"scripts": {
"dev": "cross-env NODE_OPTIONS=--max-http-header-size=32768 vite"
}
}
```
Install `cross-env`: `npm install -D cross-env`
**Solution 2: Temporary Workaround**
Clear browser state:
1. Open DevTools (F12)
2. Right-click refresh button → "Empty Cache and Hard Reload"
3. Or: Application tab → Clear all storage
4. Sign out and sign back in
This removes the problematic handshake token and forces Clerk to create a fresh one.
### What DOESN'T Work
**This won't fix it**:
```typescript
// vite.config.ts
export default defineConfig({
server: {
headers: {
'Cache-Control': 'no-cache', // Wrong - this sets RESPONSE headers
},
},
})
```
The error is about REQUEST headers (incoming), not RESPONSE headers (outgoing).
### Prevention
- Expect this error when testing custom JWT claims in dev mode
- Set `NODE_OPTIONS` in `package.json` from the start
- Keep custom claims minimal during development
- Production deployments (Cloudflare Workers, Vercel) don't have this issue
### Difference from Error #6
**Error #6** (JWT Size Limit Exceeded):
- About session token size in cookies
- 1.2KB limit for custom claims
- Affects production
- Browser cookie size limit
**Error #11** (431 Header Error):
- About handshake token size in URL parameters
- 8KB default limit (increase to 32KB)
- Only affects Vite dev mode
- Node.js HTTP server limit
### Why Production Isn't Affected
- Production builds don't use Vite dev server
- Cloudflare Workers, Vercel, Netlify have higher header limits
- Production tokens are smaller (no dev overhead)
- Handshake flow is optimized in production
**Source**: Real-world developer experience with Vite + Clerk + Custom JWT claims
---
## Quick Debugging Checklist
When auth isn't working:
1. **Check Environment Variables**
- [ ] \`CLERK_SECRET_KEY\` set?
- [ ] \`CLERK_PUBLISHABLE_KEY\` or \`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY\` set?
- [ ] Correct prefix for framework?
- [ ] Dev server restarted after changes?
2. **Check Package Versions**
- [ ] Using latest stable versions?
- [ ] React version compatible with Clerk?
- [ ] All Clerk packages same major version?
3. **Check Code Patterns**
- [ ] \`auth()\` being awaited? (v6+)
- [ ] \`authorizedParties\` set in \`verifyToken()\`?
- [ ] \`isLoaded\` checked before rendering?
- [ ] Using \`secretKey\` not \`apiKey\`?
4. **Check Network**
- [ ] Token in \`Authorization: Bearer <token>\` header?
- [ ] CORS configured if API is different domain?
- [ ] Clerk Dashboard shows API requests?
5. **Check Clerk Dashboard**
- [ ] Application configured correctly?
- [ ] Development/production keys match environment?
- [ ] Custom JWT template valid?
- [ ] Webhooks configured if using?
---
**Still Having Issues?**
1. Check official Clerk docs: https://clerk.com/docs
2. Search Clerk Discord: https://clerk.com/discord
3. File GitHub issue: https://github.com/clerk/javascript/issues
4. Check Clerk status: https://status.clerk.com
---
## Error #12: Deprecated Redirect URL Props
### Symptoms
```
Clerk: The prop "afterSignInUrl" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.
Clerk: The prop "afterSignUpUrl" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.
```
Appears in:
- Browser console
- Development mode
- When using `<SignIn>` or `<SignUp>` components from `@clerk/clerk-react`
### Why It Happens
- Clerk updated redirect prop naming in v5.x for clarity
- Old props: `afterSignInUrl`, `afterSignUpUrl`
- New props: `fallbackRedirectUrl`, `forceRedirectUrl`
- Old props still work but trigger deprecation warnings
### Solution
**Replace deprecated props:**
**Old (Deprecated)**:
```tsx
<SignIn
afterSignInUrl="/"
signUpUrl="/signup"
/>
<SignUp
afterSignUpUrl="/"
signInUrl="/login"
/>
```
**New (Recommended)**:
```tsx
<SignIn
fallbackRedirectUrl="/"
signUpUrl="/signup"
/>
<SignUp
fallbackRedirectUrl="/"
signInUrl="/login"
/>
```
### Choosing Between fallbackRedirectUrl and forceRedirectUrl
**Use `fallbackRedirectUrl`** (Most Common):
- Used as a fallback if no redirect URL is in query params
- Allows Clerk to use `redirect_url` from query string first
- More flexible for return-to-previous-page flows
- **Recommended for most use cases**
**Use `forceRedirectUrl`** (Rare):
- Always redirects here, ignoring query params
- Used when you want strict control
- Overrides any `redirect_url` in query string
- Use only when you need to force a specific destination
### Example: Complete Migration
```tsx
// LoginPage.tsx
import { SignIn } from '@clerk/clerk-react';
export function LoginPage() {
return (
<SignIn
routing="path"
path="/login"
signUpUrl="/signup"
fallbackRedirectUrl="/" // ✅ Use this instead of afterSignInUrl
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-lg',
},
}}
/>
);
}
```
```tsx
// SignupPage.tsx
import { SignUp } from '@clerk/clerk-react';
export function SignupPage() {
return (
<SignUp
routing="path"
path="/signup"
signInUrl="/login"
fallbackRedirectUrl="/" // ✅ Use this instead of afterSignUpUrl
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-lg',
},
}}
/>
);
}
```
### Migration Checklist
- [ ] Search codebase for `afterSignInUrl`
- [ ] Replace with `fallbackRedirectUrl`
- [ ] Search codebase for `afterSignUpUrl`
- [ ] Replace with `fallbackRedirectUrl`
- [ ] Test redirect flow after sign in
- [ ] Test redirect flow after sign up
- [ ] Verify no console warnings
### Why This Matters
**Clarity**: The new naming makes it clear these are fallback/default redirects
**Future-Proofing**: Old props may be removed in future major versions
**Best Practices**: Using current APIs ensures compatibility with new features
**Source**: Clerk v5.x Changelog & Official Migration Guide
**Reference**: https://clerk.com/docs/guides/custom-redirects#redirect-url-props
---
**Error #12** (Deprecated Redirect Props):
- About redirect URL prop naming
- Simple find-replace fix
- Use `fallbackRedirectUrl` for most cases
- Only affects React-based projects

View File

@@ -0,0 +1,26 @@
# [TODO: Reference Document Name]
[TODO: This file contains reference documentation that Claude can load when needed.]
[TODO: Delete this file if you don't have reference documentation to provide.]
## Purpose
[TODO: Explain what information this document contains]
## When Claude Should Use This
[TODO: Describe specific scenarios where Claude should load this reference]
## Content
[TODO: Add your reference content here - schemas, guides, specifications, etc.]
---
**Note**: This file is NOT loaded into context by default. Claude will only load it when:
- It determines the information is needed
- You explicitly ask Claude to reference it
- The SKILL.md instructions direct Claude to read it
Keep this file under 10k words for best performance.

View File

@@ -0,0 +1,660 @@
# Clerk JWT Claims: Complete Reference
**Last Updated**: 2025-10-22
**Clerk API Version**: 2025-04-10
**Status**: Production Ready ✅
---
## Overview
This guide provides comprehensive documentation for all JWT claims available in Clerk, including default claims, user property shortcodes, organization claims, metadata access patterns, and advanced template features.
**Use this guide when**:
- Creating custom JWT templates in Clerk Dashboard
- Integrating with third-party services requiring specific token formats
- Implementing role-based access control (RBAC)
- Building multi-tenant applications
- Accessing user data in backend services
---
## Table of Contents
1. [Default Claims (Auto-Included)](#default-claims-auto-included)
2. [User Property Shortcodes](#user-property-shortcodes)
3. [Organization Claims](#organization-claims)
4. [Metadata Access](#metadata-access)
5. [Advanced Template Features](#advanced-template-features)
6. [Creating JWT Templates](#creating-jwt-templates)
7. [TypeScript Type Safety](#typescript-type-safety)
8. [Common Use Cases](#common-use-cases)
9. [Limitations & Gotchas](#limitations--gotchas)
10. [Official Documentation](#official-documentation)
---
## Default Claims (Auto-Included)
Every JWT generated by Clerk automatically includes these claims. **These cannot be overridden in custom templates**.
```json
{
"azp": "http://localhost:3000", // Authorized party (your app URL)
"exp": 1639398300, // Expiration time (Unix timestamp)
"iat": 1639398272, // Issued at (Unix timestamp)
"iss": "https://your-app.clerk.accounts.dev", // Issuer (Clerk instance URL)
"jti": "10db7f531a90cb2faea4", // JWT ID (unique identifier)
"nbf": 1639398220, // Not before (Unix timestamp)
"sub": "user_1deJLArSTiWiF1YdsEWysnhJLLY" // Subject (user ID)
}
```
### Claim Descriptions
| Claim | Name | Description | Can Override? |
|-------|------|-------------|---------------|
| `azp` | Authorized Party | The URL of your application | ❌ No |
| `exp` | Expiration Time | When the token expires (Unix timestamp) | ❌ No |
| `iat` | Issued At | When the token was created (Unix timestamp) | ❌ No |
| `iss` | Issuer | Your Clerk instance URL | ❌ No |
| `jti` | JWT ID | Unique identifier for this token | ❌ No |
| `nbf` | Not Before | Token not valid before this time (Unix timestamp) | ❌ No |
| `sub` | Subject | User ID (same as `userId` in auth objects) | ❌ No |
**IMPORTANT**: These claims consume approximately **200-300 bytes** of the 4KB cookie limit, leaving ~1.2KB for custom claims.
---
## User Property Shortcodes
Shortcodes are placeholder strings that Clerk replaces with actual user data during token generation. Use double curly braces: `{{shortcode}}`.
### Basic User Properties
```json
{
"user_id": "{{user.id}}", // User's unique ID
"first_name": "{{user.first_name}}", // User's first name (or null)
"last_name": "{{user.last_name}}", // User's last name (or null)
"full_name": "{{user.full_name}}", // User's full name (or null)
"email": "{{user.primary_email_address}}", // Primary email address
"phone": "{{user.primary_phone_address}}", // Primary phone number (or null)
"avatar": "{{user.image_url}}", // User's profile image URL
"created_at": "{{user.created_at}}", // Account creation timestamp
"username": "{{user.username}}", // Username (if enabled)
"email_verified": "{{user.email_verified}}", // Boolean: email verified?
"phone_verified": "{{user.phone_number_verified}}" // Boolean: phone verified?
}
```
### Verification Status
```json
{
"has_verified_email": "{{user.email_verified}}",
"has_verified_phone": "{{user.phone_number_verified}}",
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}"
}
```
### Complete User Shortcode Reference
| Shortcode | Type | Description | Example Value |
|-----------|------|-------------|---------------|
| `{{user.id}}` | string | User's unique identifier | `"user_2abc..."` |
| `{{user.first_name}}` | string \| null | First name | `"John"` |
| `{{user.last_name}}` | string \| null | Last name | `"Doe"` |
| `{{user.full_name}}` | string \| null | Full name (computed) | `"John Doe"` |
| `{{user.primary_email_address}}` | string | Primary email | `"john@example.com"` |
| `{{user.primary_phone_address}}` | string \| null | Primary phone | `"+1234567890"` |
| `{{user.image_url}}` | string | Profile image URL | `"https://..."` |
| `{{user.created_at}}` | number | Unix timestamp | `1639398272` |
| `{{user.username}}` | string \| null | Username (if enabled) | `"johndoe"` |
| `{{user.email_verified}}` | boolean | Email verified? | `true` |
| `{{user.phone_number_verified}}` | boolean | Phone verified? | `false` |
| `{{user.public_metadata}}` | object | All public metadata | `{...}` |
| `{{user.unsafe_metadata}}` | object | All unsafe metadata | `{...}` |
---
## Organization Claims
Clerk includes claims for the **active organization** (if user is in one and has selected it).
### Active Organization Claims
```json
{
"org_id": "org_2abc...", // Active organization ID
"org_slug": "acme-corp", // Active organization slug
"org_role": "org:admin" // User's role in active organization
}
```
**CRITICAL CHANGE (Core 2)**:
-**New**: `org_id`, `org_slug`, `org_role` (active org only)
-**Removed**: `orgs` claim (previously contained all user organizations)
### Accessing Organization Data in Templates
```json
{
"organization": {
"id": "{{user.public_metadata.org_id}}",
"name": "{{user.public_metadata.org_name}}",
"role": "{{user.public_metadata.org_role}}"
}
}
```
**Note**: For all organizations (not just active), you must:
1. Store org data in `user.public_metadata` via Clerk API
2. Use custom JWT template to include it
---
## Metadata Access
Clerk provides two metadata fields for storing custom user data:
### Public Metadata
- ✅ Accessible on client side
- ✅ Included in user objects
- ✅ Can be included in JWT templates
- ❌ Should NOT contain sensitive data
### Unsafe Metadata
- ⚠️ Name is misleading - it's for "unvalidated" data
- ✅ Accessible on client side
- ✅ Can be included in JWT templates
- ❌ Should NOT contain sensitive data
### Basic Metadata Access
```json
{
"all_public": "{{user.public_metadata}}",
"all_unsafe": "{{user.unsafe_metadata}}"
}
```
**Result**:
```json
{
"all_public": {
"role": "admin",
"department": "engineering"
},
"all_unsafe": {
"onboardingComplete": true
}
}
```
### Nested Metadata with Dot Notation
For nested objects, use dot notation to access specific fields:
**User's public_metadata**:
```json
{
"profile": {
"interests": ["hiking", "knitting"],
"bio": "Software engineer passionate about..."
},
"addresses": {
"Home": "2355 Pointe Lane, 56301 Minnesota",
"Work": "3759 Newton Street, 33487 Florida"
},
"role": "admin",
"department": "engineering"
}
```
**JWT Template**:
```json
{
"role": "{{user.public_metadata.role}}",
"department": "{{user.public_metadata.department}}",
"interests": "{{user.public_metadata.profile.interests}}",
"home_address": "{{user.public_metadata.addresses.Home}}"
}
```
**Generated Token**:
```json
{
"role": "admin",
"department": "engineering",
"interests": ["hiking", "knitting"],
"home_address": "2355 Pointe Lane, 56301 Minnesota"
}
```
**Why This Matters**: Dot notation prevents including the entire metadata object, reducing token size.
---
## Advanced Template Features
### String Interpolation
Combine multiple shortcodes into a single string:
```json
{
"full_name": "{{user.last_name}} {{user.first_name}}",
"greeting": "Hello, {{user.first_name}}!",
"email_with_name": "{{user.full_name}} <{{user.primary_email_address}}>"
}
```
**Result**:
```json
{
"full_name": "Doe John",
"greeting": "Hello, John!",
"email_with_name": "John Doe <john@example.com>"
}
```
**IMPORTANT**: Interpolated values are **always strings**, even if null values are present.
### Conditional Expressions (Fallbacks)
Use `||` operator to provide fallback values:
```json
{
"full_name": "{{user.full_name || 'Guest User'}}",
"age": "{{user.public_metadata.age || 18}}",
"role": "{{user.public_metadata.role || 'user'}}",
"verified": "{{user.email_verified || user.phone_number_verified}}"
}
```
**How It Works**:
- Returns first **non-falsy** operand
- Final operand serves as default
- String literals require **single quotes**: `'default'`
- Can chain multiple fallbacks
**Example with Multiple Fallbacks**:
```json
{
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}"
}
```
This checks:
1. `user.public_metadata.age` (if null/false, continue)
2. `user.unsafe_metadata.age` (if null/false, continue)
3. `18` (default value)
### Boolean Checks
```json
{
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
"is_complete": "{{user.public_metadata.profileComplete || false}}"
}
```
**Result**:
```json
{
"has_verified_contact": true, // If either email or phone is verified
"is_complete": false // If profileComplete is not set
}
```
---
## Creating JWT Templates
### Step 1: Navigate to Clerk Dashboard
1. Go to **Sessions** page in Clerk Dashboard
2. Click **Customize session token**
3. Choose **Create template** or use pre-built templates
### Step 2: Configure Template Properties
**Template Properties**:
```json
{
"name": "supabase", // Unique identifier (lowercase, no spaces)
"lifetime": 3600, // Token expiration in seconds (default: 60)
"allowed_clock_skew": 5, // Leeway for clock differences (default: 5)
"claims": { // Your custom claims
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role}}"
}
}
```
### Step 3: Use Template in Code
**React / Next.js**:
```typescript
import { useAuth } from '@clerk/nextjs'
function MyComponent() {
const { getToken } = useAuth()
// Get token with custom template
const token = await getToken({ template: 'supabase' })
// Use token to authenticate with external service
const response = await fetch('https://api.supabase.com/endpoint', {
headers: {
'Authorization': `Bearer ${token}`
}
})
}
```
**Cloudflare Workers**:
```typescript
import { clerkClient } from '@clerk/backend'
// Generate token for user
const token = await clerkClient.users.getUserOauthAccessToken(
userId,
'supabase' // Template name
)
```
---
## TypeScript Type Safety
Add type safety for custom claims with global declarations:
**Create `types/globals.d.ts`**:
```typescript
export {}
declare global {
interface CustomJwtSessionClaims {
metadata: {
role?: 'admin' | 'moderator' | 'user'
onboardingComplete?: boolean
department?: string
organizationId?: string
}
}
}
```
**Usage**:
```typescript
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { sessionClaims } = await auth()
// TypeScript now knows about these properties
const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
const isComplete = sessionClaims?.metadata?.onboardingComplete // Type: boolean | undefined
}
```
---
## Common Use Cases
### 1. Role-Based Access Control (RBAC)
**JWT Template**:
```json
{
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}",
"permissions": "{{user.public_metadata.permissions}}"
}
```
**Backend Verification**:
```typescript
app.get('/api/admin', async (c) => {
const sessionClaims = c.get('sessionClaims')
if (sessionClaims?.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403)
}
return c.json({ message: 'Admin data' })
})
```
### 2. Multi-Tenant Applications
**JWT Template**:
```json
{
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"org_id": "{{user.public_metadata.org_id}}",
"org_slug": "{{user.public_metadata.org_slug}}",
"org_role": "{{user.public_metadata.org_role}}"
}
```
**Usage**:
```typescript
// Filter data by organization
const data = await db.query('SELECT * FROM items WHERE org_id = ?', [
sessionClaims.org_id
])
```
### 3. Supabase Integration
**JWT Template** (Name: `supabase`):
```json
{
"email": "{{user.primary_email_address}}",
"app_metadata": {
"provider": "clerk"
},
"user_metadata": {
"full_name": "{{user.full_name}}",
"avatar_url": "{{user.image_url}}"
}
}
```
**Usage**:
```typescript
import { createClient } from '@supabase/supabase-js'
const token = await getToken({ template: 'supabase' })
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${token}`
}
}
}
)
```
### 4. Grafbase GraphQL Integration
**JWT Template** (Name: `grafbase`):
```json
{
"sub": "{{user.id}}",
"groups": ["org:admin", "org:member"]
}
```
**Grafbase Configuration**:
```toml
[auth.providers.clerk]
issuer = "https://your-app.clerk.accounts.dev"
jwks = "https://your-app.clerk.accounts.dev/.well-known/jwks.json"
```
---
## Limitations & Gotchas
### 1. Token Size Limit: 1.2KB for Custom Claims
**Problem**: Browsers limit cookies to 4KB. Clerk's default claims consume ~2.8KB, leaving **1.2KB for custom claims**.
**Solution**:
- ✅ Use JWT for minimal claims (user ID, role, email)
- ✅ Store large data (bio, preferences) in your database
- ✅ Fetch additional data after authentication
- ❌ Don't include large objects or arrays
**Bad Example** (Exceeds limit):
```json
{
"email": "{{user.primary_email_address}}",
"bio": "{{user.public_metadata.bio}}", // 6KB bio field
"all_metadata": "{{user.public_metadata}}" // Entire object
}
```
**Good Example** (Under limit):
```json
{
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role}}"
}
```
### 2. Reserved Claims
These claims are **reserved** and cannot be used in custom templates:
| Claim | Reason |
|-------|--------|
| `azp` | Auto-included default claim |
| `exp` | Auto-included default claim |
| `iat` | Auto-included default claim |
| `iss` | Auto-included default claim |
| `jti` | Auto-included default claim |
| `nbf` | Auto-included default claim |
| `sub` | Auto-included default claim |
**Error**: Attempting to override reserved claims returns HTTP 400 with error code `jwt_template_reserved_claim`.
### 3. Session-Bound Claims
These claims are **session-specific** and cannot be included in **custom JWTs** (but ARE included in default session tokens):
| Claim | Description |
|-------|-------------|
| `sid` | Session ID |
| `v` | Version |
| `pla` | Plan |
| `fea` | Features |
**Why**: Custom JWTs are generated on-demand and not tied to a specific session.
**Solution**: If you need session data, use **custom session tokens** instead of custom JWTs.
### 4. Invalid Shortcodes Resolve to `null`
```json
{
"valid": "{{user.first_name}}",
"invalid": "{{user.i_dont_exist}}"
}
```
**Result**:
```json
{
"valid": "John",
"invalid": null
}
```
**Prevention**: Always test templates with real user data before deploying.
### 5. Custom JWTs May Incur Latency
- Default session tokens are cached
- Custom JWTs require fetching user data on-demand
- This can add **50-200ms** to token generation
**When to Use Custom JWTs**:
- ✅ Integrating with third-party services (Supabase, Hasura, Grafbase)
- ✅ API-to-API authentication
- ❌ Not recommended for every frontend request (use default session tokens)
### 6. Organization Data Limitations
- Only **active organization** data is included by default
- For all organizations, store in `user.public_metadata` and use custom template
- `orgs` claim was removed in Core 2 (breaking change)
### 7. Metadata Sync Timing
- Metadata changes may take a few seconds to propagate to tokens
- Use `user.reload()` in frontend to refresh user object
- Backend should always fetch fresh data for critical operations
---
## Official Documentation
- **JWT Templates Guide**: https://clerk.com/docs/guides/sessions/jwt-templates
- **Custom Session Tokens**: https://clerk.com/docs/backend-requests/making/custom-session-token
- **Session Claims Access**: https://clerk.com/docs/upgrade-guides/core-2/node
- **Backend SDK**: https://clerk.com/docs/reference/backend/overview
- **Supabase Integration**: https://clerk.com/docs/guides/development/integrations/databases/supabase
- **Grafbase Integration**: https://clerk.com/docs/guides/development/integrations/databases/grafbase
---
## Quick Reference: All Available Shortcodes
### User Properties
```
{{user.id}}
{{user.first_name}}
{{user.last_name}}
{{user.full_name}}
{{user.primary_email_address}}
{{user.primary_phone_address}}
{{user.image_url}}
{{user.created_at}}
{{user.username}}
{{user.email_verified}}
{{user.phone_number_verified}}
```
### Metadata
```
{{user.public_metadata}}
{{user.public_metadata.FIELD_NAME}}
{{user.public_metadata.nested.field}}
{{user.unsafe_metadata}}
{{user.unsafe_metadata.FIELD_NAME}}
```
### Operators
```
{{shortcode1 || shortcode2}} // Fallback
{{shortcode || 'default'}} // Default value
"{{shortcode1}} {{shortcode2}}" // String interpolation
```
---
**Last Updated**: 2025-10-22
**Verified Against**: Clerk API v2025-04-10
**Production Tested**: ✅ Multiple frameworks

575
references/testing-guide.md Normal file
View File

@@ -0,0 +1,575 @@
# Clerk Authentication - Testing Guide
**Last Updated**: 2025-10-28
**Source**: https://clerk.com/docs/guides/development/testing/overview
This guide covers testing Clerk authentication in your applications, including test credentials, session tokens, testing tokens, and E2E testing with Playwright.
---
## Overview
Testing authentication flows is essential for reliable applications. Clerk provides several tools to make testing easier:
1. **Test Emails & Phone Numbers** - Fake credentials with fixed OTP codes
2. **Session Tokens** - Generate valid tokens via Backend API
3. **Testing Tokens** - Bypass bot detection in test suites
4. **Framework Integrations** - Playwright and Cypress helpers
---
## Quick Start: Test Mode
### Enable Test Mode
Every **development instance** has test mode enabled by default.
For **production instances** (NOT recommended for real customer data):
1. Navigate to Clerk Dashboard → **Settings**
2. Enable the **Enable test mode** toggle
> **WARNING**: Never use test mode on instances managing actual customers.
---
## Test Emails & Phone Numbers
### Fake Email Addresses
Any email with the `+clerk_test` subaddress is a test email address:
```
jane+clerk_test@example.com
john+clerk_test@gmail.com
test+clerk_test@mycompany.com
```
**Behavior**:
- ✅ No emails sent (saves your email quota)
- ✅ Fixed OTP code: `424242`
- ✅ Works in all sign-up/sign-in flows
### Fake Phone Numbers
Any [fictional phone number](https://en.wikipedia.org/wiki/555_(telephone_number)) is a test phone number:
**Format**: `+1 (XXX) 555-0100` to `+1 (XXX) 555-0199`
**Examples**:
```
+12015550100
+19735550133
+14155550142
```
**Behavior**:
- ✅ No SMS sent (saves your SMS quota)
- ✅ Fixed OTP code: `424242`
- ✅ Works in all verification flows
### Monthly Limits (Development Instances)
Clerk development instances have limits:
- **20 SMS messages** per month
- **100 emails** per month
**Excluded from limits**:
- SMS to US numbers
- SMS/emails to test addresses (with `+clerk_test` or 555 numbers)
- Self-delivered messages (your own SMTP/SMS provider)
- Paid subscription apps
To request higher limits, contact Clerk support.
---
## Code Examples: Test Credentials
### Sign In with Test Email
```tsx
import { useSignIn } from '@clerk/clerk-react'
const testSignInWithEmailCode = async () => {
const { signIn } = useSignIn()
const emailAddress = 'john+clerk_test@example.com'
// Step 1: Create sign-in attempt
const signInResp = await signIn.create({ identifier: emailAddress })
// Step 2: Find email code factor
const { emailAddressId } = signInResp.supportedFirstFactors.find(
(ff) => ff.strategy === 'email_code' && ff.safeIdentifier === emailAddress,
)! as EmailCodeFactor
// Step 3: Prepare email verification
await signIn.prepareFirstFactor({
strategy: 'email_code',
emailAddressId: emailAddressId,
})
// Step 4: Verify with fixed code
const attemptResponse = await signIn.attemptFirstFactor({
strategy: 'email_code',
code: '424242', // Fixed test code
})
if (attemptResponse.status === 'complete') {
console.log('Sign in successful!')
} else {
console.error('Sign in failed')
}
}
```
### Sign Up with Test Phone
```tsx
import { useSignUp } from '@clerk/clerk-react'
const testSignUpWithPhoneNumber = async () => {
const { signUp } = useSignUp()
// Step 1: Create sign-up with test phone
await signUp.create({
phoneNumber: '+12015550100',
})
// Step 2: Prepare phone verification
await signUp.preparePhoneNumberVerification()
// Step 3: Verify with fixed code
const res = await signUp.attemptPhoneNumberVerification({
code: '424242', // Fixed test code
})
if (res.verifications.phoneNumber.status === 'verified') {
console.log('Sign up successful!')
} else {
console.error('Sign up failed')
}
}
```
---
## Session Tokens (Backend API)
For testing API endpoints or backend services, you need valid session tokens.
### Flow: Generate Session Token
**4-Step Process**:
1. **Create User** (if needed)
2. **Create Session** for user
3. **Create Session Token** from session ID
4. **Use Token** in Authorization header
### Step-by-Step Implementation
#### Step 1: Create User
**Endpoint**: `POST https://api.clerk.com/v1/users`
```bash
curl -X POST https://api.clerk.com/v1/users \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"email_address": ["test+clerk_test@example.com"],
"password": "TestPassword123!"
}'
```
**Response**:
```json
{
"id": "user_2abc123def456",
"email_addresses": [
{
"id": "idn_2xyz789",
"email_address": "test+clerk_test@example.com"
}
]
}
```
**Save**: `user_id` for next step
#### Step 2: Create Session
**Endpoint**: `POST https://api.clerk.com/v1/sessions`
```bash
curl -X POST https://api.clerk.com/v1/sessions \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_2abc123def456"
}'
```
**Response**:
```json
{
"id": "sess_2xyz789abc123",
"user_id": "user_2abc123def456",
"status": "active"
}
```
**Save**: `session_id` for next step
#### Step 3: Create Session Token
**Endpoint**: `POST https://api.clerk.com/v1/sessions/{session_id}/tokens`
```bash
curl -X POST https://api.clerk.com/v1/sessions/sess_2xyz789abc123/tokens \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY"
```
**Response**:
```json
{
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"object": "token"
}
```
**Save**: `jwt` token
#### Step 4: Use Token in Requests
```bash
curl https://yourdomain.com/api/protected \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
```
### Token Lifetime
**CRITICAL**: Session tokens are valid for **60 seconds only**.
**Refresh Strategies**:
**Option 1: Before Each Test**
```typescript
beforeEach(async () => {
sessionToken = await refreshSessionToken(sessionId)
})
```
**Option 2: Interval Timer**
```typescript
setInterval(async () => {
sessionToken = await refreshSessionToken(sessionId)
}, 50000) // Refresh every 50 seconds
```
### Node.js Script Example
See `scripts/generate-session-token.js` for a complete implementation.
---
## Testing Tokens (Bot Detection Bypass)
Testing Tokens bypass Clerk's bot detection mechanisms during automated testing.
### What Are Testing Tokens?
- **Purpose**: Prevent "Bot traffic detected" errors in test suites
- **Lifetime**: Short-lived (expires after use)
- **Scope**: Valid only for specific Clerk instance
- **Source**: Obtained via Backend API
### When to Use
**Use Testing Tokens when**:
- Running E2E tests with Playwright or Cypress
- Automated test suites triggering bot detection
- CI/CD pipelines running authentication flows
**Alternatives**:
- Use `@clerk/testing` package (handles automatically)
- Playwright integration (recommended)
- Cypress integration (recommended)
### Obtain Testing Token
**Endpoint**: `POST https://api.clerk.com/v1/testing_tokens`
```bash
curl -X POST https://api.clerk.com/v1/testing_tokens \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY"
```
**Response**:
```json
{
"token": "1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy"
}
```
### Use Testing Token
Add `__clerk_testing_token` query parameter to Frontend API requests:
```
POST https://happy-hippo-1.clerk.accounts.dev/v1/client/sign_ups?__clerk_testing_token=1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy
```
### Production Limitations
Testing Tokens work in **both development and production**, but:
**❌ Not Supported in Production**:
- Code-based authentication (SMS OTP, Email OTP)
**✅ Supported in Production**:
- Email + password authentication
- Email magic link (sign-in via email)
---
## E2E Testing with Playwright
Clerk provides first-class Playwright support via `@clerk/testing`.
### Install
```bash
npm install -D @clerk/testing
```
### Set Environment Variables
In your test runner (e.g., `.env.test` or GitHub Actions secrets):
```bash
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```
**Security**: Use GitHub Actions secrets or similar for CI/CD.
### Global Setup
Configure `clerkSetup()` to obtain Testing Token once for all tests:
**File**: `global.setup.ts`
```typescript
import { clerkSetup } from '@clerk/testing/playwright'
import { test as setup } from '@playwright/test'
// Run setup serially (important for fully parallel Playwright config)
setup.describe.configure({ mode: 'serial' })
setup('global setup', async ({}) => {
await clerkSetup()
})
```
**What This Does**:
- Obtains Testing Token from Clerk Backend API
- Stores token in `CLERK_TESTING_TOKEN` environment variable
- Makes token available for all tests
### Use in Tests
Import `setupClerkTestingToken()` and call before navigating to auth pages:
**File**: `auth.spec.ts`
```typescript
import { setupClerkTestingToken } from '@clerk/testing/playwright'
import { test, expect } from '@playwright/test'
test('sign up flow', async ({ page }) => {
// Inject Testing Token for this test
await setupClerkTestingToken({ page })
// Navigate to sign-up page
await page.goto('/sign-up')
// Fill form with test credentials
await page.fill('input[name="emailAddress"]', 'test+clerk_test@example.com')
await page.fill('input[name="password"]', 'TestPassword123!')
await page.click('button[type="submit"]')
// Verify with fixed OTP
await page.fill('input[name="code"]', '424242')
await page.click('button[type="submit"]')
// Assert success
await expect(page).toHaveURL('/dashboard')
})
test('sign in with test phone', async ({ page }) => {
await setupClerkTestingToken({ page })
await page.goto('/sign-in')
await page.fill('input[name="identifier"]', '+12015550100')
await page.click('button[type="submit"]')
// Enter fixed OTP
await page.fill('input[name="code"]', '424242')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
```
### Manual Testing Token Setup (Alternative)
Instead of `clerkSetup()`, manually set the environment variable:
```bash
export CLERK_TESTING_TOKEN="1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy"
```
Then run tests as usual. `setupClerkTestingToken()` will use this value.
### Demo Repository
Clerk provides a complete example:
**Repository**: https://github.com/clerk/clerk-playwright-nextjs
**Features**:
- Next.js App Router with Clerk
- Playwright E2E tests
- Testing Tokens setup
- Test user authentication
**To Run**:
1. Clone repo
2. Add Clerk API keys to `.env.local`
3. Create test user with username + password
4. Enable username/password auth in Clerk Dashboard
5. Run `npm test`
---
## Testing Email Links (Magic Links)
Email links are challenging to test in E2E suites.
**Recommendation**: Use email verification codes instead.
### Enable Email Verification Code
1. Clerk Dashboard → **Email, Phone, Username**
2. Enable **Email verification code** strategy
3. Use the code flow in tests (easier to automate)
**Code flow** and **link flow** are functionally equivalent for most use cases.
---
## Best Practices
### Development Testing
**Do**:
- Use test emails (`+clerk_test`) and phone numbers (555-01XX)
- Fixed OTP: `424242`
- Enable test mode in Clerk Dashboard
- Use `@clerk/testing` for Playwright/Cypress
**Don't**:
- Send real emails/SMS during tests (wastes quota)
- Use production keys in tests
- Enable test mode on production instances with real users
### Backend/API Testing
**Do**:
- Generate session tokens via Backend API
- Refresh tokens before each test or on interval
- Use test users created via API
- Store `CLERK_SECRET_KEY` securely
**Don't**:
- Hardcode session tokens (expire in 60 seconds)
- Reuse expired tokens
- Expose secret keys in logs or version control
### E2E Testing
**Do**:
- Use `@clerk/testing` for automatic Testing Token management
- Configure global setup for token generation
- Use test credentials in all flows
- Run tests in CI/CD with secret environment variables
**Don't**:
- Skip `setupClerkTestingToken()` (triggers bot detection)
- Manually implement Testing Token logic (use helpers)
- Test code-based auth in production with Testing Tokens
---
## Troubleshooting
### "Bot traffic detected" Error
**Cause**: Missing Testing Token in test suite
**Solution**:
1. Install `@clerk/testing`
2. Configure `clerkSetup()` in global setup
3. Call `setupClerkTestingToken({ page })` in tests
### Session Token Expired
**Cause**: Token lifetime is 60 seconds
**Solution**:
- Refresh token before each test: `beforeEach(() => refreshToken())`
- Use interval timer: `setInterval(() => refreshToken(), 50000)`
### Test Email Not Working
**Cause**: Test mode not enabled, or wrong email format
**Solution**:
- Ensure email has `+clerk_test` subaddress
- Enable test mode in Clerk Dashboard
- Use fixed OTP: `424242`
### 20 SMS / 100 Email Limit Reached
**Cause**: Exceeded monthly limit for development instance
**Solution**:
- Use test credentials (excluded from limits)
- Contact Clerk support to request higher limits
- Use self-delivered SMS/email provider
---
## Reference Links
**Official Docs**:
- Testing Overview: https://clerk.com/docs/guides/development/testing/overview
- Test Emails & Phones: https://clerk.com/docs/guides/development/testing/test-emails-and-phones
- Playwright Integration: https://clerk.com/docs/guides/development/testing/playwright/overview
- Backend API Reference: https://clerk.com/docs/reference/backend-api
**Packages**:
- `@clerk/testing`: https://github.com/clerk/javascript/tree/main/packages/testing
- Demo Repository: https://github.com/clerk/clerk-playwright-nextjs
**API Endpoints**:
- Create User: `POST /v1/users`
- Create Session: `POST /v1/sessions`
- Create Session Token: `POST /v1/sessions/{session_id}/tokens`
- Create Testing Token: `POST /v1/testing_tokens`
---
**Last Updated**: 2025-10-28

15
scripts/example-script.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# [TODO: Script Name]
# [TODO: Brief description of what this script does]
# Example script structure - delete if not needed
set -e # Exit on error
# [TODO: Add your script logic here]
echo "Example script - replace or delete this file"
# Usage:
# ./scripts/example-script.sh [args]

241
scripts/generate-session-token.js Executable file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env node
/**
* Clerk Session Token Generator
*
* Generates valid session tokens for testing Clerk authentication.
* Session tokens are valid for 60 seconds and must be refreshed regularly.
*
* Usage:
* node generate-session-token.js
* node generate-session-token.js --create-user
* node generate-session-token.js --user-id user_abc123
* node generate-session-token.js --refresh
*
* Environment Variables:
* CLERK_SECRET_KEY - Your Clerk secret key (required)
*
* @see https://clerk.com/docs/guides/development/testing/overview
*/
const https = require('https')
// Configuration
const CLERK_SECRET_KEY = process.env.CLERK_SECRET_KEY
const API_BASE = 'https://api.clerk.com/v1'
// Parse CLI arguments
const args = process.argv.slice(2)
const shouldCreateUser = args.includes('--create-user')
const shouldRefresh = args.includes('--refresh')
const userIdArg = args.find(arg => arg.startsWith('--user-id='))
const providedUserId = userIdArg ? userIdArg.split('=')[1] : null
// Validate secret key
if (!CLERK_SECRET_KEY) {
console.error('❌ Error: CLERK_SECRET_KEY environment variable is required')
console.error('\nUsage: CLERK_SECRET_KEY=sk_test_... node generate-session-token.js')
process.exit(1)
}
// Make HTTPS request
function makeRequest(path, method = 'GET', data = null) {
return new Promise((resolve, reject) => {
const url = new URL(`${API_BASE}${path}`)
const options = {
hostname: url.hostname,
path: url.pathname + url.search,
method,
headers: {
'Authorization': `Bearer ${CLERK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
}
const req = https.request(options, (res) => {
let body = ''
res.on('data', (chunk) => body += chunk)
res.on('end', () => {
try {
const json = JSON.parse(body)
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(json)
} else {
reject({
statusCode: res.statusCode,
error: json,
})
}
} catch (err) {
reject({
statusCode: res.statusCode,
error: body,
parseError: err.message,
})
}
})
})
req.on('error', reject)
if (data) {
req.write(JSON.stringify(data))
}
req.end()
})
}
// Create test user
async function createUser() {
console.log('📝 Creating test user...')
const email = `test+clerk_test_${Date.now()}@example.com`
const password = 'TestPassword123!'
try {
const user = await makeRequest('/users', 'POST', {
email_address: [email],
password: password,
skip_password_checks: true,
})
console.log('✅ User created:')
console.log(` User ID: ${user.id}`)
console.log(` Email: ${email}`)
console.log(` Password: ${password}`)
return user.id
} catch (err) {
console.error('❌ Failed to create user:', err.error)
throw err
}
}
// Get existing user (first user in instance)
async function getExistingUser() {
console.log('🔍 Finding existing user...')
try {
const response = await makeRequest('/users?limit=1')
if (response.data && response.data.length > 0) {
const user = response.data[0]
console.log(`✅ Found user: ${user.id}`)
return user.id
} else {
console.log('⚠️ No users found. Use --create-user to create one.')
return null
}
} catch (err) {
console.error('❌ Failed to get user:', err.error)
throw err
}
}
// Create session for user
async function createSession(userId) {
console.log(`🔐 Creating session for user ${userId}...`)
try {
const session = await makeRequest('/sessions', 'POST', {
user_id: userId,
})
console.log(`✅ Session created: ${session.id}`)
return session.id
} catch (err) {
console.error('❌ Failed to create session:', err.error)
throw err
}
}
// Create session token
async function createSessionToken(sessionId) {
try {
const response = await makeRequest(`/sessions/${sessionId}/tokens`, 'POST')
return response.jwt
} catch (err) {
console.error('❌ Failed to create session token:', err.error)
throw err
}
}
// Refresh session token (same as create)
async function refreshSessionToken(sessionId) {
console.log('🔄 Refreshing session token...')
const token = await createSessionToken(sessionId)
console.log('✅ Token refreshed')
return token
}
// Main function
async function main() {
console.log('🎫 Clerk Session Token Generator\n')
try {
// Step 1: Get or create user
let userId = providedUserId
if (!userId) {
if (shouldCreateUser) {
userId = await createUser()
} else {
userId = await getExistingUser()
}
} else {
console.log(`📌 Using provided user ID: ${userId}`)
}
if (!userId) {
console.log('\n💡 Tip: Run with --create-user to create a test user')
process.exit(1)
}
// Step 2: Create session
const sessionId = await createSession(userId)
// Step 3: Create token
console.log('🎫 Generating session token...')
const token = await createSessionToken(sessionId)
console.log('\n✅ Session Token Generated!\n')
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
console.log('Token (valid for 60 seconds):')
console.log(token)
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
console.log('📋 Usage in API requests:\n')
console.log('curl https://yourdomain.com/api/protected \\')
console.log(` -H "Authorization: Bearer ${token.substring(0, 50)}..."\n`)
// Step 4: Refresh mode (optional)
if (shouldRefresh) {
console.log('🔄 Refresh mode enabled. Token will refresh every 50 seconds.')
console.log('Press Ctrl+C to stop.\n')
// Refresh every 50 seconds
setInterval(async () => {
try {
const newToken = await refreshSessionToken(sessionId)
console.log(`\n🎫 New Token: ${newToken}\n`)
} catch (err) {
console.error('❌ Failed to refresh token:', err)
process.exit(1)
}
}, 50000)
} else {
console.log('💡 Tip: Add --refresh flag to auto-refresh token every 50 seconds')
console.log(`💡 Tip: Reuse this session with --user-id=${userId}`)
}
} catch (err) {
console.error('\n❌ Error:', err)
process.exit(1)
}
}
// Run main function
main()

View File

@@ -0,0 +1,210 @@
/**
* Cloudflare Worker with Clerk Authentication
*
* This template demonstrates:
* - JWT token verification with @clerk/backend
* - Protected API routes
* - Type-safe Hono context with auth state
* - Proper error handling
*
* Dependencies:
* - @clerk/backend@^2.17.2
* - hono@^4.10.1
*/
import { Hono } from 'hono'
import { verifyToken } from '@clerk/backend'
import { cors } from 'hono/cors'
// Type-safe environment bindings
type Bindings = {
CLERK_SECRET_KEY: string
CLERK_PUBLISHABLE_KEY: string
}
// Context variables with auth state
type Variables = {
userId: string | null
sessionClaims: any | null
}
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// CORS middleware (adjust origins for production)
app.use('*', cors({
origin: ['http://localhost:5173', 'https://yourdomain.com'],
credentials: true,
}))
/**
* Auth Middleware - Verifies Clerk JWT tokens
*
* CRITICAL SECURITY:
* - Always set authorizedParties to prevent CSRF attacks
* - Use secretKey, not deprecated apiKey
* - Token is in Authorization: Bearer <token> header
*/
app.use('/api/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// No auth header - continue as unauthenticated
c.set('userId', null)
c.set('sessionClaims', null)
return next()
}
// Extract token from "Bearer <token>"
const token = authHeader.substring(7)
try {
// Verify token with Clerk
const { data, error } = await verifyToken(token, {
secretKey: c.env.CLERK_SECRET_KEY,
// IMPORTANT: Set to your actual domain(s) to prevent CSRF
// Source: https://clerk.com/docs/reference/backend/verify-token
authorizedParties: [
'http://localhost:5173', // Development
'https://yourdomain.com', // Production
],
})
if (error) {
console.error('[Auth] Token verification failed:', error.message)
c.set('userId', null)
c.set('sessionClaims', null)
} else {
// 'sub' claim contains the user ID
c.set('userId', data.sub)
c.set('sessionClaims', data)
}
} catch (err) {
console.error('[Auth] Token verification error:', err)
c.set('userId', null)
c.set('sessionClaims', null)
}
return next()
})
/**
* Public Routes - No authentication required
*/
app.get('/api/public', (c) => {
return c.json({
message: 'This endpoint is public',
timestamp: new Date().toISOString(),
})
})
app.get('/api/health', (c) => {
return c.json({
status: 'ok',
version: '1.0.0',
})
})
/**
* Protected Routes - Require authentication
*/
app.get('/api/protected', (c) => {
const userId = c.get('userId')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
return c.json({
message: 'This endpoint is protected',
userId,
timestamp: new Date().toISOString(),
})
})
app.get('/api/user/profile', (c) => {
const userId = c.get('userId')
const sessionClaims = c.get('sessionClaims')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Access custom claims from JWT template (if configured)
return c.json({
userId,
email: sessionClaims?.email,
role: sessionClaims?.role,
organizationId: sessionClaims?.organization_id,
})
})
/**
* POST Example - Create resource with auth
*/
app.post('/api/items', async (c) => {
const userId = c.get('userId')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
const body = await c.req.json()
// Validate and process body
// Example: save to D1, KV, or R2
return c.json({
success: true,
itemId: crypto.randomUUID(),
userId,
}, 201)
})
/**
* Role-Based Access Control Example
*/
app.get('/api/admin/dashboard', (c) => {
const userId = c.get('userId')
const sessionClaims = c.get('sessionClaims')
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Check role from custom JWT claims
const role = sessionClaims?.role
if (role !== 'admin') {
return c.json({ error: 'Forbidden: Admin access required' }, 403)
}
return c.json({
message: 'Admin dashboard data',
userId,
})
})
/**
* Error Handling
*/
app.onError((err, c) => {
console.error('[Error]', err)
return c.json({ error: 'Internal Server Error' }, 500)
})
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
/**
* Export the Hono app
*
* ES Module format for Cloudflare Workers
*/
export default app

View File

@@ -0,0 +1,46 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-clerk-worker",
"main": "src/index.ts",
"account_id": "YOUR_ACCOUNT_ID",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
"vars": {
"CLERK_PUBLISHABLE_KEY": "pk_test_..."
}
/**
* CRITICAL: Never commit CLERK_SECRET_KEY to version control
*
* To set CLERK_SECRET_KEY:
*
* 1. Production:
* wrangler secret put CLERK_SECRET_KEY
*
* 2. Local development:
* Create .dev.vars file (see templates/env-examples/.dev.vars.example)
*
* After setting, access via c.env.CLERK_SECRET_KEY in your Worker
*/
/**
* Optional: Add other Cloudflare bindings
*
* KV Namespace:
* "kv_namespaces": [
* { "binding": "AUTH_CACHE", "id": "YOUR_KV_ID" }
* ]
*
* D1 Database:
* "d1_databases": [
* { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
* ]
*
* R2 Bucket:
* "r2_buckets": [
* { "binding": "ASSETS", "bucket_name": "my-bucket" }
* ]
*/
}

View File

@@ -0,0 +1,63 @@
# Clerk Environment Variables for Cloudflare Workers
#
# Copy this file to .dev.vars for local development
# Get your keys from https://dashboard.clerk.com
# ==========================================
# LOCAL DEVELOPMENT (.dev.vars)
# ==========================================
# Secret Key (server-side verification)
CLERK_SECRET_KEY=sk_test_...
# Publishable Key (can be in wrangler.jsonc or here)
CLERK_PUBLISHABLE_KEY=pk_test_...
# ==========================================
# PRODUCTION DEPLOYMENT
# ==========================================
# For production, use wrangler secrets:
#
# 1. Set secret key (encrypted, not in wrangler.jsonc):
# wrangler secret put CLERK_SECRET_KEY
#
# 2. Set publishable key in wrangler.jsonc:
# {
# "vars": {
# "CLERK_PUBLISHABLE_KEY": "pk_live_..."
# }
# }
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .dev.vars to version control
# .dev.vars is in .gitignore by default
#
# 2. Use different keys for development and production
# - Development: pk_test_... / sk_test_...
# - Production: pk_live_... / sk_live_...
#
# 3. Production secrets via wrangler secret put
# This encrypts secrets, they won't appear in wrangler.jsonc
#
# 4. Rotate CLERK_SECRET_KEY if compromised
# Generate new keys in Clerk Dashboard
# ==========================================
# OPTIONAL - Additional Bindings
# ==========================================
# If your Worker uses other services, add them here:
# DATABASE_URL=...
# API_KEY=...
# ==========================================
# REFERENCE
# ==========================================
# Official Docs:
# https://clerk.com/docs/reference/backend/verify-token
# https://developers.cloudflare.com/workers/wrangler/configuration/

View File

@@ -0,0 +1,89 @@
# Clerk Environment Variables for Next.js
#
# Copy this file to .env.local and fill in your actual values
# Get your keys from https://dashboard.clerk.com
# ==========================================
# REQUIRED
# ==========================================
# Publishable Key (safe to expose to client)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
# Secret Key (NEVER expose to client, server-side only)
CLERK_SECRET_KEY=sk_test_...
# ==========================================
# OPTIONAL - Custom Pages
# ==========================================
# Uncomment to use custom sign-in/sign-up pages
# NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
# NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# ==========================================
# OPTIONAL - Redirect URLs
# ==========================================
# Where to redirect after sign-in (forced - always goes here)
# NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
# Where to redirect after sign-up (forced - always goes here)
# NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
# Fallback redirect if no forced redirect is set (default: /)
# NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
# NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
# ==========================================
# OPTIONAL - Webhooks
# ==========================================
# Webhook signing secret for verifying Clerk webhooks
# Get this from Clerk Dashboard > Webhooks > Add Endpoint
# CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
# ==========================================
# OPTIONAL - Multi-Domain (Satellite Domains)
# ==========================================
# For multi-domain authentication
# NEXT_PUBLIC_CLERK_DOMAIN=accounts.yourdomain.com
# NEXT_PUBLIC_CLERK_IS_SATELLITE=true
# ==========================================
# OPTIONAL - Advanced Configuration
# ==========================================
# Custom Clerk JS URL (usually not needed)
# NEXT_PUBLIC_CLERK_JS_URL=https://...
# Proxy URL for requests (enterprise feature)
# NEXT_PUBLIC_CLERK_PROXY_URL=https://...
# Disable telemetry
# CLERK_TELEMETRY_DISABLED=1
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .env.local to version control
# Add .env.local to .gitignore
#
# 2. Use different keys for development and production
# - Development: pk_test_... / sk_test_...
# - Production: pk_live_... / sk_live_...
#
# 3. NEVER use NEXT_PUBLIC_ prefix for secrets
# NEXT_PUBLIC_ variables are exposed to the browser
#
# 4. Rotate CLERK_SECRET_KEY if compromised
# Generate new keys in Clerk Dashboard
# ==========================================
# REFERENCE
# ==========================================
# Official Docs:
# https://clerk.com/docs/guides/development/clerk-environment-variables

View File

@@ -0,0 +1,46 @@
# Clerk Environment Variables for React + Vite
#
# Copy this file to .env.local and fill in your actual values
# Get your keys from https://dashboard.clerk.com
# ==========================================
# REQUIRED
# ==========================================
# Publishable Key (safe to expose to client)
# CRITICAL: Must use VITE_ prefix for Vite to expose to client
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
# ==========================================
# SECURITY NOTES
# ==========================================
# 1. NEVER commit .env.local to version control
# Add .env.local to .gitignore
#
# 2. Must use VITE_ prefix for client-side variables
# Without VITE_ prefix, variable won't be available
#
# 3. Only VITE_ prefixed vars are exposed to browser
# Never use VITE_ prefix for secrets
#
# 4. Restart dev server after changing .env.local
# Vite only reads env vars on startup
#
# 5. Use different keys for development and production
# - Development: pk_test_...
# - Production: pk_live_...
# ==========================================
# ACCESS IN CODE
# ==========================================
# Use import.meta.env to access:
# const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
# ==========================================
# REFERENCE
# ==========================================
# Vite Env Vars: https://vitejs.dev/guide/env-and-mode.html
# Clerk Docs: https://clerk.com/docs/references/react/clerk-provider

View File

@@ -0,0 +1,23 @@
{
"$comment": "Advanced Clerk JWT Template - Multi-Tenant with Fallbacks",
"$description": "This template demonstrates advanced features: string interpolation, conditional expressions, nested metadata access, and organization claims. Copy this JSON (without $ prefixed fields) into Clerk Dashboard.",
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"full_name": "{{user.last_name}} {{user.first_name}}",
"avatar": "{{user.image_url}}",
"role": "{{user.public_metadata.role || 'user'}}",
"department": "{{user.public_metadata.department || 'general'}}",
"permissions": "{{user.public_metadata.permissions}}",
"org_id": "{{user.public_metadata.org_id}}",
"org_slug": "{{user.public_metadata.org_slug}}",
"org_role": "{{user.public_metadata.org_role}}",
"interests": "{{user.public_metadata.profile.interests}}",
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}",
"onboarding_complete": "{{user.public_metadata.onboardingComplete || false}}"
}

View File

@@ -0,0 +1,8 @@
{
"$comment": "Basic Clerk JWT Template - Role-Based Access Control",
"$description": "This template includes minimal user information for role-based authentication. Copy this JSON (without comments) into Clerk Dashboard > Sessions > Customize session token > Create template.",
"user_id": "{{user.id}}",
"email": "{{user.primary_email_address}}",
"role": "{{user.public_metadata.role || 'user'}}"
}

View File

@@ -0,0 +1,14 @@
{
"$comment": "Grafbase GraphQL Integration JWT Template",
"$description": "This template is for Grafbase integration with role-based access control. Grafbase uses 'groups' array for authorization. Name it 'grafbase' in Clerk Dashboard.",
"$usage": "const token = await getToken({ template: 'grafbase' }); // Use in GraphQL requests",
"$grafbase_config": "In grafbase.toml: [auth.providers.clerk] issuer = 'https://your-app.clerk.accounts.dev' jwks = 'https://your-app.clerk.accounts.dev/.well-known/jwks.json'",
"sub": "{{user.id}}",
"groups": [
"org:{{user.public_metadata.org_role || 'member'}}",
"user:authenticated"
],
"email": "{{user.primary_email_address}}",
"name": "{{user.full_name || user.first_name}}"
}

View File

@@ -0,0 +1,17 @@
{
"$comment": "Supabase Integration JWT Template",
"$description": "This template is designed for Supabase integration. Name it 'supabase' in Clerk Dashboard. Use with getToken({ template: 'supabase' }) to authenticate Supabase client.",
"$usage": "const token = await getToken({ template: 'supabase' }); const supabase = createClient(url, key, { global: { headers: { Authorization: `Bearer ${token}` } } });",
"aud": "authenticated",
"email": "{{user.primary_email_address}}",
"app_metadata": {
"provider": "clerk",
"providers": ["clerk"]
},
"user_metadata": {
"full_name": "{{user.full_name || user.first_name || 'User'}}",
"avatar_url": "{{user.image_url}}",
"email": "{{user.primary_email_address}}"
}
}

View File

@@ -0,0 +1,101 @@
/**
* Next.js App Router Layout with Clerk
*
* Place this in app/layout.tsx
*
* Dependencies:
* - @clerk/nextjs@^6.33.3
*/
import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'My App',
description: 'Authenticated with Clerk',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
)
}
/**
* With Dark Mode Support (using next-themes):
*
* 1. Install: npm install next-themes
* 2. Use this pattern:
*/
/*
import { ClerkProvider } from '@clerk/nextjs'
import { ThemeProvider } from 'next-themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</ClerkProvider>
)
}
*/
/**
* With Clerk Appearance Customization:
*/
/*
import { ClerkProvider } from '@clerk/nextjs'
import { dark } from '@clerk/themes'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
colorBackground: '#0f172a',
},
elements: {
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
card: 'shadow-xl',
},
}}
>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
)
}
*/

View File

@@ -0,0 +1,143 @@
/**
* Next.js Middleware with Clerk Authentication
*
* This middleware protects routes using Clerk's clerkMiddleware.
* Place this file in the root of your Next.js project.
*
* Dependencies:
* - @clerk/nextjs@^6.33.3
*
* CRITICAL (v6 Breaking Change):
* - auth.protect() is now async - must use await
* - Source: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
*/
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
/**
* Define public routes (routes that don't require authentication)
*
* Glob patterns supported:
* - '/path' - exact match
* - '/path(.*)' - path and all sub-paths
* - '/api/public/*' - wildcard
*/
const isPublicRoute = createRouteMatcher([
'/', // Homepage
'/sign-in(.*)', // Sign-in page and sub-paths
'/sign-up(.*)', // Sign-up page and sub-paths
'/api/public(.*)', // Public API routes
'/api/webhooks(.*)', // Webhook endpoints
'/about', // Static pages
'/pricing',
'/contact',
])
/**
* Alternative: Define protected routes instead
*
* Uncomment this pattern if you prefer to explicitly protect
* specific routes rather than inverting the logic:
*/
/*
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/profile(.*)',
'/admin(.*)',
'/api/private(.*)',
])
export default clerkMiddleware(async (auth, request) => {
if (isProtectedRoute(request)) {
await auth.protect()
}
})
*/
/**
* Default Pattern: Protect all routes except public ones
*
* CRITICAL:
* - auth.protect() MUST be awaited (async in v6)
* - Without await, route protection will not work
*/
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
/**
* Matcher Configuration
*
* Defines which paths run middleware.
* This is the recommended configuration from Clerk.
*/
export const config = {
matcher: [
// Skip Next.js internals and static files
'/((?!_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)(.*)',
],
}
/**
* Advanced: Role-Based Protection
*
* Protect routes based on user role or organization membership:
*/
/*
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isOrgRoute = createRouteMatcher(['/org(.*)'])
export default clerkMiddleware(async (auth, request) => {
// Admin routes require 'admin' role
if (isAdminRoute(request)) {
await auth.protect((has) => {
return has({ role: 'admin' })
})
}
// Organization routes require organization membership
if (isOrgRoute(request)) {
await auth.protect((has) => {
return has({ permission: 'org:member' })
})
}
// All other routes use default protection
if (!isPublicRoute(request)) {
await auth.protect()
}
})
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
}
*/
/**
* Troubleshooting:
*
* 1. Routes not protected?
* - Ensure auth.protect() is awaited
* - Check matcher configuration includes your routes
* - Verify middleware.ts is in project root
*
* 2. Infinite redirects?
* - Ensure sign-in/sign-up routes are in isPublicRoute
* - Check NEXT_PUBLIC_CLERK_SIGN_IN_URL in .env.local
*
* 3. API routes returning HTML?
* - Verify '/(api|trpc)(.*)' is in matcher
* - Check API routes are not in isPublicRoute if protected
*
* Official Docs: https://clerk.com/docs/reference/nextjs/clerk-middleware
*/

View File

@@ -0,0 +1,114 @@
/**
* Server Component with Clerk Auth
*
* Demonstrates using auth() and currentUser() in Server Components
*
* CRITICAL (v6): auth() is now async - must use await
*/
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
/**
* Option 1: Lightweight auth check
*
* Use auth() when you only need userId/sessionId
* This is faster than currentUser()
*/
const { userId, sessionId } = await auth()
// Redirect if not authenticated (shouldn't happen if middleware configured)
if (!userId) {
redirect('/sign-in')
}
/**
* Option 2: Full user object
*
* Use currentUser() when you need full user data
* Heavier than auth(), so use sparingly
*/
const user = await currentUser()
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<div className="mt-4 space-y-2">
<p>
<strong>User ID:</strong> {userId}
</p>
<p>
<strong>Session ID:</strong> {sessionId}
</p>
<p>
<strong>Email:</strong>{' '}
{user?.primaryEmailAddress?.emailAddress}
</p>
<p>
<strong>Name:</strong> {user?.firstName} {user?.lastName}
</p>
{/* Access public metadata */}
{user?.publicMetadata && (
<div>
<strong>Role:</strong>{' '}
{(user.publicMetadata as any).role || 'user'}
</div>
)}
</div>
</div>
)
}
/**
* API Route Example (app/api/user/route.ts)
*/
/*
import { auth, currentUser } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = await currentUser()
return NextResponse.json({
userId,
email: user?.primaryEmailAddress?.emailAddress,
name: `${user?.firstName} ${user?.lastName}`,
})
}
*/
/**
* Protected API Route with POST (app/api/items/route.ts)
*/
/*
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
// Validate and process
// Example: save to database with userId
return NextResponse.json({
success: true,
itemId: crypto.randomUUID(),
userId,
}, { status: 201 })
}
*/

215
templates/react/App.tsx Normal file
View File

@@ -0,0 +1,215 @@
/**
* React App Component with Clerk Hooks
*
* Demonstrates:
* - useUser() for user data
* - useAuth() for session tokens
* - useClerk() for auth methods
* - Proper loading state handling
*/
import { useUser, useAuth, useClerk, SignInButton, UserButton } from '@clerk/clerk-react'
function App() {
// Get user object (includes email, metadata, etc.)
const { isLoaded, isSignedIn, user } = useUser()
// Get auth state and session methods
const { userId, getToken } = useAuth()
// Get Clerk instance for advanced operations
const { openSignIn, signOut } = useClerk()
/**
* CRITICAL: Always check isLoaded before rendering
*
* Why: Prevents flash of wrong content while Clerk initializes
* Source: https://clerk.com/docs/references/react/use-user
*/
if (!isLoaded) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
)
}
/**
* Unauthenticated View
*/
if (!isSignedIn) {
return (
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
<h1 className="text-4xl font-bold">Welcome</h1>
<p className="text-gray-600">Sign in to continue</p>
{/* Option 1: Clerk's pre-built button */}
<SignInButton mode="modal">
<button className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
Sign In
</button>
</SignInButton>
{/* Option 2: Custom button with openSignIn() */}
{/* <button
onClick={() => openSignIn()}
className="px-6 py-2 bg-blue-500 text-white rounded-lg"
>
Sign In
</button> */}
</div>
)
}
/**
* Authenticated View
*/
return (
<div className="container mx-auto p-8">
<header className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
{/* Clerk's pre-built user button (profile + sign out) */}
<UserButton afterSignOutUrl="/" />
</header>
<div className="space-y-4">
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">User Information</h2>
<dl className="space-y-2">
<div>
<dt className="font-medium text-gray-700">User ID</dt>
<dd className="text-gray-900">{userId}</dd>
</div>
<div>
<dt className="font-medium text-gray-700">Email</dt>
<dd className="text-gray-900">
{user.primaryEmailAddress?.emailAddress}
</dd>
</div>
<div>
<dt className="font-medium text-gray-700">Name</dt>
<dd className="text-gray-900">
{user.firstName} {user.lastName}
</dd>
</div>
{/* Access public metadata */}
{user.publicMetadata && Object.keys(user.publicMetadata).length > 0 && (
<div>
<dt className="font-medium text-gray-700">Metadata</dt>
<dd className="text-gray-900">
<pre className="text-sm bg-gray-100 p-2 rounded">
{JSON.stringify(user.publicMetadata, null, 2)}
</pre>
</dd>
</div>
)}
</dl>
</div>
{/* Example: Call protected API */}
<ProtectedAPIExample getToken={getToken} />
{/* Custom sign out button */}
<button
onClick={() => signOut()}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Sign Out
</button>
</div>
</div>
)
}
/**
* Example Component: Calling Protected API
*/
function ProtectedAPIExample({ getToken }: { getToken: () => Promise<string | null> }) {
const [data, setData] = React.useState<any>(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const fetchProtectedData = async () => {
setLoading(true)
setError(null)
try {
// Get fresh session token (auto-refreshes)
const token = await getToken()
if (!token) {
throw new Error('No session token available')
}
// Call your API with Authorization header
const response = await fetch('https://your-worker.workers.dev/api/protected', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
const result = await response.json()
setData(result)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Protected API Call</h2>
<button
onClick={fetchProtectedData}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Fetch Protected Data'}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 text-red-700 rounded">
Error: {error}
</div>
)}
{data && (
<div className="mt-4">
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto">
{JSON.stringify(data, null, 2)}
</pre>
</div>
)}
</div>
)
}
export default App
/**
* Troubleshooting:
*
* 1. "Missing Publishable Key" error?
* - Check .env.local has VITE_CLERK_PUBLISHABLE_KEY
* - Restart dev server after adding env var
*
* 2. Flash of unauthenticated content?
* - Always check isLoaded before rendering
* - Show loading state while isLoaded is false
*
* 3. Token not working with API?
* - Ensure getToken() is called fresh (don't cache)
* - Check Authorization header format: "Bearer <token>"
* - Verify API is using @clerk/backend to verify token
*/

66
templates/react/main.tsx Normal file
View File

@@ -0,0 +1,66 @@
/**
* React + Vite Entry Point with Clerk
*
* Place this in src/main.tsx
*
* Dependencies:
* - @clerk/clerk-react@^5.51.0
*/
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ClerkProvider } from '@clerk/clerk-react'
import App from './App.tsx'
import './index.css'
// Get publishable key from environment
// CRITICAL: Must use VITE_ prefix for Vite to expose to client
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY in .env.local')
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<App />
</ClerkProvider>
</React.StrictMode>,
)
/**
* With Dark Mode Support (using custom theme):
*/
/*
import { ClerkProvider } from '@clerk/clerk-react'
import { dark } from '@clerk/themes'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
},
}}
>
<App />
</ClerkProvider>
</React.StrictMode>,
)
*/
/**
* Environment Variables:
*
* Create .env.local with:
* VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
*
* CRITICAL:
* - Must use VITE_ prefix (Vite requirement)
* - Never commit .env.local to version control
* - Use different keys for development and production
*/

View File

@@ -0,0 +1,125 @@
/**
* Custom JWT Session Claims Type Definitions
*
* This file provides TypeScript type safety for custom JWT claims in Clerk.
* Place this in your project's types/ directory (e.g., types/globals.d.ts).
*
* After adding this, sessionClaims will have auto-complete and type checking
* for your custom claims.
*
* Usage:
* ```typescript
* import { auth } from '@clerk/nextjs/server'
*
* const { sessionClaims } = await auth()
* const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
* ```
*/
export {}
declare global {
/**
* Extend Clerk's CustomJwtSessionClaims interface with your custom claims.
*
* IMPORTANT: The structure must match your JWT template exactly.
*/
interface CustomJwtSessionClaims {
/**
* Custom metadata claims
*/
metadata: {
/**
* User's role in the application
* Maps to: {{user.public_metadata.role}}
*/
role?: 'admin' | 'moderator' | 'user'
/**
* Whether user has completed onboarding
* Maps to: {{user.public_metadata.onboardingComplete}}
*/
onboardingComplete?: boolean
/**
* User's department
* Maps to: {{user.public_metadata.department}}
*/
department?: string
/**
* User's permissions array
* Maps to: {{user.public_metadata.permissions}}
*/
permissions?: string[]
/**
* Organization ID for multi-tenant apps
* Maps to: {{user.public_metadata.org_id}}
*/
organizationId?: string
/**
* Organization slug for multi-tenant apps
* Maps to: {{user.public_metadata.org_slug}}
*/
organizationSlug?: string
/**
* User's role in organization
* Maps to: {{user.public_metadata.org_role}}
*/
organizationRole?: string
}
/**
* User's email address (if included in template)
* Maps to: {{user.primary_email_address}}
*/
email?: string
/**
* User's full name (if included in template)
* Maps to: {{user.full_name}}
*/
full_name?: string
/**
* User ID (if included in template)
* Maps to: {{user.id}}
* Note: Also available as 'sub' in default claims
*/
user_id?: string
}
}
/**
* Example Usage in Next.js Server Component:
*
* ```typescript
* import { auth } from '@clerk/nextjs/server'
*
* export default async function AdminPage() {
* const { sessionClaims } = await auth()
*
* // TypeScript knows about these properties now
* if (sessionClaims?.metadata?.role !== 'admin') {
* return <div>Unauthorized</div>
* }
*
* return <div>Admin Dashboard</div>
* }
* ```
*
* Example Usage in Cloudflare Workers:
*
* ```typescript
* import { verifyToken } from '@clerk/backend'
*
* const { data } = await verifyToken(token, { secretKey })
*
* // Access custom claims with type safety
* const role = data.metadata?.role
* const isAdmin = role === 'admin'
* ```
*/

View File

@@ -0,0 +1,42 @@
{
"$comment": "Vite + Clerk: package.json with increased header size limit",
"$description": "This template shows how to configure Vite dev server to handle Clerk's authentication handshake tokens, which can exceed the default 8KB Node.js header limit when using custom JWT claims.",
"name": "vite-clerk-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@clerk/clerk-react": "^5.51.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.0"
},
"$notes": [
"The key change is in the 'dev' script:",
"NODE_OPTIONS='--max-http-header-size=32768' increases the limit from 8KB to 32KB",
"",
"For Windows PowerShell, use:",
"\"dev\": \"cross-env NODE_OPTIONS=--max-http-header-size=32768 vite\"",
"And install: npm install -D cross-env",
"",
"This prevents '431 Request Header Fields Too Large' errors when:",
"- Testing Clerk authentication in development mode",
"- Using custom JWT claims that increase token size",
"- Clerk's __clerk_handshake parameter exceeds header limit",
"",
"Production deployments (Cloudflare, Vercel, Netlify) don't need this fix."
]
}