commit d3ec204941243ad0e218126fbac8632e3367c0ca Author: Zhongwei Li Date: Sun Nov 30 08:24:03 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..d7cd4cc --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..48527af --- /dev/null +++ b/README.md @@ -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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..cb73a8e --- /dev/null +++ b/SKILL.md @@ -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
Unauthorized
+ } + + return
User ID: {userId}
+} +``` + +### 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 ` + +### 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. diff --git a/assets/example-template.txt b/assets/example-template.txt new file mode 100644 index 0000000..349fec2 --- /dev/null +++ b/assets/example-template.txt @@ -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. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..02a8d38 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/references/common-errors.md b/references/common-errors.md new file mode 100644 index 0000000..039a020 --- /dev/null +++ b/references/common-errors.md @@ -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 | 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 \` 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 `` or `` 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 + + + +``` + +✅ **New (Recommended)**: +```tsx + + + +``` + +### 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 ( + + ); +} +``` + +```tsx +// SignupPage.tsx +import { SignUp } from '@clerk/clerk-react'; + +export function SignupPage() { + return ( + + ); +} +``` + +### 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 + diff --git a/references/example-reference.md b/references/example-reference.md new file mode 100644 index 0000000..1be1b40 --- /dev/null +++ b/references/example-reference.md @@ -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. diff --git a/references/jwt-claims-guide.md b/references/jwt-claims-guide.md new file mode 100644 index 0000000..ddfd4bf --- /dev/null +++ b/references/jwt-claims-guide.md @@ -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 " +} +``` + +**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 diff --git a/references/testing-guide.md b/references/testing-guide.md new file mode 100644 index 0000000..87d2531 --- /dev/null +++ b/references/testing-guide.md @@ -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 diff --git a/scripts/example-script.sh b/scripts/example-script.sh new file mode 100755 index 0000000..1c0c72e --- /dev/null +++ b/scripts/example-script.sh @@ -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] diff --git a/scripts/generate-session-token.js b/scripts/generate-session-token.js new file mode 100755 index 0000000..153f7c3 --- /dev/null +++ b/scripts/generate-session-token.js @@ -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() diff --git a/templates/cloudflare/worker-auth.ts b/templates/cloudflare/worker-auth.ts new file mode 100644 index 0000000..9c51075 --- /dev/null +++ b/templates/cloudflare/worker-auth.ts @@ -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 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 " + 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 diff --git a/templates/cloudflare/wrangler.jsonc b/templates/cloudflare/wrangler.jsonc new file mode 100644 index 0000000..f23c446 --- /dev/null +++ b/templates/cloudflare/wrangler.jsonc @@ -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" } + * ] + */ +} diff --git a/templates/env-examples/.dev.vars.example b/templates/env-examples/.dev.vars.example new file mode 100644 index 0000000..5fd29a2 --- /dev/null +++ b/templates/env-examples/.dev.vars.example @@ -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/ diff --git a/templates/env-examples/.env.local.example b/templates/env-examples/.env.local.example new file mode 100644 index 0000000..e337bc3 --- /dev/null +++ b/templates/env-examples/.env.local.example @@ -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 diff --git a/templates/env-examples/.env.local.vite.example b/templates/env-examples/.env.local.vite.example new file mode 100644 index 0000000..0eb60cd --- /dev/null +++ b/templates/env-examples/.env.local.vite.example @@ -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 diff --git a/templates/jwt/advanced-template.json b/templates/jwt/advanced-template.json new file mode 100644 index 0000000..77e7e4c --- /dev/null +++ b/templates/jwt/advanced-template.json @@ -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}}" +} diff --git a/templates/jwt/basic-template.json b/templates/jwt/basic-template.json new file mode 100644 index 0000000..410e475 --- /dev/null +++ b/templates/jwt/basic-template.json @@ -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'}}" +} diff --git a/templates/jwt/grafbase-template.json b/templates/jwt/grafbase-template.json new file mode 100644 index 0000000..250c4dc --- /dev/null +++ b/templates/jwt/grafbase-template.json @@ -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}}" +} diff --git a/templates/jwt/supabase-template.json b/templates/jwt/supabase-template.json new file mode 100644 index 0000000..67084b3 --- /dev/null +++ b/templates/jwt/supabase-template.json @@ -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}}" + } +} diff --git a/templates/nextjs/app-layout.tsx b/templates/nextjs/app-layout.tsx new file mode 100644 index 0000000..467ca35 --- /dev/null +++ b/templates/nextjs/app-layout.tsx @@ -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 ( + + + {children} + + + ) +} + +/** + * 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 ( + + + + + {children} + + + + + ) +} +*/ + +/** + * With Clerk Appearance Customization: + */ +/* +import { ClerkProvider } from '@clerk/nextjs' +import { dark } from '@clerk/themes' + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} +*/ diff --git a/templates/nextjs/middleware.ts b/templates/nextjs/middleware.ts new file mode 100644 index 0000000..089f7f7 --- /dev/null +++ b/templates/nextjs/middleware.ts @@ -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 + */ diff --git a/templates/nextjs/server-component-example.tsx b/templates/nextjs/server-component-example.tsx new file mode 100644 index 0000000..da6bde4 --- /dev/null +++ b/templates/nextjs/server-component-example.tsx @@ -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 ( +
+

Dashboard

+ +
+

+ User ID: {userId} +

+

+ Session ID: {sessionId} +

+

+ Email:{' '} + {user?.primaryEmailAddress?.emailAddress} +

+

+ Name: {user?.firstName} {user?.lastName} +

+ + {/* Access public metadata */} + {user?.publicMetadata && ( +
+ Role:{' '} + {(user.publicMetadata as any).role || 'user'} +
+ )} +
+
+ ) +} + +/** + * 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 }) +} +*/ diff --git a/templates/react/App.tsx b/templates/react/App.tsx new file mode 100644 index 0000000..cd0214e --- /dev/null +++ b/templates/react/App.tsx @@ -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 ( +
+
Loading...
+
+ ) + } + + /** + * Unauthenticated View + */ + if (!isSignedIn) { + return ( +
+

Welcome

+

Sign in to continue

+ + {/* Option 1: Clerk's pre-built button */} + + + + + {/* Option 2: Custom button with openSignIn() */} + {/* */} +
+ ) + } + + /** + * Authenticated View + */ + return ( +
+
+

Dashboard

+ + {/* Clerk's pre-built user button (profile + sign out) */} + +
+ +
+
+

User Information

+ +
+
+
User ID
+
{userId}
+
+ +
+
Email
+
+ {user.primaryEmailAddress?.emailAddress} +
+
+ +
+
Name
+
+ {user.firstName} {user.lastName} +
+
+ + {/* Access public metadata */} + {user.publicMetadata && Object.keys(user.publicMetadata).length > 0 && ( +
+
Metadata
+
+
+                    {JSON.stringify(user.publicMetadata, null, 2)}
+                  
+
+
+ )} +
+
+ + {/* Example: Call protected API */} + + + {/* Custom sign out button */} + +
+
+ ) +} + +/** + * Example Component: Calling Protected API + */ +function ProtectedAPIExample({ getToken }: { getToken: () => Promise }) { + const [data, setData] = React.useState(null) + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState(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 ( +
+

Protected API Call

+ + + + {error && ( +
+ Error: {error} +
+ )} + + {data && ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ) +} + +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 " + * - Verify API is using @clerk/backend to verify token + */ diff --git a/templates/react/main.tsx b/templates/react/main.tsx new file mode 100644 index 0000000..de0eadf --- /dev/null +++ b/templates/react/main.tsx @@ -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( + + + + + , +) + +/** + * With Dark Mode Support (using custom theme): + */ +/* +import { ClerkProvider } from '@clerk/clerk-react' +import { dark } from '@clerk/themes' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +) +*/ + +/** + * 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 + */ diff --git a/templates/typescript/custom-jwt-types.d.ts b/templates/typescript/custom-jwt-types.d.ts new file mode 100644 index 0000000..151a827 --- /dev/null +++ b/templates/typescript/custom-jwt-types.d.ts @@ -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
Unauthorized
+ * } + * + * return
Admin Dashboard
+ * } + * ``` + * + * 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' + * ``` + */ diff --git a/templates/vite/package.json b/templates/vite/package.json new file mode 100644 index 0000000..0064f7b --- /dev/null +++ b/templates/vite/package.json @@ -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." + ] +}