Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "clerk-auth",
|
||||||
|
"description": "Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Jeremy Dawes",
|
||||||
|
"email": "jeremy@jezweb.net"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# clerk-auth
|
||||||
|
|
||||||
|
Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.
|
||||||
420
SKILL.md
Normal file
420
SKILL.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
---
|
||||||
|
name: clerk-auth
|
||||||
|
description: |
|
||||||
|
Clerk auth with API version 2025-11-10 breaking changes (billing endpoints, payment_source→payment_method), Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense. Use when: troubleshooting "Missing Clerk Secret Key", JWKS errors, authorizedParties CSRF, JWT size limits (1.2KB), 431 header errors (Vite dev mode), or testing with 424242 OTP.
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
version: 2.0.0
|
||||||
|
last_verified: 2025-11-22
|
||||||
|
sdk_versions:
|
||||||
|
nextjs: 6.35.4
|
||||||
|
backend: 2.23.2
|
||||||
|
clerk_react: 5.56.2
|
||||||
|
testing: 1.13.18
|
||||||
|
token_savings: ~52%
|
||||||
|
errors_prevented: 11
|
||||||
|
breaking_changes: Nov 2025 - API version 2025-11-10 (billing endpoints), Oct 2024 - Next.js v6 async auth()
|
||||||
|
keywords:
|
||||||
|
- clerk
|
||||||
|
- clerk auth
|
||||||
|
- api version 2025-11-10
|
||||||
|
- billing api breaking changes
|
||||||
|
- commerce to billing migration
|
||||||
|
- payment_source to payment_method
|
||||||
|
- "@clerk/nextjs"
|
||||||
|
- "@clerk/backend"
|
||||||
|
- "@clerk/clerk-react"
|
||||||
|
- next.js v6 async auth
|
||||||
|
- next.js 16 support
|
||||||
|
- pkce custom oauth
|
||||||
|
- credential stuffing defense
|
||||||
|
- client trust
|
||||||
|
- verifyToken
|
||||||
|
- authorizedParties csrf
|
||||||
|
- JWT template
|
||||||
|
- JWT size limit 1.2kb
|
||||||
|
- 431 request header too large
|
||||||
|
- vite dev mode clerk
|
||||||
|
- "@clerk/testing"
|
||||||
|
- 424242 OTP
|
||||||
|
- test credentials
|
||||||
|
- session token script
|
||||||
|
- "Missing Clerk Secret Key"
|
||||||
|
- JWKS cache race condition
|
||||||
|
- core 2 migration
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clerk Auth - Breaking Changes & Error Prevention Guide
|
||||||
|
|
||||||
|
**Package Versions**: @clerk/nextjs@6.35.4, @clerk/backend@2.23.2, @clerk/clerk-react@5.56.2, @clerk/testing@1.13.18
|
||||||
|
**Breaking Changes**: Nov 2025 - API version 2025-11-10, Oct 2024 - Next.js v6 async auth()
|
||||||
|
**Last Updated**: 2025-11-22
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's New in v6.35.x & API 2025-11-10 (Nov 2025)
|
||||||
|
|
||||||
|
### 1. API Version 2025-11-10 (Nov 10, 2025) - BREAKING CHANGES ⚠️
|
||||||
|
|
||||||
|
**Affects:** Applications using Clerk Billing/Commerce APIs
|
||||||
|
|
||||||
|
**Critical Changes:**
|
||||||
|
- **Endpoint URLs:** `/commerce/` → `/billing/` (30+ endpoints)
|
||||||
|
```
|
||||||
|
GET /v1/commerce/plans → GET /v1/billing/plans
|
||||||
|
GET /v1/commerce/statements → GET /v1/billing/statements
|
||||||
|
POST /v1/me/commerce/checkouts → POST /v1/me/billing/checkouts
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Field Terminology:** `payment_source` → `payment_method`
|
||||||
|
```typescript
|
||||||
|
// OLD (deprecated)
|
||||||
|
{ payment_source_id: "...", payment_source: {...} }
|
||||||
|
|
||||||
|
// NEW (required)
|
||||||
|
{ payment_method_id: "...", payment_method: {...} }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Removed Fields:** Plans responses no longer include:
|
||||||
|
- `amount`, `amount_formatted` (use `fee.amount` instead)
|
||||||
|
- `currency`, `currency_symbol` (use fee objects)
|
||||||
|
- `payer_type` (use `for_payer_type`)
|
||||||
|
- `annual_monthly_amount`, `annual_amount`
|
||||||
|
|
||||||
|
- **Removed Endpoints:**
|
||||||
|
- Invoices endpoint (use statements)
|
||||||
|
- Products endpoint
|
||||||
|
|
||||||
|
- **Null Handling:** Explicit rules - `null` means "doesn't exist", omitted means "not asserting existence"
|
||||||
|
|
||||||
|
**Migration:** Update SDK to v6.35.0+ which includes support for API version 2025-11-10.
|
||||||
|
|
||||||
|
**Official Guide:** https://clerk.com/docs/guides/development/upgrading/upgrade-guides/2025-11-10
|
||||||
|
|
||||||
|
### 2. Next.js v6 Async auth() (Oct 2024) - BREAKING CHANGE ⚠️
|
||||||
|
|
||||||
|
**Affects:** All Next.js Server Components using `auth()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ OLD (v5 - synchronous)
|
||||||
|
const { userId } = auth()
|
||||||
|
|
||||||
|
// ✅ NEW (v6 - asynchronous)
|
||||||
|
const { userId } = await auth()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also affects:** `auth.protect()` is now async in middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ OLD (v5)
|
||||||
|
auth.protect()
|
||||||
|
|
||||||
|
// ✅ NEW (v6)
|
||||||
|
await auth.protect()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Compatibility:** Next.js 15, 16 supported. Static rendering by default.
|
||||||
|
|
||||||
|
### 3. PKCE Support for Custom OAuth (Nov 12, 2025)
|
||||||
|
|
||||||
|
Custom OIDC providers and social connections now support PKCE (Proof Key for Code Exchange) for enhanced security in native/mobile applications where client secrets cannot be safely stored.
|
||||||
|
|
||||||
|
**Use case:** Mobile apps, native apps, public clients that can't securely store secrets.
|
||||||
|
|
||||||
|
### 4. Client Trust: Credential Stuffing Defense (Nov 14, 2025)
|
||||||
|
|
||||||
|
Automatic secondary authentication when users sign in from unrecognized devices:
|
||||||
|
- Activates for users with valid passwords but no 2FA
|
||||||
|
- No configuration required
|
||||||
|
- Included in all Clerk plans
|
||||||
|
|
||||||
|
**How it works:** Clerk automatically prompts for additional verification (email code, backup code) when detecting sign-in from new device.
|
||||||
|
|
||||||
|
### 5. Next.js 16 Support (Nov 2025)
|
||||||
|
|
||||||
|
**@clerk/nextjs v6.35.2+** includes cache invalidation improvements for Next.js 16 during sign-out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Patterns & Error Prevention
|
||||||
|
|
||||||
|
### Next.js v6: Async auth() Helper
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
```typescript
|
||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const { userId } = await auth() // ← Must await
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return <div>Unauthorized</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>User ID: {userId}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare Workers: authorizedParties (CSRF Prevention)
|
||||||
|
|
||||||
|
**CRITICAL:** Always set `authorizedParties` to prevent CSRF attacks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { verifyToken } from '@clerk/backend'
|
||||||
|
|
||||||
|
const { data, error } = await verifyToken(token, {
|
||||||
|
secretKey: c.env.CLERK_SECRET_KEY,
|
||||||
|
// REQUIRED: Prevent CSRF attacks
|
||||||
|
authorizedParties: ['https://yourdomain.com'],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Without `authorizedParties`, attackers can use valid tokens from other domains.
|
||||||
|
|
||||||
|
**Source:** https://clerk.com/docs/reference/backend/verify-token
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JWT Templates - Size Limits & Shortcodes
|
||||||
|
|
||||||
|
### JWT Size Limitation: 1.2KB for Custom Claims ⚠️
|
||||||
|
|
||||||
|
**Problem**: Browser cookies limited to 4KB. Clerk's default claims consume ~2.8KB, leaving **1.2KB for custom claims**.
|
||||||
|
|
||||||
|
**⚠️ Development Note**: When testing custom JWT claims in Vite dev mode, you may encounter **"431 Request Header Fields Too Large"** error. This is caused by Clerk's handshake token in the URL exceeding Vite's 8KB limit. See [Issue #11](#issue-11-431-request-header-fields-too-large-vite-dev-mode) for solution.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```json
|
||||||
|
// ✅ GOOD: Minimal claims
|
||||||
|
{
|
||||||
|
"user_id": "{{user.id}}",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ BAD: Exceeds limit
|
||||||
|
{
|
||||||
|
"bio": "{{user.public_metadata.bio}}", // 6KB field
|
||||||
|
"all_metadata": "{{user.public_metadata}}" // Entire object
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best Practice**: Store large data in database, include only identifiers/roles in JWT.
|
||||||
|
|
||||||
|
### Available Shortcodes Reference
|
||||||
|
|
||||||
|
| Category | Shortcodes | Example |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| **User ID & Name** | `{{user.id}}`, `{{user.first_name}}`, `{{user.last_name}}`, `{{user.full_name}}` | `"John Doe"` |
|
||||||
|
| **Contact** | `{{user.primary_email_address}}`, `{{user.primary_phone_address}}` | `"john@example.com"` |
|
||||||
|
| **Profile** | `{{user.image_url}}`, `{{user.username}}`, `{{user.created_at}}` | `"https://..."` |
|
||||||
|
| **Verification** | `{{user.email_verified}}`, `{{user.phone_number_verified}}` | `true` |
|
||||||
|
| **Metadata** | `{{user.public_metadata}}`, `{{user.public_metadata.FIELD}}` | `{"role": "admin"}` |
|
||||||
|
| **Organization** | `org_id`, `org_slug`, `org_role` (in sessionClaims) | `"org:admin"` |
|
||||||
|
|
||||||
|
**Advanced Features:**
|
||||||
|
- **String Interpolation**: `"{{user.last_name}} {{user.first_name}}"`
|
||||||
|
- **Conditional Fallbacks**: `"{{user.public_metadata.role || 'user'}}"`
|
||||||
|
- **Nested Metadata**: `"{{user.public_metadata.profile.interests}}"`
|
||||||
|
|
||||||
|
**Official Docs**: https://clerk.com/docs/guides/sessions/jwt-templates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing with Clerk
|
||||||
|
|
||||||
|
### Test Credentials (Fixed OTP: 424242)
|
||||||
|
|
||||||
|
**Test Emails** (no emails sent, fixed OTP):
|
||||||
|
```
|
||||||
|
john+clerk_test@example.com
|
||||||
|
jane+clerk_test@gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Phone Numbers** (no SMS sent, fixed OTP):
|
||||||
|
```
|
||||||
|
+12015550100
|
||||||
|
+19735550133
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixed OTP Code**: `424242` (works for all test credentials)
|
||||||
|
|
||||||
|
### Generate Session Tokens (60-second lifetime)
|
||||||
|
|
||||||
|
**Script** (`scripts/generate-session-token.js`):
|
||||||
|
```bash
|
||||||
|
# Generate token
|
||||||
|
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js
|
||||||
|
|
||||||
|
# Create new test user
|
||||||
|
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --create-user
|
||||||
|
|
||||||
|
# Auto-refresh token every 50 seconds
|
||||||
|
CLERK_SECRET_KEY=sk_test_... node scripts/generate-session-token.js --refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Flow**:
|
||||||
|
1. Create user: `POST /v1/users`
|
||||||
|
2. Create session: `POST /v1/sessions`
|
||||||
|
3. Generate token: `POST /v1/sessions/{session_id}/tokens`
|
||||||
|
4. Use in header: `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
### E2E Testing with Playwright
|
||||||
|
|
||||||
|
Install `@clerk/testing` for automatic Testing Token management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D @clerk/testing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Global Setup** (`global.setup.ts`):
|
||||||
|
```typescript
|
||||||
|
import { clerkSetup } from '@clerk/testing/playwright'
|
||||||
|
import { test as setup } from '@playwright/test'
|
||||||
|
|
||||||
|
setup('global setup', async ({}) => {
|
||||||
|
await clerkSetup()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test File** (`auth.spec.ts`):
|
||||||
|
```typescript
|
||||||
|
import { setupClerkTestingToken } from '@clerk/testing/playwright'
|
||||||
|
import { test } from '@playwright/test'
|
||||||
|
|
||||||
|
test('sign up', async ({ page }) => {
|
||||||
|
await setupClerkTestingToken({ page })
|
||||||
|
|
||||||
|
await page.goto('/sign-up')
|
||||||
|
await page.fill('input[name="emailAddress"]', 'test+clerk_test@example.com')
|
||||||
|
await page.fill('input[name="password"]', 'TestPassword123!')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// Verify with fixed OTP
|
||||||
|
await page.fill('input[name="code"]', '424242')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/dashboard')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Official Docs**: https://clerk.com/docs/guides/development/testing/overview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Issues Prevention
|
||||||
|
|
||||||
|
This skill prevents **11 documented issues**:
|
||||||
|
|
||||||
|
### Issue #1: Missing Clerk Secret Key
|
||||||
|
**Error**: "Missing Clerk Secret Key or API Key"
|
||||||
|
**Source**: https://stackoverflow.com/questions/77620604
|
||||||
|
**Prevention**: Always set in `.env.local` or via `wrangler secret put`
|
||||||
|
|
||||||
|
### Issue #2: API Key → Secret Key Migration
|
||||||
|
**Error**: "apiKey is deprecated, use secretKey"
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
**Prevention**: Replace `apiKey` with `secretKey` in all calls
|
||||||
|
|
||||||
|
### Issue #3: JWKS Cache Race Condition
|
||||||
|
**Error**: "No JWK available"
|
||||||
|
**Source**: https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md
|
||||||
|
**Prevention**: Use @clerk/backend@2.17.2 or later (fixed)
|
||||||
|
|
||||||
|
### Issue #4: Missing authorizedParties (CSRF)
|
||||||
|
**Error**: No error, but CSRF vulnerability
|
||||||
|
**Source**: https://clerk.com/docs/reference/backend/verify-token
|
||||||
|
**Prevention**: Always set `authorizedParties: ['https://yourdomain.com']`
|
||||||
|
|
||||||
|
### Issue #5: Import Path Changes (Core 2)
|
||||||
|
**Error**: "Cannot find module"
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
**Prevention**: Update import paths for Core 2
|
||||||
|
|
||||||
|
### Issue #6: JWT Size Limit Exceeded
|
||||||
|
**Error**: Token exceeds size limit
|
||||||
|
**Source**: https://clerk.com/docs/backend-requests/making/custom-session-token
|
||||||
|
**Prevention**: Keep custom claims under 1.2KB
|
||||||
|
|
||||||
|
### Issue #7: Deprecated API Version v1
|
||||||
|
**Error**: "API version v1 is deprecated"
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
**Prevention**: Use latest SDK versions (API v2025-11-10)
|
||||||
|
|
||||||
|
### Issue #8: ClerkProvider JSX Component Error
|
||||||
|
**Error**: "cannot be used as a JSX component"
|
||||||
|
**Source**: https://stackoverflow.com/questions/79265537
|
||||||
|
**Prevention**: Ensure React 19 compatibility with @clerk/clerk-react@5.56.2+
|
||||||
|
|
||||||
|
### Issue #9: Async auth() Helper Confusion
|
||||||
|
**Error**: "auth() is not a function"
|
||||||
|
**Source**: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
|
||||||
|
**Prevention**: Always await: `const { userId } = await auth()`
|
||||||
|
|
||||||
|
### Issue #10: Environment Variable Misconfiguration
|
||||||
|
**Error**: "Missing Publishable Key" or secret leaked
|
||||||
|
**Prevention**: Use correct prefixes (`NEXT_PUBLIC_`, `VITE_`), never commit secrets
|
||||||
|
|
||||||
|
### Issue #11: 431 Request Header Fields Too Large (Vite Dev Mode)
|
||||||
|
**Error**: "431 Request Header Fields Too Large" when signing in
|
||||||
|
**Source**: Common in Vite dev mode when testing custom JWT claims
|
||||||
|
**Cause**: Clerk's `__clerk_handshake` token in URL exceeds Vite's 8KB header limit
|
||||||
|
**Prevention**:
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Temporary Workaround**: Clear browser cache, sign out, sign back in
|
||||||
|
|
||||||
|
**Why**: Clerk dev tokens are larger than production; custom JWT claims increase handshake token size
|
||||||
|
|
||||||
|
**Note**: This is different from Issue #6 (session token size). Issue #6 is about cookies (1.2KB), this is about URL parameters in dev mode (8KB → 32KB).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
|
||||||
|
- **Clerk Docs**: https://clerk.com/docs
|
||||||
|
- **Next.js Guide**: https://clerk.com/docs/references/nextjs/overview
|
||||||
|
- **React Guide**: https://clerk.com/docs/references/react/overview
|
||||||
|
- **Backend SDK**: https://clerk.com/docs/reference/backend/overview
|
||||||
|
- **JWT Templates**: https://clerk.com/docs/guides/sessions/jwt-templates
|
||||||
|
- **API Version 2025-11-10 Upgrade**: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/2025-11-10
|
||||||
|
- **Testing Guide**: https://clerk.com/docs/guides/development/testing/overview
|
||||||
|
- **Context7 Library ID**: `/clerk/clerk-docs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package Versions
|
||||||
|
|
||||||
|
**Latest (Nov 22, 2025):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/nextjs": "^6.35.4",
|
||||||
|
"@clerk/clerk-react": "^5.56.2",
|
||||||
|
"@clerk/backend": "^2.23.2",
|
||||||
|
"@clerk/testing": "^1.13.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Token Efficiency**:
|
||||||
|
- **Without skill**: ~5,200 tokens (setup tutorials, JWT templates, testing setup)
|
||||||
|
- **With skill**: ~2,500 tokens (breaking changes + critical patterns + error prevention)
|
||||||
|
- **Savings**: ~52% (~2,700 tokens)
|
||||||
|
|
||||||
|
**Errors prevented**: 11 documented issues with exact solutions
|
||||||
|
**Key value**: API 2025-11-10 breaking changes, Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense, JWT size limits, 431 header error workaround
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last verified**: 2025-11-22 | **Skill version**: 2.0.0 | **Changes**: Added API version 2025-11-10 breaking changes (billing endpoints), PKCE support, Client Trust defense, Next.js 16 support. Removed tutorials (~480 lines). Updated SDK versions. Focused on breaking changes + error prevention + critical patterns.
|
||||||
14
assets/example-template.txt
Normal file
14
assets/example-template.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[TODO: Example Template File]
|
||||||
|
|
||||||
|
[TODO: This directory contains files that will be used in the OUTPUT that Claude produces.]
|
||||||
|
|
||||||
|
[TODO: Examples:]
|
||||||
|
- Templates (.html, .tsx, .md)
|
||||||
|
- Images (.png, .svg)
|
||||||
|
- Fonts (.ttf, .woff)
|
||||||
|
- Boilerplate code
|
||||||
|
- Configuration file templates
|
||||||
|
|
||||||
|
[TODO: Delete this file and add your actual assets]
|
||||||
|
|
||||||
|
These files are NOT loaded into context. They are copied or used directly in the final output.
|
||||||
137
plugin.lock.json
Normal file
137
plugin.lock.json
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:jezweb/claude-skills:skills/clerk-auth",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "9fce0ffec647b8d8c61c76bd4fca8d99d2baff35",
|
||||||
|
"treeHash": "f72a84de09ac89d9f601331c56be2456523750f8902e426b3e15417d871693f4",
|
||||||
|
"generatedAt": "2025-11-28T10:19:03.105129Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "clerk-auth",
|
||||||
|
"description": "Add Clerk authentication to React, Next.js, and Cloudflare Workers. Features: JWT templates, protected routes, shadcn/ui integration, E2E testing support. Use when: setting up auth, configuring custom JWT claims/middleware, or troubleshooting Missing Clerk Secret Key, JWKS errors, Core 2 migration, authorizedParties issues.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "6b38d36c1223919554d05e8990a2a555011915ea6e07f880993a1b652336dec4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "777b54890367198bc182e4c08e1b7dec0ee9b19e8e6c788ae90d5febbe46a354"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/example-reference.md",
|
||||||
|
"sha256": "77c788d727d05d6479a61d6652b132e43882ffc67c145bb46ba880567d83f7f8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/jwt-claims-guide.md",
|
||||||
|
"sha256": "e62be80eb192068532466ee545b4230090983eaaea42bb2def0ca94b7caab724"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/common-errors.md",
|
||||||
|
"sha256": "026bbaefa69b87ad3cd9099d33e5d8403d8e39aa6164f33d9ef018b4483614c0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/testing-guide.md",
|
||||||
|
"sha256": "01d0a6216c98fc371218eb25399cc6c1402b36f8c187dcb10229062dcc5b5897"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/example-script.sh",
|
||||||
|
"sha256": "83d2b09d044811608e17cbd8e66d993b1e9998c7bd3379a42ab81fbdba973e0e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/generate-session-token.js",
|
||||||
|
"sha256": "535155e99263a2e8d4417409a1092ebf2af12fbb1ff8cb082e4f0e42db420ecc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "7374c496926cea0a8057b7723a2f5ad8c88e03e71616c8312d34f83b43c265e0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/jwt/supabase-template.json",
|
||||||
|
"sha256": "a3b554eb7dcb51759bdf9beca6db74bf8d4511d5750fb598ca8eda3a537b5d16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/jwt/grafbase-template.json",
|
||||||
|
"sha256": "4b3a9d3f55e01c4400313caaf05360873221e32c2e077a96c31c1c79139526ac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/jwt/basic-template.json",
|
||||||
|
"sha256": "333acef2a560adede6151e6a9afe0f4f84fefa801211a09d23f9ddd9c6ce2937"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/jwt/advanced-template.json",
|
||||||
|
"sha256": "f81ed4716ca32f2a105563e4bfff35967d35706efd40aaf0b641e446e6f8e49a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/typescript/custom-jwt-types.d.ts",
|
||||||
|
"sha256": "c1bd761f82dc93ccfef8dac718df7113f0027839e0bcc53df575db9c49acdb3c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/cloudflare/wrangler.jsonc",
|
||||||
|
"sha256": "0361caad0f82846078b605ddb6e3b88eb56ab854109b0e4dbb1fbbd84341d7b3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/cloudflare/worker-auth.ts",
|
||||||
|
"sha256": "6abc3321967c0bed597204eef3fc36ef7caefdec8e8a26b237fd7717e9e2037b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/vite/package.json",
|
||||||
|
"sha256": "b4b496d3c457af1247768c4a6c725b01e2c2f82ac7648bbf24d69911e60e4d00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/nextjs/middleware.ts",
|
||||||
|
"sha256": "866f24cef11c0dcd59fcdd12f429fe54b3ec77626e7b7705726776362e15daea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/nextjs/server-component-example.tsx",
|
||||||
|
"sha256": "235dc29fb7fceb29621dced8671467ad5b7a0dc2012f704c2ef73a18b574ffb5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/nextjs/app-layout.tsx",
|
||||||
|
"sha256": "efdeba1d20c15771a7e15974bfbb418708dc80d163f41f4946b49bb41423a9ec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/env-examples/.dev.vars.example",
|
||||||
|
"sha256": "1e3d178480a4c771fbbde5e87e33bfc6b4779a3a319fad4c45c7552a3140b8b0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/env-examples/.env.local.example",
|
||||||
|
"sha256": "fc0f7ec352213a16c72973d5462aa35a41d1c78f5c9108974fba5bec82947475"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/env-examples/.env.local.vite.example",
|
||||||
|
"sha256": "077bf978ef4d088934e1bbf7811dad25e1bd80d94c319d452ff5c522e790bb9c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/react/App.tsx",
|
||||||
|
"sha256": "b7cf39eb6d8381348442f360f32ed01cf75b992ac9816413b36f7d88bae3618f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/react/main.tsx",
|
||||||
|
"sha256": "3184f8908c096402d5519497ceb31c6d0e157741429c2bc830a9c7aaf518d02f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "assets/example-template.txt",
|
||||||
|
"sha256": "3f725c80d70847fd8272bf1400515ba753f12f98f3b294d09e50b54b4c1b024a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "f72a84de09ac89d9f601331c56be2456523750f8902e426b3e15417d871693f4"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
642
references/common-errors.md
Normal file
642
references/common-errors.md
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
# Clerk Authentication - Common Errors & Solutions
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-22
|
||||||
|
|
||||||
|
This document provides detailed troubleshooting for all 10 documented Clerk authentication issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #1: Missing Clerk Secret Key
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: Missing Clerk Secret Key or API Key
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- \`CLERK_SECRET_KEY\` environment variable not set
|
||||||
|
- Environment file not loaded
|
||||||
|
- Wrong environment file name
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Next.js**:
|
||||||
|
1. Create \`.env.local\` in project root
|
||||||
|
2. Add: \`CLERK_SECRET_KEY=sk_test_...\`
|
||||||
|
3. Restart dev server
|
||||||
|
|
||||||
|
**Cloudflare Workers**:
|
||||||
|
1. Local: Create \`.dev.vars\` with \`CLERK_SECRET_KEY=sk_test_...\`
|
||||||
|
2. Production: Run \`wrangler secret put CLERK_SECRET_KEY\`
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Always set secret key before running app
|
||||||
|
- Verify with: \`echo $CLERK_SECRET_KEY\` (should be empty - it's private!)
|
||||||
|
- Check environment is loading: add \`console.log(!!process.env.CLERK_SECRET_KEY)\`
|
||||||
|
|
||||||
|
**Source**: https://stackoverflow.com/questions/77620604
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #2: API Key → Secret Key Migration (Core 2)
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Warning: apiKey is deprecated, use secretKey instead
|
||||||
|
TypeError: Cannot read properties of undefined
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Upgrading from @clerk/backend v1 to v2 (Core 2)
|
||||||
|
- Old code uses \`apiKey\` parameter
|
||||||
|
- Breaking change in Core 2
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Before (v1)**:
|
||||||
|
\`\`\`typescript
|
||||||
|
const clerk = createClerkClient({
|
||||||
|
apiKey: process.env.CLERK_API_KEY // ❌ Deprecated
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**After (v2)**:
|
||||||
|
\`\`\`typescript
|
||||||
|
const clerk = createClerkClient({
|
||||||
|
secretKey: process.env.CLERK_SECRET_KEY // ✅ Correct
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Use \`secretKey\` in all new code
|
||||||
|
- Search codebase for \`apiKey:\` and replace with \`secretKey:\`
|
||||||
|
- Update environment variable name from \`CLERK_API_KEY\` to \`CLERK_SECRET_KEY\`
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #3: JWKS Cache Race Condition
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: No JWK available
|
||||||
|
Token verification fails intermittently
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Race condition in older @clerk/backend versions
|
||||||
|
- JWKS cache not populated before verification
|
||||||
|
- Fixed in recent versions
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Update Package**:
|
||||||
|
\`\`\`bash
|
||||||
|
npm install @clerk/backend@latest
|
||||||
|
# Ensure version is 2.17.2 or later
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Verify Version**:
|
||||||
|
\`\`\`bash
|
||||||
|
npm list @clerk/backend
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Always use latest stable @clerk/backend version
|
||||||
|
- This issue is fixed in modern versions
|
||||||
|
|
||||||
|
**Source**: https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #4: Missing authorizedParties (CSRF Vulnerability)
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
- No error, but security vulnerability
|
||||||
|
- Tokens from other domains accepted
|
||||||
|
- CSRF attacks possible
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- \`authorizedParties\` not set in \`verifyToken()\`
|
||||||
|
- Clerk accepts tokens from any domain by default
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Always Set authorizedParties**:
|
||||||
|
\`\`\`typescript
|
||||||
|
const { data, error } = await verifyToken(token, {
|
||||||
|
secretKey: env.CLERK_SECRET_KEY,
|
||||||
|
authorizedParties: [
|
||||||
|
'http://localhost:5173', // Development
|
||||||
|
'https://yourdomain.com', // Production
|
||||||
|
], // ✅ Required for security
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Always set \`authorizedParties\` in production
|
||||||
|
- List all domains that should be able to make requests
|
||||||
|
- Never use wildcard or leave empty
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/docs/reference/backend/verify-token
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #5: Import Path Changes (Core 2 Upgrade)
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: Cannot find module '@clerk/backend'
|
||||||
|
Module not found: Can't resolve '@clerk/backend/errors'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Import paths changed in Core 2
|
||||||
|
- Old imports don't work in new version
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Update Imports**:
|
||||||
|
\`\`\`typescript
|
||||||
|
// Before (v1)
|
||||||
|
import { TokenVerificationError } from '@clerk/backend'
|
||||||
|
|
||||||
|
// After (v2)
|
||||||
|
import { TokenVerificationError } from '@clerk/backend/errors'
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Common Path Changes
|
||||||
|
| Old Path | New Path |
|
||||||
|
|----------|----------|
|
||||||
|
| \`@clerk/backend\` (errors) | \`@clerk/backend/errors\` |
|
||||||
|
| \`@clerk/backend\` (types) | \`@clerk/backend/types\` |
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Follow official migration guide
|
||||||
|
- Update all imports when upgrading
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #6: JWT Size Limit Exceeded
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: Token exceeds maximum size
|
||||||
|
Session token too large
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Custom JWT claims exceed 1.2KB limit
|
||||||
|
- Too much data in \`publicMetadata\`
|
||||||
|
- Clerk's default claims + your claims > 1.2KB
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Reduce Custom Claims**:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role}}"
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Store Large Data in Database**:
|
||||||
|
- Use JWT for minimal claims (user ID, role)
|
||||||
|
- Store profile data, preferences in your database
|
||||||
|
- Fetch after authentication
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Keep custom claims minimal
|
||||||
|
- Use database for large/complex data
|
||||||
|
- Monitor token size during development
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/docs/backend-requests/making/custom-session-token
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #7: Deprecated API Version v1
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Warning: API version v1 is deprecated
|
||||||
|
Please upgrade to API version 2025-04-10
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Using old SDK version
|
||||||
|
- API version 1 deprecated April 2025
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Update SDKs**:
|
||||||
|
\`\`\`bash
|
||||||
|
npm install @clerk/nextjs@latest
|
||||||
|
npm install @clerk/backend@latest
|
||||||
|
npm install @clerk/clerk-react@latest
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Verify Versions**:
|
||||||
|
- @clerk/nextjs: 6.0.0+
|
||||||
|
- @clerk/backend: 2.0.0+
|
||||||
|
- @clerk/clerk-react: 5.0.0+
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Keep SDKs updated
|
||||||
|
- Use latest stable versions
|
||||||
|
- Follow deprecation warnings
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/docs/upgrade-guides/core-2/backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #8: ClerkProvider JSX Component Error
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: 'ClerkProvider' cannot be used as a JSX component
|
||||||
|
Its element type 'ReactElement<any, any> | Component<...>' is not a valid JSX element
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- React version mismatch
|
||||||
|
- @clerk/clerk-react not compatible with React version
|
||||||
|
- TypeScript type conflicts
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Update Packages**:
|
||||||
|
\`\`\`bash
|
||||||
|
npm install @clerk/clerk-react@latest react@latest react-dom@latest
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Ensure Compatibility**:
|
||||||
|
- React 19: Use @clerk/clerk-react@5.51.0+
|
||||||
|
- React 18: Use @clerk/clerk-react@5.0.0+
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Keep React and Clerk packages in sync
|
||||||
|
- Use compatible versions
|
||||||
|
|
||||||
|
**Source**: https://stackoverflow.com/questions/79265537
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #9: Async auth() Helper Confusion
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: auth() is not a function
|
||||||
|
TypeError: Cannot read properties of undefined
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- @clerk/nextjs v6 made \`auth()\` async (breaking change)
|
||||||
|
- Old code doesn't await \`auth()\`
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Before (v5)**:
|
||||||
|
\`\`\`typescript
|
||||||
|
const { userId } = auth() // ❌ Sync in v5
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**After (v6)**:
|
||||||
|
\`\`\`typescript
|
||||||
|
const { userId } = await auth() // ✅ Async in v6
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### All Affected Code
|
||||||
|
- \`const { userId } = await auth()\`
|
||||||
|
- \`const user = await currentUser()\`
|
||||||
|
- \`await auth.protect()\` (in middleware)
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Always await auth-related functions in v6+
|
||||||
|
- Update all instances when upgrading
|
||||||
|
|
||||||
|
**Source**: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #10: Environment Variable Misconfiguration
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
\`\`\`
|
||||||
|
Error: Missing Publishable Key
|
||||||
|
Warning: Secret key exposed to client
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Wrong environment variable prefix
|
||||||
|
- Secrets committed to version control
|
||||||
|
- Environment file not loaded
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Next.js Prefix Rules**:
|
||||||
|
- Client variables: \`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY\`
|
||||||
|
- Server variables: \`CLERK_SECRET_KEY\` (no prefix!)
|
||||||
|
|
||||||
|
**Vite Prefix Rules**:
|
||||||
|
- Client variables: \`VITE_CLERK_PUBLISHABLE_KEY\`
|
||||||
|
|
||||||
|
**Cloudflare Workers**:
|
||||||
|
- Local: \`.dev.vars\` file
|
||||||
|
- Production: \`wrangler secret put CLERK_SECRET_KEY\`
|
||||||
|
|
||||||
|
### Prevention Checklist
|
||||||
|
- [ ] Use correct prefix for framework
|
||||||
|
- [ ] Never use \`NEXT_PUBLIC_\` or \`VITE_\` for secrets
|
||||||
|
- [ ] Add \`.env.local\` and \`.dev.vars\` to \`.gitignore\`
|
||||||
|
- [ ] Use different keys for dev and production
|
||||||
|
- [ ] Restart dev server after changing env vars
|
||||||
|
|
||||||
|
**Source**: General best practices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #11: 431 Request Header Fields Too Large (Vite Dev Mode)
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
```
|
||||||
|
431 Request Header Fields Too Large
|
||||||
|
Failed to load resource: the server responded with a status of 431 ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Happens when:
|
||||||
|
- Signing in to your Vite app in development mode
|
||||||
|
- After adding custom JWT claims
|
||||||
|
- URL contains very long `__clerk_handshake=...` parameter
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Clerk's authentication handshake passes a JWT token in the URL as `__clerk_handshake=...`
|
||||||
|
- Vite dev server (Node.js http) has default 8KB request header limit
|
||||||
|
- Clerk development tokens are larger than production tokens
|
||||||
|
- Custom JWT claims increase handshake token size
|
||||||
|
- Long URLs with JWT tokens exceed Vite's header limit
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
**Solution 1: Increase Node.js Header Limit (Recommended)**
|
||||||
|
|
||||||
|
Update `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
|
||||||
|
"build": "vite build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This increases the limit from 8KB to 32KB.
|
||||||
|
|
||||||
|
**For Windows PowerShell**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_OPTIONS=--max-http-header-size=32768 vite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `cross-env`: `npm install -D cross-env`
|
||||||
|
|
||||||
|
**Solution 2: Temporary Workaround**
|
||||||
|
|
||||||
|
Clear browser state:
|
||||||
|
1. Open DevTools (F12)
|
||||||
|
2. Right-click refresh button → "Empty Cache and Hard Reload"
|
||||||
|
3. Or: Application tab → Clear all storage
|
||||||
|
4. Sign out and sign back in
|
||||||
|
|
||||||
|
This removes the problematic handshake token and forces Clerk to create a fresh one.
|
||||||
|
|
||||||
|
### What DOESN'T Work
|
||||||
|
|
||||||
|
❌ **This won't fix it**:
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache', // Wrong - this sets RESPONSE headers
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The error is about REQUEST headers (incoming), not RESPONSE headers (outgoing).
|
||||||
|
|
||||||
|
### Prevention
|
||||||
|
- Expect this error when testing custom JWT claims in dev mode
|
||||||
|
- Set `NODE_OPTIONS` in `package.json` from the start
|
||||||
|
- Keep custom claims minimal during development
|
||||||
|
- Production deployments (Cloudflare Workers, Vercel) don't have this issue
|
||||||
|
|
||||||
|
### Difference from Error #6
|
||||||
|
|
||||||
|
**Error #6** (JWT Size Limit Exceeded):
|
||||||
|
- About session token size in cookies
|
||||||
|
- 1.2KB limit for custom claims
|
||||||
|
- Affects production
|
||||||
|
- Browser cookie size limit
|
||||||
|
|
||||||
|
**Error #11** (431 Header Error):
|
||||||
|
- About handshake token size in URL parameters
|
||||||
|
- 8KB default limit (increase to 32KB)
|
||||||
|
- Only affects Vite dev mode
|
||||||
|
- Node.js HTTP server limit
|
||||||
|
|
||||||
|
### Why Production Isn't Affected
|
||||||
|
- Production builds don't use Vite dev server
|
||||||
|
- Cloudflare Workers, Vercel, Netlify have higher header limits
|
||||||
|
- Production tokens are smaller (no dev overhead)
|
||||||
|
- Handshake flow is optimized in production
|
||||||
|
|
||||||
|
**Source**: Real-world developer experience with Vite + Clerk + Custom JWT claims
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Debugging Checklist
|
||||||
|
|
||||||
|
When auth isn't working:
|
||||||
|
|
||||||
|
1. **Check Environment Variables**
|
||||||
|
- [ ] \`CLERK_SECRET_KEY\` set?
|
||||||
|
- [ ] \`CLERK_PUBLISHABLE_KEY\` or \`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY\` set?
|
||||||
|
- [ ] Correct prefix for framework?
|
||||||
|
- [ ] Dev server restarted after changes?
|
||||||
|
|
||||||
|
2. **Check Package Versions**
|
||||||
|
- [ ] Using latest stable versions?
|
||||||
|
- [ ] React version compatible with Clerk?
|
||||||
|
- [ ] All Clerk packages same major version?
|
||||||
|
|
||||||
|
3. **Check Code Patterns**
|
||||||
|
- [ ] \`auth()\` being awaited? (v6+)
|
||||||
|
- [ ] \`authorizedParties\` set in \`verifyToken()\`?
|
||||||
|
- [ ] \`isLoaded\` checked before rendering?
|
||||||
|
- [ ] Using \`secretKey\` not \`apiKey\`?
|
||||||
|
|
||||||
|
4. **Check Network**
|
||||||
|
- [ ] Token in \`Authorization: Bearer <token>\` header?
|
||||||
|
- [ ] CORS configured if API is different domain?
|
||||||
|
- [ ] Clerk Dashboard shows API requests?
|
||||||
|
|
||||||
|
5. **Check Clerk Dashboard**
|
||||||
|
- [ ] Application configured correctly?
|
||||||
|
- [ ] Development/production keys match environment?
|
||||||
|
- [ ] Custom JWT template valid?
|
||||||
|
- [ ] Webhooks configured if using?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Still Having Issues?**
|
||||||
|
|
||||||
|
1. Check official Clerk docs: https://clerk.com/docs
|
||||||
|
2. Search Clerk Discord: https://clerk.com/discord
|
||||||
|
3. File GitHub issue: https://github.com/clerk/javascript/issues
|
||||||
|
4. Check Clerk status: https://status.clerk.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #12: Deprecated Redirect URL Props
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
```
|
||||||
|
Clerk: The prop "afterSignInUrl" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.
|
||||||
|
Clerk: The prop "afterSignUpUrl" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
Appears in:
|
||||||
|
- Browser console
|
||||||
|
- Development mode
|
||||||
|
- When using `<SignIn>` or `<SignUp>` components from `@clerk/clerk-react`
|
||||||
|
|
||||||
|
### Why It Happens
|
||||||
|
- Clerk updated redirect prop naming in v5.x for clarity
|
||||||
|
- Old props: `afterSignInUrl`, `afterSignUpUrl`
|
||||||
|
- New props: `fallbackRedirectUrl`, `forceRedirectUrl`
|
||||||
|
- Old props still work but trigger deprecation warnings
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
**Replace deprecated props:**
|
||||||
|
|
||||||
|
❌ **Old (Deprecated)**:
|
||||||
|
```tsx
|
||||||
|
<SignIn
|
||||||
|
afterSignInUrl="/"
|
||||||
|
signUpUrl="/signup"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SignUp
|
||||||
|
afterSignUpUrl="/"
|
||||||
|
signInUrl="/login"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **New (Recommended)**:
|
||||||
|
```tsx
|
||||||
|
<SignIn
|
||||||
|
fallbackRedirectUrl="/"
|
||||||
|
signUpUrl="/signup"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SignUp
|
||||||
|
fallbackRedirectUrl="/"
|
||||||
|
signInUrl="/login"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Choosing Between fallbackRedirectUrl and forceRedirectUrl
|
||||||
|
|
||||||
|
**Use `fallbackRedirectUrl`** (Most Common):
|
||||||
|
- Used as a fallback if no redirect URL is in query params
|
||||||
|
- Allows Clerk to use `redirect_url` from query string first
|
||||||
|
- More flexible for return-to-previous-page flows
|
||||||
|
- **Recommended for most use cases**
|
||||||
|
|
||||||
|
**Use `forceRedirectUrl`** (Rare):
|
||||||
|
- Always redirects here, ignoring query params
|
||||||
|
- Used when you want strict control
|
||||||
|
- Overrides any `redirect_url` in query string
|
||||||
|
- Use only when you need to force a specific destination
|
||||||
|
|
||||||
|
### Example: Complete Migration
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// LoginPage.tsx
|
||||||
|
import { SignIn } from '@clerk/clerk-react';
|
||||||
|
|
||||||
|
export function LoginPage() {
|
||||||
|
return (
|
||||||
|
<SignIn
|
||||||
|
routing="path"
|
||||||
|
path="/login"
|
||||||
|
signUpUrl="/signup"
|
||||||
|
fallbackRedirectUrl="/" // ✅ Use this instead of afterSignInUrl
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
rootBox: 'mx-auto',
|
||||||
|
card: 'shadow-lg',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// SignupPage.tsx
|
||||||
|
import { SignUp } from '@clerk/clerk-react';
|
||||||
|
|
||||||
|
export function SignupPage() {
|
||||||
|
return (
|
||||||
|
<SignUp
|
||||||
|
routing="path"
|
||||||
|
path="/signup"
|
||||||
|
signInUrl="/login"
|
||||||
|
fallbackRedirectUrl="/" // ✅ Use this instead of afterSignUpUrl
|
||||||
|
appearance={{
|
||||||
|
elements: {
|
||||||
|
rootBox: 'mx-auto',
|
||||||
|
card: 'shadow-lg',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Search codebase for `afterSignInUrl`
|
||||||
|
- [ ] Replace with `fallbackRedirectUrl`
|
||||||
|
- [ ] Search codebase for `afterSignUpUrl`
|
||||||
|
- [ ] Replace with `fallbackRedirectUrl`
|
||||||
|
- [ ] Test redirect flow after sign in
|
||||||
|
- [ ] Test redirect flow after sign up
|
||||||
|
- [ ] Verify no console warnings
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
|
||||||
|
**Clarity**: The new naming makes it clear these are fallback/default redirects
|
||||||
|
**Future-Proofing**: Old props may be removed in future major versions
|
||||||
|
**Best Practices**: Using current APIs ensures compatibility with new features
|
||||||
|
|
||||||
|
**Source**: Clerk v5.x Changelog & Official Migration Guide
|
||||||
|
**Reference**: https://clerk.com/docs/guides/custom-redirects#redirect-url-props
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Error #12** (Deprecated Redirect Props):
|
||||||
|
- About redirect URL prop naming
|
||||||
|
- Simple find-replace fix
|
||||||
|
- Use `fallbackRedirectUrl` for most cases
|
||||||
|
- Only affects React-based projects
|
||||||
|
|
||||||
26
references/example-reference.md
Normal file
26
references/example-reference.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# [TODO: Reference Document Name]
|
||||||
|
|
||||||
|
[TODO: This file contains reference documentation that Claude can load when needed.]
|
||||||
|
|
||||||
|
[TODO: Delete this file if you don't have reference documentation to provide.]
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
[TODO: Explain what information this document contains]
|
||||||
|
|
||||||
|
## When Claude Should Use This
|
||||||
|
|
||||||
|
[TODO: Describe specific scenarios where Claude should load this reference]
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
[TODO: Add your reference content here - schemas, guides, specifications, etc.]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This file is NOT loaded into context by default. Claude will only load it when:
|
||||||
|
- It determines the information is needed
|
||||||
|
- You explicitly ask Claude to reference it
|
||||||
|
- The SKILL.md instructions direct Claude to read it
|
||||||
|
|
||||||
|
Keep this file under 10k words for best performance.
|
||||||
660
references/jwt-claims-guide.md
Normal file
660
references/jwt-claims-guide.md
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
# Clerk JWT Claims: Complete Reference
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-22
|
||||||
|
**Clerk API Version**: 2025-04-10
|
||||||
|
**Status**: Production Ready ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide provides comprehensive documentation for all JWT claims available in Clerk, including default claims, user property shortcodes, organization claims, metadata access patterns, and advanced template features.
|
||||||
|
|
||||||
|
**Use this guide when**:
|
||||||
|
- Creating custom JWT templates in Clerk Dashboard
|
||||||
|
- Integrating with third-party services requiring specific token formats
|
||||||
|
- Implementing role-based access control (RBAC)
|
||||||
|
- Building multi-tenant applications
|
||||||
|
- Accessing user data in backend services
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Default Claims (Auto-Included)](#default-claims-auto-included)
|
||||||
|
2. [User Property Shortcodes](#user-property-shortcodes)
|
||||||
|
3. [Organization Claims](#organization-claims)
|
||||||
|
4. [Metadata Access](#metadata-access)
|
||||||
|
5. [Advanced Template Features](#advanced-template-features)
|
||||||
|
6. [Creating JWT Templates](#creating-jwt-templates)
|
||||||
|
7. [TypeScript Type Safety](#typescript-type-safety)
|
||||||
|
8. [Common Use Cases](#common-use-cases)
|
||||||
|
9. [Limitations & Gotchas](#limitations--gotchas)
|
||||||
|
10. [Official Documentation](#official-documentation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default Claims (Auto-Included)
|
||||||
|
|
||||||
|
Every JWT generated by Clerk automatically includes these claims. **These cannot be overridden in custom templates**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"azp": "http://localhost:3000", // Authorized party (your app URL)
|
||||||
|
"exp": 1639398300, // Expiration time (Unix timestamp)
|
||||||
|
"iat": 1639398272, // Issued at (Unix timestamp)
|
||||||
|
"iss": "https://your-app.clerk.accounts.dev", // Issuer (Clerk instance URL)
|
||||||
|
"jti": "10db7f531a90cb2faea4", // JWT ID (unique identifier)
|
||||||
|
"nbf": 1639398220, // Not before (Unix timestamp)
|
||||||
|
"sub": "user_1deJLArSTiWiF1YdsEWysnhJLLY" // Subject (user ID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claim Descriptions
|
||||||
|
|
||||||
|
| Claim | Name | Description | Can Override? |
|
||||||
|
|-------|------|-------------|---------------|
|
||||||
|
| `azp` | Authorized Party | The URL of your application | ❌ No |
|
||||||
|
| `exp` | Expiration Time | When the token expires (Unix timestamp) | ❌ No |
|
||||||
|
| `iat` | Issued At | When the token was created (Unix timestamp) | ❌ No |
|
||||||
|
| `iss` | Issuer | Your Clerk instance URL | ❌ No |
|
||||||
|
| `jti` | JWT ID | Unique identifier for this token | ❌ No |
|
||||||
|
| `nbf` | Not Before | Token not valid before this time (Unix timestamp) | ❌ No |
|
||||||
|
| `sub` | Subject | User ID (same as `userId` in auth objects) | ❌ No |
|
||||||
|
|
||||||
|
**IMPORTANT**: These claims consume approximately **200-300 bytes** of the 4KB cookie limit, leaving ~1.2KB for custom claims.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Property Shortcodes
|
||||||
|
|
||||||
|
Shortcodes are placeholder strings that Clerk replaces with actual user data during token generation. Use double curly braces: `{{shortcode}}`.
|
||||||
|
|
||||||
|
### Basic User Properties
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "{{user.id}}", // User's unique ID
|
||||||
|
"first_name": "{{user.first_name}}", // User's first name (or null)
|
||||||
|
"last_name": "{{user.last_name}}", // User's last name (or null)
|
||||||
|
"full_name": "{{user.full_name}}", // User's full name (or null)
|
||||||
|
"email": "{{user.primary_email_address}}", // Primary email address
|
||||||
|
"phone": "{{user.primary_phone_address}}", // Primary phone number (or null)
|
||||||
|
"avatar": "{{user.image_url}}", // User's profile image URL
|
||||||
|
"created_at": "{{user.created_at}}", // Account creation timestamp
|
||||||
|
"username": "{{user.username}}", // Username (if enabled)
|
||||||
|
"email_verified": "{{user.email_verified}}", // Boolean: email verified?
|
||||||
|
"phone_verified": "{{user.phone_number_verified}}" // Boolean: phone verified?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification Status
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"has_verified_email": "{{user.email_verified}}",
|
||||||
|
"has_verified_phone": "{{user.phone_number_verified}}",
|
||||||
|
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete User Shortcode Reference
|
||||||
|
|
||||||
|
| Shortcode | Type | Description | Example Value |
|
||||||
|
|-----------|------|-------------|---------------|
|
||||||
|
| `{{user.id}}` | string | User's unique identifier | `"user_2abc..."` |
|
||||||
|
| `{{user.first_name}}` | string \| null | First name | `"John"` |
|
||||||
|
| `{{user.last_name}}` | string \| null | Last name | `"Doe"` |
|
||||||
|
| `{{user.full_name}}` | string \| null | Full name (computed) | `"John Doe"` |
|
||||||
|
| `{{user.primary_email_address}}` | string | Primary email | `"john@example.com"` |
|
||||||
|
| `{{user.primary_phone_address}}` | string \| null | Primary phone | `"+1234567890"` |
|
||||||
|
| `{{user.image_url}}` | string | Profile image URL | `"https://..."` |
|
||||||
|
| `{{user.created_at}}` | number | Unix timestamp | `1639398272` |
|
||||||
|
| `{{user.username}}` | string \| null | Username (if enabled) | `"johndoe"` |
|
||||||
|
| `{{user.email_verified}}` | boolean | Email verified? | `true` |
|
||||||
|
| `{{user.phone_number_verified}}` | boolean | Phone verified? | `false` |
|
||||||
|
| `{{user.public_metadata}}` | object | All public metadata | `{...}` |
|
||||||
|
| `{{user.unsafe_metadata}}` | object | All unsafe metadata | `{...}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Organization Claims
|
||||||
|
|
||||||
|
Clerk includes claims for the **active organization** (if user is in one and has selected it).
|
||||||
|
|
||||||
|
### Active Organization Claims
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"org_id": "org_2abc...", // Active organization ID
|
||||||
|
"org_slug": "acme-corp", // Active organization slug
|
||||||
|
"org_role": "org:admin" // User's role in active organization
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL CHANGE (Core 2)**:
|
||||||
|
- ✅ **New**: `org_id`, `org_slug`, `org_role` (active org only)
|
||||||
|
- ❌ **Removed**: `orgs` claim (previously contained all user organizations)
|
||||||
|
|
||||||
|
### Accessing Organization Data in Templates
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"organization": {
|
||||||
|
"id": "{{user.public_metadata.org_id}}",
|
||||||
|
"name": "{{user.public_metadata.org_name}}",
|
||||||
|
"role": "{{user.public_metadata.org_role}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: For all organizations (not just active), you must:
|
||||||
|
1. Store org data in `user.public_metadata` via Clerk API
|
||||||
|
2. Use custom JWT template to include it
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata Access
|
||||||
|
|
||||||
|
Clerk provides two metadata fields for storing custom user data:
|
||||||
|
|
||||||
|
### Public Metadata
|
||||||
|
- ✅ Accessible on client side
|
||||||
|
- ✅ Included in user objects
|
||||||
|
- ✅ Can be included in JWT templates
|
||||||
|
- ❌ Should NOT contain sensitive data
|
||||||
|
|
||||||
|
### Unsafe Metadata
|
||||||
|
- ⚠️ Name is misleading - it's for "unvalidated" data
|
||||||
|
- ✅ Accessible on client side
|
||||||
|
- ✅ Can be included in JWT templates
|
||||||
|
- ❌ Should NOT contain sensitive data
|
||||||
|
|
||||||
|
### Basic Metadata Access
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"all_public": "{{user.public_metadata}}",
|
||||||
|
"all_unsafe": "{{user.unsafe_metadata}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"all_public": {
|
||||||
|
"role": "admin",
|
||||||
|
"department": "engineering"
|
||||||
|
},
|
||||||
|
"all_unsafe": {
|
||||||
|
"onboardingComplete": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Metadata with Dot Notation
|
||||||
|
|
||||||
|
For nested objects, use dot notation to access specific fields:
|
||||||
|
|
||||||
|
**User's public_metadata**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"profile": {
|
||||||
|
"interests": ["hiking", "knitting"],
|
||||||
|
"bio": "Software engineer passionate about..."
|
||||||
|
},
|
||||||
|
"addresses": {
|
||||||
|
"Home": "2355 Pointe Lane, 56301 Minnesota",
|
||||||
|
"Work": "3759 Newton Street, 33487 Florida"
|
||||||
|
},
|
||||||
|
"role": "admin",
|
||||||
|
"department": "engineering"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JWT Template**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "{{user.public_metadata.role}}",
|
||||||
|
"department": "{{user.public_metadata.department}}",
|
||||||
|
"interests": "{{user.public_metadata.profile.interests}}",
|
||||||
|
"home_address": "{{user.public_metadata.addresses.Home}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated Token**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "admin",
|
||||||
|
"department": "engineering",
|
||||||
|
"interests": ["hiking", "knitting"],
|
||||||
|
"home_address": "2355 Pointe Lane, 56301 Minnesota"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why This Matters**: Dot notation prevents including the entire metadata object, reducing token size.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Template Features
|
||||||
|
|
||||||
|
### String Interpolation
|
||||||
|
|
||||||
|
Combine multiple shortcodes into a single string:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"full_name": "{{user.last_name}} {{user.first_name}}",
|
||||||
|
"greeting": "Hello, {{user.first_name}}!",
|
||||||
|
"email_with_name": "{{user.full_name}} <{{user.primary_email_address}}>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"full_name": "Doe John",
|
||||||
|
"greeting": "Hello, John!",
|
||||||
|
"email_with_name": "John Doe <john@example.com>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT**: Interpolated values are **always strings**, even if null values are present.
|
||||||
|
|
||||||
|
### Conditional Expressions (Fallbacks)
|
||||||
|
|
||||||
|
Use `||` operator to provide fallback values:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"full_name": "{{user.full_name || 'Guest User'}}",
|
||||||
|
"age": "{{user.public_metadata.age || 18}}",
|
||||||
|
"role": "{{user.public_metadata.role || 'user'}}",
|
||||||
|
"verified": "{{user.email_verified || user.phone_number_verified}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**How It Works**:
|
||||||
|
- Returns first **non-falsy** operand
|
||||||
|
- Final operand serves as default
|
||||||
|
- String literals require **single quotes**: `'default'`
|
||||||
|
- Can chain multiple fallbacks
|
||||||
|
|
||||||
|
**Example with Multiple Fallbacks**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks:
|
||||||
|
1. `user.public_metadata.age` (if null/false, continue)
|
||||||
|
2. `user.unsafe_metadata.age` (if null/false, continue)
|
||||||
|
3. `18` (default value)
|
||||||
|
|
||||||
|
### Boolean Checks
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
|
||||||
|
"is_complete": "{{user.public_metadata.profileComplete || false}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"has_verified_contact": true, // If either email or phone is verified
|
||||||
|
"is_complete": false // If profileComplete is not set
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating JWT Templates
|
||||||
|
|
||||||
|
### Step 1: Navigate to Clerk Dashboard
|
||||||
|
|
||||||
|
1. Go to **Sessions** page in Clerk Dashboard
|
||||||
|
2. Click **Customize session token**
|
||||||
|
3. Choose **Create template** or use pre-built templates
|
||||||
|
|
||||||
|
### Step 2: Configure Template Properties
|
||||||
|
|
||||||
|
**Template Properties**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "supabase", // Unique identifier (lowercase, no spaces)
|
||||||
|
"lifetime": 3600, // Token expiration in seconds (default: 60)
|
||||||
|
"allowed_clock_skew": 5, // Leeway for clock differences (default: 5)
|
||||||
|
"claims": { // Your custom claims
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Use Template in Code
|
||||||
|
|
||||||
|
**React / Next.js**:
|
||||||
|
```typescript
|
||||||
|
import { useAuth } from '@clerk/nextjs'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
|
||||||
|
// Get token with custom template
|
||||||
|
const token = await getToken({ template: 'supabase' })
|
||||||
|
|
||||||
|
// Use token to authenticate with external service
|
||||||
|
const response = await fetch('https://api.supabase.com/endpoint', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cloudflare Workers**:
|
||||||
|
```typescript
|
||||||
|
import { clerkClient } from '@clerk/backend'
|
||||||
|
|
||||||
|
// Generate token for user
|
||||||
|
const token = await clerkClient.users.getUserOauthAccessToken(
|
||||||
|
userId,
|
||||||
|
'supabase' // Template name
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Type Safety
|
||||||
|
|
||||||
|
Add type safety for custom claims with global declarations:
|
||||||
|
|
||||||
|
**Create `types/globals.d.ts`**:
|
||||||
|
```typescript
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface CustomJwtSessionClaims {
|
||||||
|
metadata: {
|
||||||
|
role?: 'admin' | 'moderator' | 'user'
|
||||||
|
onboardingComplete?: boolean
|
||||||
|
department?: string
|
||||||
|
organizationId?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```typescript
|
||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const { sessionClaims } = await auth()
|
||||||
|
|
||||||
|
// TypeScript now knows about these properties
|
||||||
|
const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
|
||||||
|
const isComplete = sessionClaims?.metadata?.onboardingComplete // Type: boolean | undefined
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### 1. Role-Based Access Control (RBAC)
|
||||||
|
|
||||||
|
**JWT Template**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role || 'user'}}",
|
||||||
|
"permissions": "{{user.public_metadata.permissions}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend Verification**:
|
||||||
|
```typescript
|
||||||
|
app.get('/api/admin', async (c) => {
|
||||||
|
const sessionClaims = c.get('sessionClaims')
|
||||||
|
|
||||||
|
if (sessionClaims?.role !== 'admin') {
|
||||||
|
return c.json({ error: 'Forbidden' }, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ message: 'Admin data' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Multi-Tenant Applications
|
||||||
|
|
||||||
|
**JWT Template**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "{{user.id}}",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"org_id": "{{user.public_metadata.org_id}}",
|
||||||
|
"org_slug": "{{user.public_metadata.org_slug}}",
|
||||||
|
"org_role": "{{user.public_metadata.org_role}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```typescript
|
||||||
|
// Filter data by organization
|
||||||
|
const data = await db.query('SELECT * FROM items WHERE org_id = ?', [
|
||||||
|
sessionClaims.org_id
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Supabase Integration
|
||||||
|
|
||||||
|
**JWT Template** (Name: `supabase`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"app_metadata": {
|
||||||
|
"provider": "clerk"
|
||||||
|
},
|
||||||
|
"user_metadata": {
|
||||||
|
"full_name": "{{user.full_name}}",
|
||||||
|
"avatar_url": "{{user.image_url}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```typescript
|
||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const token = await getToken({ template: 'supabase' })
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_ANON_KEY,
|
||||||
|
{
|
||||||
|
global: {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Grafbase GraphQL Integration
|
||||||
|
|
||||||
|
**JWT Template** (Name: `grafbase`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "{{user.id}}",
|
||||||
|
"groups": ["org:admin", "org:member"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grafbase Configuration**:
|
||||||
|
```toml
|
||||||
|
[auth.providers.clerk]
|
||||||
|
issuer = "https://your-app.clerk.accounts.dev"
|
||||||
|
jwks = "https://your-app.clerk.accounts.dev/.well-known/jwks.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations & Gotchas
|
||||||
|
|
||||||
|
### 1. Token Size Limit: 1.2KB for Custom Claims
|
||||||
|
|
||||||
|
**Problem**: Browsers limit cookies to 4KB. Clerk's default claims consume ~2.8KB, leaving **1.2KB for custom claims**.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- ✅ Use JWT for minimal claims (user ID, role, email)
|
||||||
|
- ✅ Store large data (bio, preferences) in your database
|
||||||
|
- ✅ Fetch additional data after authentication
|
||||||
|
- ❌ Don't include large objects or arrays
|
||||||
|
|
||||||
|
**Bad Example** (Exceeds limit):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"bio": "{{user.public_metadata.bio}}", // 6KB bio field
|
||||||
|
"all_metadata": "{{user.public_metadata}}" // Entire object
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Good Example** (Under limit):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "{{user.id}}",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Reserved Claims
|
||||||
|
|
||||||
|
These claims are **reserved** and cannot be used in custom templates:
|
||||||
|
|
||||||
|
| Claim | Reason |
|
||||||
|
|-------|--------|
|
||||||
|
| `azp` | Auto-included default claim |
|
||||||
|
| `exp` | Auto-included default claim |
|
||||||
|
| `iat` | Auto-included default claim |
|
||||||
|
| `iss` | Auto-included default claim |
|
||||||
|
| `jti` | Auto-included default claim |
|
||||||
|
| `nbf` | Auto-included default claim |
|
||||||
|
| `sub` | Auto-included default claim |
|
||||||
|
|
||||||
|
**Error**: Attempting to override reserved claims returns HTTP 400 with error code `jwt_template_reserved_claim`.
|
||||||
|
|
||||||
|
### 3. Session-Bound Claims
|
||||||
|
|
||||||
|
These claims are **session-specific** and cannot be included in **custom JWTs** (but ARE included in default session tokens):
|
||||||
|
|
||||||
|
| Claim | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `sid` | Session ID |
|
||||||
|
| `v` | Version |
|
||||||
|
| `pla` | Plan |
|
||||||
|
| `fea` | Features |
|
||||||
|
|
||||||
|
**Why**: Custom JWTs are generated on-demand and not tied to a specific session.
|
||||||
|
|
||||||
|
**Solution**: If you need session data, use **custom session tokens** instead of custom JWTs.
|
||||||
|
|
||||||
|
### 4. Invalid Shortcodes Resolve to `null`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": "{{user.first_name}}",
|
||||||
|
"invalid": "{{user.i_dont_exist}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": "John",
|
||||||
|
"invalid": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevention**: Always test templates with real user data before deploying.
|
||||||
|
|
||||||
|
### 5. Custom JWTs May Incur Latency
|
||||||
|
|
||||||
|
- Default session tokens are cached
|
||||||
|
- Custom JWTs require fetching user data on-demand
|
||||||
|
- This can add **50-200ms** to token generation
|
||||||
|
|
||||||
|
**When to Use Custom JWTs**:
|
||||||
|
- ✅ Integrating with third-party services (Supabase, Hasura, Grafbase)
|
||||||
|
- ✅ API-to-API authentication
|
||||||
|
- ❌ Not recommended for every frontend request (use default session tokens)
|
||||||
|
|
||||||
|
### 6. Organization Data Limitations
|
||||||
|
|
||||||
|
- Only **active organization** data is included by default
|
||||||
|
- For all organizations, store in `user.public_metadata` and use custom template
|
||||||
|
- `orgs` claim was removed in Core 2 (breaking change)
|
||||||
|
|
||||||
|
### 7. Metadata Sync Timing
|
||||||
|
|
||||||
|
- Metadata changes may take a few seconds to propagate to tokens
|
||||||
|
- Use `user.reload()` in frontend to refresh user object
|
||||||
|
- Backend should always fetch fresh data for critical operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
|
||||||
|
- **JWT Templates Guide**: https://clerk.com/docs/guides/sessions/jwt-templates
|
||||||
|
- **Custom Session Tokens**: https://clerk.com/docs/backend-requests/making/custom-session-token
|
||||||
|
- **Session Claims Access**: https://clerk.com/docs/upgrade-guides/core-2/node
|
||||||
|
- **Backend SDK**: https://clerk.com/docs/reference/backend/overview
|
||||||
|
- **Supabase Integration**: https://clerk.com/docs/guides/development/integrations/databases/supabase
|
||||||
|
- **Grafbase Integration**: https://clerk.com/docs/guides/development/integrations/databases/grafbase
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference: All Available Shortcodes
|
||||||
|
|
||||||
|
### User Properties
|
||||||
|
```
|
||||||
|
{{user.id}}
|
||||||
|
{{user.first_name}}
|
||||||
|
{{user.last_name}}
|
||||||
|
{{user.full_name}}
|
||||||
|
{{user.primary_email_address}}
|
||||||
|
{{user.primary_phone_address}}
|
||||||
|
{{user.image_url}}
|
||||||
|
{{user.created_at}}
|
||||||
|
{{user.username}}
|
||||||
|
{{user.email_verified}}
|
||||||
|
{{user.phone_number_verified}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
```
|
||||||
|
{{user.public_metadata}}
|
||||||
|
{{user.public_metadata.FIELD_NAME}}
|
||||||
|
{{user.public_metadata.nested.field}}
|
||||||
|
{{user.unsafe_metadata}}
|
||||||
|
{{user.unsafe_metadata.FIELD_NAME}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operators
|
||||||
|
```
|
||||||
|
{{shortcode1 || shortcode2}} // Fallback
|
||||||
|
{{shortcode || 'default'}} // Default value
|
||||||
|
"{{shortcode1}} {{shortcode2}}" // String interpolation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-22
|
||||||
|
**Verified Against**: Clerk API v2025-04-10
|
||||||
|
**Production Tested**: ✅ Multiple frameworks
|
||||||
575
references/testing-guide.md
Normal file
575
references/testing-guide.md
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
# Clerk Authentication - Testing Guide
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-28
|
||||||
|
**Source**: https://clerk.com/docs/guides/development/testing/overview
|
||||||
|
|
||||||
|
This guide covers testing Clerk authentication in your applications, including test credentials, session tokens, testing tokens, and E2E testing with Playwright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Testing authentication flows is essential for reliable applications. Clerk provides several tools to make testing easier:
|
||||||
|
|
||||||
|
1. **Test Emails & Phone Numbers** - Fake credentials with fixed OTP codes
|
||||||
|
2. **Session Tokens** - Generate valid tokens via Backend API
|
||||||
|
3. **Testing Tokens** - Bypass bot detection in test suites
|
||||||
|
4. **Framework Integrations** - Playwright and Cypress helpers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start: Test Mode
|
||||||
|
|
||||||
|
### Enable Test Mode
|
||||||
|
|
||||||
|
Every **development instance** has test mode enabled by default.
|
||||||
|
|
||||||
|
For **production instances** (NOT recommended for real customer data):
|
||||||
|
1. Navigate to Clerk Dashboard → **Settings**
|
||||||
|
2. Enable the **Enable test mode** toggle
|
||||||
|
|
||||||
|
> **WARNING**: Never use test mode on instances managing actual customers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Emails & Phone Numbers
|
||||||
|
|
||||||
|
### Fake Email Addresses
|
||||||
|
|
||||||
|
Any email with the `+clerk_test` subaddress is a test email address:
|
||||||
|
|
||||||
|
```
|
||||||
|
jane+clerk_test@example.com
|
||||||
|
john+clerk_test@gmail.com
|
||||||
|
test+clerk_test@mycompany.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- ✅ No emails sent (saves your email quota)
|
||||||
|
- ✅ Fixed OTP code: `424242`
|
||||||
|
- ✅ Works in all sign-up/sign-in flows
|
||||||
|
|
||||||
|
### Fake Phone Numbers
|
||||||
|
|
||||||
|
Any [fictional phone number](https://en.wikipedia.org/wiki/555_(telephone_number)) is a test phone number:
|
||||||
|
|
||||||
|
**Format**: `+1 (XXX) 555-0100` to `+1 (XXX) 555-0199`
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```
|
||||||
|
+12015550100
|
||||||
|
+19735550133
|
||||||
|
+14155550142
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- ✅ No SMS sent (saves your SMS quota)
|
||||||
|
- ✅ Fixed OTP code: `424242`
|
||||||
|
- ✅ Works in all verification flows
|
||||||
|
|
||||||
|
### Monthly Limits (Development Instances)
|
||||||
|
|
||||||
|
Clerk development instances have limits:
|
||||||
|
- **20 SMS messages** per month
|
||||||
|
- **100 emails** per month
|
||||||
|
|
||||||
|
**Excluded from limits**:
|
||||||
|
- SMS to US numbers
|
||||||
|
- SMS/emails to test addresses (with `+clerk_test` or 555 numbers)
|
||||||
|
- Self-delivered messages (your own SMTP/SMS provider)
|
||||||
|
- Paid subscription apps
|
||||||
|
|
||||||
|
To request higher limits, contact Clerk support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Examples: Test Credentials
|
||||||
|
|
||||||
|
### Sign In with Test Email
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useSignIn } from '@clerk/clerk-react'
|
||||||
|
|
||||||
|
const testSignInWithEmailCode = async () => {
|
||||||
|
const { signIn } = useSignIn()
|
||||||
|
|
||||||
|
const emailAddress = 'john+clerk_test@example.com'
|
||||||
|
|
||||||
|
// Step 1: Create sign-in attempt
|
||||||
|
const signInResp = await signIn.create({ identifier: emailAddress })
|
||||||
|
|
||||||
|
// Step 2: Find email code factor
|
||||||
|
const { emailAddressId } = signInResp.supportedFirstFactors.find(
|
||||||
|
(ff) => ff.strategy === 'email_code' && ff.safeIdentifier === emailAddress,
|
||||||
|
)! as EmailCodeFactor
|
||||||
|
|
||||||
|
// Step 3: Prepare email verification
|
||||||
|
await signIn.prepareFirstFactor({
|
||||||
|
strategy: 'email_code',
|
||||||
|
emailAddressId: emailAddressId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 4: Verify with fixed code
|
||||||
|
const attemptResponse = await signIn.attemptFirstFactor({
|
||||||
|
strategy: 'email_code',
|
||||||
|
code: '424242', // Fixed test code
|
||||||
|
})
|
||||||
|
|
||||||
|
if (attemptResponse.status === 'complete') {
|
||||||
|
console.log('Sign in successful!')
|
||||||
|
} else {
|
||||||
|
console.error('Sign in failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sign Up with Test Phone
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useSignUp } from '@clerk/clerk-react'
|
||||||
|
|
||||||
|
const testSignUpWithPhoneNumber = async () => {
|
||||||
|
const { signUp } = useSignUp()
|
||||||
|
|
||||||
|
// Step 1: Create sign-up with test phone
|
||||||
|
await signUp.create({
|
||||||
|
phoneNumber: '+12015550100',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 2: Prepare phone verification
|
||||||
|
await signUp.preparePhoneNumberVerification()
|
||||||
|
|
||||||
|
// Step 3: Verify with fixed code
|
||||||
|
const res = await signUp.attemptPhoneNumberVerification({
|
||||||
|
code: '424242', // Fixed test code
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.verifications.phoneNumber.status === 'verified') {
|
||||||
|
console.log('Sign up successful!')
|
||||||
|
} else {
|
||||||
|
console.error('Sign up failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Tokens (Backend API)
|
||||||
|
|
||||||
|
For testing API endpoints or backend services, you need valid session tokens.
|
||||||
|
|
||||||
|
### Flow: Generate Session Token
|
||||||
|
|
||||||
|
**4-Step Process**:
|
||||||
|
|
||||||
|
1. **Create User** (if needed)
|
||||||
|
2. **Create Session** for user
|
||||||
|
3. **Create Session Token** from session ID
|
||||||
|
4. **Use Token** in Authorization header
|
||||||
|
|
||||||
|
### Step-by-Step Implementation
|
||||||
|
|
||||||
|
#### Step 1: Create User
|
||||||
|
|
||||||
|
**Endpoint**: `POST https://api.clerk.com/v1/users`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.clerk.com/v1/users \
|
||||||
|
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email_address": ["test+clerk_test@example.com"],
|
||||||
|
"password": "TestPassword123!"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "user_2abc123def456",
|
||||||
|
"email_addresses": [
|
||||||
|
{
|
||||||
|
"id": "idn_2xyz789",
|
||||||
|
"email_address": "test+clerk_test@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save**: `user_id` for next step
|
||||||
|
|
||||||
|
#### Step 2: Create Session
|
||||||
|
|
||||||
|
**Endpoint**: `POST https://api.clerk.com/v1/sessions`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.clerk.com/v1/sessions \
|
||||||
|
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"user_id": "user_2abc123def456"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "sess_2xyz789abc123",
|
||||||
|
"user_id": "user_2abc123def456",
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save**: `session_id` for next step
|
||||||
|
|
||||||
|
#### Step 3: Create Session Token
|
||||||
|
|
||||||
|
**Endpoint**: `POST https://api.clerk.com/v1/sessions/{session_id}/tokens`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.clerk.com/v1/sessions/sess_2xyz789abc123/tokens \
|
||||||
|
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"object": "token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save**: `jwt` token
|
||||||
|
|
||||||
|
#### Step 4: Use Token in Requests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://yourdomain.com/api/protected \
|
||||||
|
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Lifetime
|
||||||
|
|
||||||
|
**CRITICAL**: Session tokens are valid for **60 seconds only**.
|
||||||
|
|
||||||
|
**Refresh Strategies**:
|
||||||
|
|
||||||
|
**Option 1: Before Each Test**
|
||||||
|
```typescript
|
||||||
|
beforeEach(async () => {
|
||||||
|
sessionToken = await refreshSessionToken(sessionId)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Interval Timer**
|
||||||
|
```typescript
|
||||||
|
setInterval(async () => {
|
||||||
|
sessionToken = await refreshSessionToken(sessionId)
|
||||||
|
}, 50000) // Refresh every 50 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Script Example
|
||||||
|
|
||||||
|
See `scripts/generate-session-token.js` for a complete implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Tokens (Bot Detection Bypass)
|
||||||
|
|
||||||
|
Testing Tokens bypass Clerk's bot detection mechanisms during automated testing.
|
||||||
|
|
||||||
|
### What Are Testing Tokens?
|
||||||
|
|
||||||
|
- **Purpose**: Prevent "Bot traffic detected" errors in test suites
|
||||||
|
- **Lifetime**: Short-lived (expires after use)
|
||||||
|
- **Scope**: Valid only for specific Clerk instance
|
||||||
|
- **Source**: Obtained via Backend API
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
|
||||||
|
**Use Testing Tokens when**:
|
||||||
|
- Running E2E tests with Playwright or Cypress
|
||||||
|
- Automated test suites triggering bot detection
|
||||||
|
- CI/CD pipelines running authentication flows
|
||||||
|
|
||||||
|
**Alternatives**:
|
||||||
|
- Use `@clerk/testing` package (handles automatically)
|
||||||
|
- Playwright integration (recommended)
|
||||||
|
- Cypress integration (recommended)
|
||||||
|
|
||||||
|
### Obtain Testing Token
|
||||||
|
|
||||||
|
**Endpoint**: `POST https://api.clerk.com/v1/testing_tokens`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.clerk.com/v1/testing_tokens \
|
||||||
|
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Testing Token
|
||||||
|
|
||||||
|
Add `__clerk_testing_token` query parameter to Frontend API requests:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://happy-hippo-1.clerk.accounts.dev/v1/client/sign_ups?__clerk_testing_token=1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Limitations
|
||||||
|
|
||||||
|
Testing Tokens work in **both development and production**, but:
|
||||||
|
|
||||||
|
**❌ Not Supported in Production**:
|
||||||
|
- Code-based authentication (SMS OTP, Email OTP)
|
||||||
|
|
||||||
|
**✅ Supported in Production**:
|
||||||
|
- Email + password authentication
|
||||||
|
- Email magic link (sign-in via email)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E2E Testing with Playwright
|
||||||
|
|
||||||
|
Clerk provides first-class Playwright support via `@clerk/testing`.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D @clerk/testing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Environment Variables
|
||||||
|
|
||||||
|
In your test runner (e.g., `.env.test` or GitHub Actions secrets):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
CLERK_SECRET_KEY=sk_test_...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security**: Use GitHub Actions secrets or similar for CI/CD.
|
||||||
|
|
||||||
|
### Global Setup
|
||||||
|
|
||||||
|
Configure `clerkSetup()` to obtain Testing Token once for all tests:
|
||||||
|
|
||||||
|
**File**: `global.setup.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { clerkSetup } from '@clerk/testing/playwright'
|
||||||
|
import { test as setup } from '@playwright/test'
|
||||||
|
|
||||||
|
// Run setup serially (important for fully parallel Playwright config)
|
||||||
|
setup.describe.configure({ mode: 'serial' })
|
||||||
|
|
||||||
|
setup('global setup', async ({}) => {
|
||||||
|
await clerkSetup()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**What This Does**:
|
||||||
|
- Obtains Testing Token from Clerk Backend API
|
||||||
|
- Stores token in `CLERK_TESTING_TOKEN` environment variable
|
||||||
|
- Makes token available for all tests
|
||||||
|
|
||||||
|
### Use in Tests
|
||||||
|
|
||||||
|
Import `setupClerkTestingToken()` and call before navigating to auth pages:
|
||||||
|
|
||||||
|
**File**: `auth.spec.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setupClerkTestingToken } from '@clerk/testing/playwright'
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test('sign up flow', async ({ page }) => {
|
||||||
|
// Inject Testing Token for this test
|
||||||
|
await setupClerkTestingToken({ page })
|
||||||
|
|
||||||
|
// Navigate to sign-up page
|
||||||
|
await page.goto('/sign-up')
|
||||||
|
|
||||||
|
// Fill form with test credentials
|
||||||
|
await page.fill('input[name="emailAddress"]', 'test+clerk_test@example.com')
|
||||||
|
await page.fill('input[name="password"]', 'TestPassword123!')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// Verify with fixed OTP
|
||||||
|
await page.fill('input[name="code"]', '424242')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// Assert success
|
||||||
|
await expect(page).toHaveURL('/dashboard')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sign in with test phone', async ({ page }) => {
|
||||||
|
await setupClerkTestingToken({ page })
|
||||||
|
|
||||||
|
await page.goto('/sign-in')
|
||||||
|
await page.fill('input[name="identifier"]', '+12015550100')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
// Enter fixed OTP
|
||||||
|
await page.fill('input[name="code"]', '424242')
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/dashboard')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Testing Token Setup (Alternative)
|
||||||
|
|
||||||
|
Instead of `clerkSetup()`, manually set the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLERK_TESTING_TOKEN="1713877200-c_2J2MvPu9PnXcuhbPZNao0LOXqK9A7YrnBn0HmIWxy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run tests as usual. `setupClerkTestingToken()` will use this value.
|
||||||
|
|
||||||
|
### Demo Repository
|
||||||
|
|
||||||
|
Clerk provides a complete example:
|
||||||
|
|
||||||
|
**Repository**: https://github.com/clerk/clerk-playwright-nextjs
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Next.js App Router with Clerk
|
||||||
|
- Playwright E2E tests
|
||||||
|
- Testing Tokens setup
|
||||||
|
- Test user authentication
|
||||||
|
|
||||||
|
**To Run**:
|
||||||
|
1. Clone repo
|
||||||
|
2. Add Clerk API keys to `.env.local`
|
||||||
|
3. Create test user with username + password
|
||||||
|
4. Enable username/password auth in Clerk Dashboard
|
||||||
|
5. Run `npm test`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Email Links (Magic Links)
|
||||||
|
|
||||||
|
Email links are challenging to test in E2E suites.
|
||||||
|
|
||||||
|
**Recommendation**: Use email verification codes instead.
|
||||||
|
|
||||||
|
### Enable Email Verification Code
|
||||||
|
|
||||||
|
1. Clerk Dashboard → **Email, Phone, Username**
|
||||||
|
2. Enable **Email verification code** strategy
|
||||||
|
3. Use the code flow in tests (easier to automate)
|
||||||
|
|
||||||
|
**Code flow** and **link flow** are functionally equivalent for most use cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Development Testing
|
||||||
|
|
||||||
|
✅ **Do**:
|
||||||
|
- Use test emails (`+clerk_test`) and phone numbers (555-01XX)
|
||||||
|
- Fixed OTP: `424242`
|
||||||
|
- Enable test mode in Clerk Dashboard
|
||||||
|
- Use `@clerk/testing` for Playwright/Cypress
|
||||||
|
|
||||||
|
❌ **Don't**:
|
||||||
|
- Send real emails/SMS during tests (wastes quota)
|
||||||
|
- Use production keys in tests
|
||||||
|
- Enable test mode on production instances with real users
|
||||||
|
|
||||||
|
### Backend/API Testing
|
||||||
|
|
||||||
|
✅ **Do**:
|
||||||
|
- Generate session tokens via Backend API
|
||||||
|
- Refresh tokens before each test or on interval
|
||||||
|
- Use test users created via API
|
||||||
|
- Store `CLERK_SECRET_KEY` securely
|
||||||
|
|
||||||
|
❌ **Don't**:
|
||||||
|
- Hardcode session tokens (expire in 60 seconds)
|
||||||
|
- Reuse expired tokens
|
||||||
|
- Expose secret keys in logs or version control
|
||||||
|
|
||||||
|
### E2E Testing
|
||||||
|
|
||||||
|
✅ **Do**:
|
||||||
|
- Use `@clerk/testing` for automatic Testing Token management
|
||||||
|
- Configure global setup for token generation
|
||||||
|
- Use test credentials in all flows
|
||||||
|
- Run tests in CI/CD with secret environment variables
|
||||||
|
|
||||||
|
❌ **Don't**:
|
||||||
|
- Skip `setupClerkTestingToken()` (triggers bot detection)
|
||||||
|
- Manually implement Testing Token logic (use helpers)
|
||||||
|
- Test code-based auth in production with Testing Tokens
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Bot traffic detected" Error
|
||||||
|
|
||||||
|
**Cause**: Missing Testing Token in test suite
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Install `@clerk/testing`
|
||||||
|
2. Configure `clerkSetup()` in global setup
|
||||||
|
3. Call `setupClerkTestingToken({ page })` in tests
|
||||||
|
|
||||||
|
### Session Token Expired
|
||||||
|
|
||||||
|
**Cause**: Token lifetime is 60 seconds
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Refresh token before each test: `beforeEach(() => refreshToken())`
|
||||||
|
- Use interval timer: `setInterval(() => refreshToken(), 50000)`
|
||||||
|
|
||||||
|
### Test Email Not Working
|
||||||
|
|
||||||
|
**Cause**: Test mode not enabled, or wrong email format
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Ensure email has `+clerk_test` subaddress
|
||||||
|
- Enable test mode in Clerk Dashboard
|
||||||
|
- Use fixed OTP: `424242`
|
||||||
|
|
||||||
|
### 20 SMS / 100 Email Limit Reached
|
||||||
|
|
||||||
|
**Cause**: Exceeded monthly limit for development instance
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Use test credentials (excluded from limits)
|
||||||
|
- Contact Clerk support to request higher limits
|
||||||
|
- Use self-delivered SMS/email provider
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference Links
|
||||||
|
|
||||||
|
**Official Docs**:
|
||||||
|
- Testing Overview: https://clerk.com/docs/guides/development/testing/overview
|
||||||
|
- Test Emails & Phones: https://clerk.com/docs/guides/development/testing/test-emails-and-phones
|
||||||
|
- Playwright Integration: https://clerk.com/docs/guides/development/testing/playwright/overview
|
||||||
|
- Backend API Reference: https://clerk.com/docs/reference/backend-api
|
||||||
|
|
||||||
|
**Packages**:
|
||||||
|
- `@clerk/testing`: https://github.com/clerk/javascript/tree/main/packages/testing
|
||||||
|
- Demo Repository: https://github.com/clerk/clerk-playwright-nextjs
|
||||||
|
|
||||||
|
**API Endpoints**:
|
||||||
|
- Create User: `POST /v1/users`
|
||||||
|
- Create Session: `POST /v1/sessions`
|
||||||
|
- Create Session Token: `POST /v1/sessions/{session_id}/tokens`
|
||||||
|
- Create Testing Token: `POST /v1/testing_tokens`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-28
|
||||||
15
scripts/example-script.sh
Executable file
15
scripts/example-script.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# [TODO: Script Name]
|
||||||
|
# [TODO: Brief description of what this script does]
|
||||||
|
|
||||||
|
# Example script structure - delete if not needed
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# [TODO: Add your script logic here]
|
||||||
|
|
||||||
|
echo "Example script - replace or delete this file"
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/example-script.sh [args]
|
||||||
241
scripts/generate-session-token.js
Executable file
241
scripts/generate-session-token.js
Executable file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clerk Session Token Generator
|
||||||
|
*
|
||||||
|
* Generates valid session tokens for testing Clerk authentication.
|
||||||
|
* Session tokens are valid for 60 seconds and must be refreshed regularly.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node generate-session-token.js
|
||||||
|
* node generate-session-token.js --create-user
|
||||||
|
* node generate-session-token.js --user-id user_abc123
|
||||||
|
* node generate-session-token.js --refresh
|
||||||
|
*
|
||||||
|
* Environment Variables:
|
||||||
|
* CLERK_SECRET_KEY - Your Clerk secret key (required)
|
||||||
|
*
|
||||||
|
* @see https://clerk.com/docs/guides/development/testing/overview
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https')
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const CLERK_SECRET_KEY = process.env.CLERK_SECRET_KEY
|
||||||
|
const API_BASE = 'https://api.clerk.com/v1'
|
||||||
|
|
||||||
|
// Parse CLI arguments
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const shouldCreateUser = args.includes('--create-user')
|
||||||
|
const shouldRefresh = args.includes('--refresh')
|
||||||
|
const userIdArg = args.find(arg => arg.startsWith('--user-id='))
|
||||||
|
const providedUserId = userIdArg ? userIdArg.split('=')[1] : null
|
||||||
|
|
||||||
|
// Validate secret key
|
||||||
|
if (!CLERK_SECRET_KEY) {
|
||||||
|
console.error('❌ Error: CLERK_SECRET_KEY environment variable is required')
|
||||||
|
console.error('\nUsage: CLERK_SECRET_KEY=sk_test_... node generate-session-token.js')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make HTTPS request
|
||||||
|
function makeRequest(path, method = 'GET', data = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(`${API_BASE}${path}`)
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${CLERK_SECRET_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = ''
|
||||||
|
res.on('data', (chunk) => body += chunk)
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body)
|
||||||
|
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(json)
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
error: json,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject({
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
error: body,
|
||||||
|
parseError: err.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('error', reject)
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
req.write(JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test user
|
||||||
|
async function createUser() {
|
||||||
|
console.log('📝 Creating test user...')
|
||||||
|
|
||||||
|
const email = `test+clerk_test_${Date.now()}@example.com`
|
||||||
|
const password = 'TestPassword123!'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await makeRequest('/users', 'POST', {
|
||||||
|
email_address: [email],
|
||||||
|
password: password,
|
||||||
|
skip_password_checks: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ User created:')
|
||||||
|
console.log(` User ID: ${user.id}`)
|
||||||
|
console.log(` Email: ${email}`)
|
||||||
|
console.log(` Password: ${password}`)
|
||||||
|
|
||||||
|
return user.id
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to create user:', err.error)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing user (first user in instance)
|
||||||
|
async function getExistingUser() {
|
||||||
|
console.log('🔍 Finding existing user...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await makeRequest('/users?limit=1')
|
||||||
|
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
const user = response.data[0]
|
||||||
|
console.log(`✅ Found user: ${user.id}`)
|
||||||
|
return user.id
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No users found. Use --create-user to create one.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to get user:', err.error)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session for user
|
||||||
|
async function createSession(userId) {
|
||||||
|
console.log(`🔐 Creating session for user ${userId}...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await makeRequest('/sessions', 'POST', {
|
||||||
|
user_id: userId,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Session created: ${session.id}`)
|
||||||
|
return session.id
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to create session:', err.error)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session token
|
||||||
|
async function createSessionToken(sessionId) {
|
||||||
|
try {
|
||||||
|
const response = await makeRequest(`/sessions/${sessionId}/tokens`, 'POST')
|
||||||
|
return response.jwt
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to create session token:', err.error)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh session token (same as create)
|
||||||
|
async function refreshSessionToken(sessionId) {
|
||||||
|
console.log('🔄 Refreshing session token...')
|
||||||
|
const token = await createSessionToken(sessionId)
|
||||||
|
console.log('✅ Token refreshed')
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
async function main() {
|
||||||
|
console.log('🎫 Clerk Session Token Generator\n')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Get or create user
|
||||||
|
let userId = providedUserId
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
if (shouldCreateUser) {
|
||||||
|
userId = await createUser()
|
||||||
|
} else {
|
||||||
|
userId = await getExistingUser()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`📌 Using provided user ID: ${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.log('\n💡 Tip: Run with --create-user to create a test user')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Create session
|
||||||
|
const sessionId = await createSession(userId)
|
||||||
|
|
||||||
|
// Step 3: Create token
|
||||||
|
console.log('🎫 Generating session token...')
|
||||||
|
const token = await createSessionToken(sessionId)
|
||||||
|
|
||||||
|
console.log('\n✅ Session Token Generated!\n')
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||||
|
console.log('Token (valid for 60 seconds):')
|
||||||
|
console.log(token)
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
|
||||||
|
|
||||||
|
console.log('📋 Usage in API requests:\n')
|
||||||
|
console.log('curl https://yourdomain.com/api/protected \\')
|
||||||
|
console.log(` -H "Authorization: Bearer ${token.substring(0, 50)}..."\n`)
|
||||||
|
|
||||||
|
// Step 4: Refresh mode (optional)
|
||||||
|
if (shouldRefresh) {
|
||||||
|
console.log('🔄 Refresh mode enabled. Token will refresh every 50 seconds.')
|
||||||
|
console.log('Press Ctrl+C to stop.\n')
|
||||||
|
|
||||||
|
// Refresh every 50 seconds
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const newToken = await refreshSessionToken(sessionId)
|
||||||
|
console.log(`\n🎫 New Token: ${newToken}\n`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Failed to refresh token:', err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}, 50000)
|
||||||
|
} else {
|
||||||
|
console.log('💡 Tip: Add --refresh flag to auto-refresh token every 50 seconds')
|
||||||
|
console.log(`💡 Tip: Reuse this session with --user-id=${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ Error:', err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run main function
|
||||||
|
main()
|
||||||
210
templates/cloudflare/worker-auth.ts
Normal file
210
templates/cloudflare/worker-auth.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Worker with Clerk Authentication
|
||||||
|
*
|
||||||
|
* This template demonstrates:
|
||||||
|
* - JWT token verification with @clerk/backend
|
||||||
|
* - Protected API routes
|
||||||
|
* - Type-safe Hono context with auth state
|
||||||
|
* - Proper error handling
|
||||||
|
*
|
||||||
|
* Dependencies:
|
||||||
|
* - @clerk/backend@^2.17.2
|
||||||
|
* - hono@^4.10.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { verifyToken } from '@clerk/backend'
|
||||||
|
import { cors } from 'hono/cors'
|
||||||
|
|
||||||
|
// Type-safe environment bindings
|
||||||
|
type Bindings = {
|
||||||
|
CLERK_SECRET_KEY: string
|
||||||
|
CLERK_PUBLISHABLE_KEY: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context variables with auth state
|
||||||
|
type Variables = {
|
||||||
|
userId: string | null
|
||||||
|
sessionClaims: any | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
||||||
|
|
||||||
|
// CORS middleware (adjust origins for production)
|
||||||
|
app.use('*', cors({
|
||||||
|
origin: ['http://localhost:5173', 'https://yourdomain.com'],
|
||||||
|
credentials: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Middleware - Verifies Clerk JWT tokens
|
||||||
|
*
|
||||||
|
* CRITICAL SECURITY:
|
||||||
|
* - Always set authorizedParties to prevent CSRF attacks
|
||||||
|
* - Use secretKey, not deprecated apiKey
|
||||||
|
* - Token is in Authorization: Bearer <token> header
|
||||||
|
*/
|
||||||
|
app.use('/api/*', async (c, next) => {
|
||||||
|
const authHeader = c.req.header('Authorization')
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
// No auth header - continue as unauthenticated
|
||||||
|
c.set('userId', null)
|
||||||
|
c.set('sessionClaims', null)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from "Bearer <token>"
|
||||||
|
const token = authHeader.substring(7)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify token with Clerk
|
||||||
|
const { data, error } = await verifyToken(token, {
|
||||||
|
secretKey: c.env.CLERK_SECRET_KEY,
|
||||||
|
|
||||||
|
// IMPORTANT: Set to your actual domain(s) to prevent CSRF
|
||||||
|
// Source: https://clerk.com/docs/reference/backend/verify-token
|
||||||
|
authorizedParties: [
|
||||||
|
'http://localhost:5173', // Development
|
||||||
|
'https://yourdomain.com', // Production
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('[Auth] Token verification failed:', error.message)
|
||||||
|
c.set('userId', null)
|
||||||
|
c.set('sessionClaims', null)
|
||||||
|
} else {
|
||||||
|
// 'sub' claim contains the user ID
|
||||||
|
c.set('userId', data.sub)
|
||||||
|
c.set('sessionClaims', data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Auth] Token verification error:', err)
|
||||||
|
c.set('userId', null)
|
||||||
|
c.set('sessionClaims', null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Routes - No authentication required
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.get('/api/public', (c) => {
|
||||||
|
return c.json({
|
||||||
|
message: 'This endpoint is public',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/health', (c) => {
|
||||||
|
return c.json({
|
||||||
|
status: 'ok',
|
||||||
|
version: '1.0.0',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected Routes - Require authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.get('/api/protected', (c) => {
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
message: 'This endpoint is protected',
|
||||||
|
userId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/user/profile', (c) => {
|
||||||
|
const userId = c.get('userId')
|
||||||
|
const sessionClaims = c.get('sessionClaims')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access custom claims from JWT template (if configured)
|
||||||
|
return c.json({
|
||||||
|
userId,
|
||||||
|
email: sessionClaims?.email,
|
||||||
|
role: sessionClaims?.role,
|
||||||
|
organizationId: sessionClaims?.organization_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST Example - Create resource with auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.post('/api/items', async (c) => {
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await c.req.json()
|
||||||
|
|
||||||
|
// Validate and process body
|
||||||
|
// Example: save to D1, KV, or R2
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
itemId: crypto.randomUUID(),
|
||||||
|
userId,
|
||||||
|
}, 201)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role-Based Access Control Example
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.get('/api/admin/dashboard', (c) => {
|
||||||
|
const userId = c.get('userId')
|
||||||
|
const sessionClaims = c.get('sessionClaims')
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role from custom JWT claims
|
||||||
|
const role = sessionClaims?.role
|
||||||
|
|
||||||
|
if (role !== 'admin') {
|
||||||
|
return c.json({ error: 'Forbidden: Admin access required' }, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
message: 'Admin dashboard data',
|
||||||
|
userId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error Handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.onError((err, c) => {
|
||||||
|
console.error('[Error]', err)
|
||||||
|
return c.json({ error: 'Internal Server Error' }, 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.notFound((c) => {
|
||||||
|
return c.json({ error: 'Not Found' }, 404)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the Hono app
|
||||||
|
*
|
||||||
|
* ES Module format for Cloudflare Workers
|
||||||
|
*/
|
||||||
|
export default app
|
||||||
46
templates/cloudflare/wrangler.jsonc
Normal file
46
templates/cloudflare/wrangler.jsonc
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "my-clerk-worker",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"account_id": "YOUR_ACCOUNT_ID",
|
||||||
|
"compatibility_date": "2025-10-11",
|
||||||
|
"observability": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"vars": {
|
||||||
|
"CLERK_PUBLISHABLE_KEY": "pk_test_..."
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL: Never commit CLERK_SECRET_KEY to version control
|
||||||
|
*
|
||||||
|
* To set CLERK_SECRET_KEY:
|
||||||
|
*
|
||||||
|
* 1. Production:
|
||||||
|
* wrangler secret put CLERK_SECRET_KEY
|
||||||
|
*
|
||||||
|
* 2. Local development:
|
||||||
|
* Create .dev.vars file (see templates/env-examples/.dev.vars.example)
|
||||||
|
*
|
||||||
|
* After setting, access via c.env.CLERK_SECRET_KEY in your Worker
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional: Add other Cloudflare bindings
|
||||||
|
*
|
||||||
|
* KV Namespace:
|
||||||
|
* "kv_namespaces": [
|
||||||
|
* { "binding": "AUTH_CACHE", "id": "YOUR_KV_ID" }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* D1 Database:
|
||||||
|
* "d1_databases": [
|
||||||
|
* { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* R2 Bucket:
|
||||||
|
* "r2_buckets": [
|
||||||
|
* { "binding": "ASSETS", "bucket_name": "my-bucket" }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
}
|
||||||
63
templates/env-examples/.dev.vars.example
Normal file
63
templates/env-examples/.dev.vars.example
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Clerk Environment Variables for Cloudflare Workers
|
||||||
|
#
|
||||||
|
# Copy this file to .dev.vars for local development
|
||||||
|
# Get your keys from https://dashboard.clerk.com
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# LOCAL DEVELOPMENT (.dev.vars)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Secret Key (server-side verification)
|
||||||
|
CLERK_SECRET_KEY=sk_test_...
|
||||||
|
|
||||||
|
# Publishable Key (can be in wrangler.jsonc or here)
|
||||||
|
CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# PRODUCTION DEPLOYMENT
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# For production, use wrangler secrets:
|
||||||
|
#
|
||||||
|
# 1. Set secret key (encrypted, not in wrangler.jsonc):
|
||||||
|
# wrangler secret put CLERK_SECRET_KEY
|
||||||
|
#
|
||||||
|
# 2. Set publishable key in wrangler.jsonc:
|
||||||
|
# {
|
||||||
|
# "vars": {
|
||||||
|
# "CLERK_PUBLISHABLE_KEY": "pk_live_..."
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SECURITY NOTES
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. NEVER commit .dev.vars to version control
|
||||||
|
# .dev.vars is in .gitignore by default
|
||||||
|
#
|
||||||
|
# 2. Use different keys for development and production
|
||||||
|
# - Development: pk_test_... / sk_test_...
|
||||||
|
# - Production: pk_live_... / sk_live_...
|
||||||
|
#
|
||||||
|
# 3. Production secrets via wrangler secret put
|
||||||
|
# This encrypts secrets, they won't appear in wrangler.jsonc
|
||||||
|
#
|
||||||
|
# 4. Rotate CLERK_SECRET_KEY if compromised
|
||||||
|
# Generate new keys in Clerk Dashboard
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Additional Bindings
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# If your Worker uses other services, add them here:
|
||||||
|
# DATABASE_URL=...
|
||||||
|
# API_KEY=...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# REFERENCE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Official Docs:
|
||||||
|
# https://clerk.com/docs/reference/backend/verify-token
|
||||||
|
# https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
89
templates/env-examples/.env.local.example
Normal file
89
templates/env-examples/.env.local.example
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Clerk Environment Variables for Next.js
|
||||||
|
#
|
||||||
|
# Copy this file to .env.local and fill in your actual values
|
||||||
|
# Get your keys from https://dashboard.clerk.com
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# REQUIRED
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Publishable Key (safe to expose to client)
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
|
||||||
|
# Secret Key (NEVER expose to client, server-side only)
|
||||||
|
CLERK_SECRET_KEY=sk_test_...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Custom Pages
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Uncomment to use custom sign-in/sign-up pages
|
||||||
|
# NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||||
|
# NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Redirect URLs
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Where to redirect after sign-in (forced - always goes here)
|
||||||
|
# NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
||||||
|
|
||||||
|
# Where to redirect after sign-up (forced - always goes here)
|
||||||
|
# NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
|
||||||
|
|
||||||
|
# Fallback redirect if no forced redirect is set (default: /)
|
||||||
|
# NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
|
||||||
|
# NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Webhooks
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Webhook signing secret for verifying Clerk webhooks
|
||||||
|
# Get this from Clerk Dashboard > Webhooks > Add Endpoint
|
||||||
|
# CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Multi-Domain (Satellite Domains)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# For multi-domain authentication
|
||||||
|
# NEXT_PUBLIC_CLERK_DOMAIN=accounts.yourdomain.com
|
||||||
|
# NEXT_PUBLIC_CLERK_IS_SATELLITE=true
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# OPTIONAL - Advanced Configuration
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Custom Clerk JS URL (usually not needed)
|
||||||
|
# NEXT_PUBLIC_CLERK_JS_URL=https://...
|
||||||
|
|
||||||
|
# Proxy URL for requests (enterprise feature)
|
||||||
|
# NEXT_PUBLIC_CLERK_PROXY_URL=https://...
|
||||||
|
|
||||||
|
# Disable telemetry
|
||||||
|
# CLERK_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SECURITY NOTES
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. NEVER commit .env.local to version control
|
||||||
|
# Add .env.local to .gitignore
|
||||||
|
#
|
||||||
|
# 2. Use different keys for development and production
|
||||||
|
# - Development: pk_test_... / sk_test_...
|
||||||
|
# - Production: pk_live_... / sk_live_...
|
||||||
|
#
|
||||||
|
# 3. NEVER use NEXT_PUBLIC_ prefix for secrets
|
||||||
|
# NEXT_PUBLIC_ variables are exposed to the browser
|
||||||
|
#
|
||||||
|
# 4. Rotate CLERK_SECRET_KEY if compromised
|
||||||
|
# Generate new keys in Clerk Dashboard
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# REFERENCE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Official Docs:
|
||||||
|
# https://clerk.com/docs/guides/development/clerk-environment-variables
|
||||||
46
templates/env-examples/.env.local.vite.example
Normal file
46
templates/env-examples/.env.local.vite.example
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Clerk Environment Variables for React + Vite
|
||||||
|
#
|
||||||
|
# Copy this file to .env.local and fill in your actual values
|
||||||
|
# Get your keys from https://dashboard.clerk.com
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# REQUIRED
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Publishable Key (safe to expose to client)
|
||||||
|
# CRITICAL: Must use VITE_ prefix for Vite to expose to client
|
||||||
|
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SECURITY NOTES
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. NEVER commit .env.local to version control
|
||||||
|
# Add .env.local to .gitignore
|
||||||
|
#
|
||||||
|
# 2. Must use VITE_ prefix for client-side variables
|
||||||
|
# Without VITE_ prefix, variable won't be available
|
||||||
|
#
|
||||||
|
# 3. Only VITE_ prefixed vars are exposed to browser
|
||||||
|
# Never use VITE_ prefix for secrets
|
||||||
|
#
|
||||||
|
# 4. Restart dev server after changing .env.local
|
||||||
|
# Vite only reads env vars on startup
|
||||||
|
#
|
||||||
|
# 5. Use different keys for development and production
|
||||||
|
# - Development: pk_test_...
|
||||||
|
# - Production: pk_live_...
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ACCESS IN CODE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Use import.meta.env to access:
|
||||||
|
# const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# REFERENCE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Vite Env Vars: https://vitejs.dev/guide/env-and-mode.html
|
||||||
|
# Clerk Docs: https://clerk.com/docs/references/react/clerk-provider
|
||||||
23
templates/jwt/advanced-template.json
Normal file
23
templates/jwt/advanced-template.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$comment": "Advanced Clerk JWT Template - Multi-Tenant with Fallbacks",
|
||||||
|
"$description": "This template demonstrates advanced features: string interpolation, conditional expressions, nested metadata access, and organization claims. Copy this JSON (without $ prefixed fields) into Clerk Dashboard.",
|
||||||
|
|
||||||
|
"user_id": "{{user.id}}",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"full_name": "{{user.last_name}} {{user.first_name}}",
|
||||||
|
"avatar": "{{user.image_url}}",
|
||||||
|
|
||||||
|
"role": "{{user.public_metadata.role || 'user'}}",
|
||||||
|
"department": "{{user.public_metadata.department || 'general'}}",
|
||||||
|
"permissions": "{{user.public_metadata.permissions}}",
|
||||||
|
|
||||||
|
"org_id": "{{user.public_metadata.org_id}}",
|
||||||
|
"org_slug": "{{user.public_metadata.org_slug}}",
|
||||||
|
"org_role": "{{user.public_metadata.org_role}}",
|
||||||
|
|
||||||
|
"interests": "{{user.public_metadata.profile.interests}}",
|
||||||
|
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
|
||||||
|
|
||||||
|
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}",
|
||||||
|
"onboarding_complete": "{{user.public_metadata.onboardingComplete || false}}"
|
||||||
|
}
|
||||||
8
templates/jwt/basic-template.json
Normal file
8
templates/jwt/basic-template.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$comment": "Basic Clerk JWT Template - Role-Based Access Control",
|
||||||
|
"$description": "This template includes minimal user information for role-based authentication. Copy this JSON (without comments) into Clerk Dashboard > Sessions > Customize session token > Create template.",
|
||||||
|
|
||||||
|
"user_id": "{{user.id}}",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"role": "{{user.public_metadata.role || 'user'}}"
|
||||||
|
}
|
||||||
14
templates/jwt/grafbase-template.json
Normal file
14
templates/jwt/grafbase-template.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$comment": "Grafbase GraphQL Integration JWT Template",
|
||||||
|
"$description": "This template is for Grafbase integration with role-based access control. Grafbase uses 'groups' array for authorization. Name it 'grafbase' in Clerk Dashboard.",
|
||||||
|
"$usage": "const token = await getToken({ template: 'grafbase' }); // Use in GraphQL requests",
|
||||||
|
"$grafbase_config": "In grafbase.toml: [auth.providers.clerk] issuer = 'https://your-app.clerk.accounts.dev' jwks = 'https://your-app.clerk.accounts.dev/.well-known/jwks.json'",
|
||||||
|
|
||||||
|
"sub": "{{user.id}}",
|
||||||
|
"groups": [
|
||||||
|
"org:{{user.public_metadata.org_role || 'member'}}",
|
||||||
|
"user:authenticated"
|
||||||
|
],
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"name": "{{user.full_name || user.first_name}}"
|
||||||
|
}
|
||||||
17
templates/jwt/supabase-template.json
Normal file
17
templates/jwt/supabase-template.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$comment": "Supabase Integration JWT Template",
|
||||||
|
"$description": "This template is designed for Supabase integration. Name it 'supabase' in Clerk Dashboard. Use with getToken({ template: 'supabase' }) to authenticate Supabase client.",
|
||||||
|
"$usage": "const token = await getToken({ template: 'supabase' }); const supabase = createClient(url, key, { global: { headers: { Authorization: `Bearer ${token}` } } });",
|
||||||
|
|
||||||
|
"aud": "authenticated",
|
||||||
|
"email": "{{user.primary_email_address}}",
|
||||||
|
"app_metadata": {
|
||||||
|
"provider": "clerk",
|
||||||
|
"providers": ["clerk"]
|
||||||
|
},
|
||||||
|
"user_metadata": {
|
||||||
|
"full_name": "{{user.full_name || user.first_name || 'User'}}",
|
||||||
|
"avatar_url": "{{user.image_url}}",
|
||||||
|
"email": "{{user.primary_email_address}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
101
templates/nextjs/app-layout.tsx
Normal file
101
templates/nextjs/app-layout.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Next.js App Router Layout with Clerk
|
||||||
|
*
|
||||||
|
* Place this in app/layout.tsx
|
||||||
|
*
|
||||||
|
* Dependencies:
|
||||||
|
* - @clerk/nextjs@^6.33.3
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ClerkProvider } from '@clerk/nextjs'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'My App',
|
||||||
|
description: 'Authenticated with Clerk',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ClerkProvider>
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
</ClerkProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With Dark Mode Support (using next-themes):
|
||||||
|
*
|
||||||
|
* 1. Install: npm install next-themes
|
||||||
|
* 2. Use this pattern:
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { ClerkProvider } from '@clerk/nextjs'
|
||||||
|
import { ThemeProvider } from 'next-themes'
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ClerkProvider>
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</ClerkProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With Clerk Appearance Customization:
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { ClerkProvider } from '@clerk/nextjs'
|
||||||
|
import { dark } from '@clerk/themes'
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ClerkProvider
|
||||||
|
appearance={{
|
||||||
|
baseTheme: dark,
|
||||||
|
variables: {
|
||||||
|
colorPrimary: '#3b82f6',
|
||||||
|
colorBackground: '#0f172a',
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
|
||||||
|
card: 'shadow-xl',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
</ClerkProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
143
templates/nextjs/middleware.ts
Normal file
143
templates/nextjs/middleware.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Next.js Middleware with Clerk Authentication
|
||||||
|
*
|
||||||
|
* This middleware protects routes using Clerk's clerkMiddleware.
|
||||||
|
* Place this file in the root of your Next.js project.
|
||||||
|
*
|
||||||
|
* Dependencies:
|
||||||
|
* - @clerk/nextjs@^6.33.3
|
||||||
|
*
|
||||||
|
* CRITICAL (v6 Breaking Change):
|
||||||
|
* - auth.protect() is now async - must use await
|
||||||
|
* - Source: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define public routes (routes that don't require authentication)
|
||||||
|
*
|
||||||
|
* Glob patterns supported:
|
||||||
|
* - '/path' - exact match
|
||||||
|
* - '/path(.*)' - path and all sub-paths
|
||||||
|
* - '/api/public/*' - wildcard
|
||||||
|
*/
|
||||||
|
const isPublicRoute = createRouteMatcher([
|
||||||
|
'/', // Homepage
|
||||||
|
'/sign-in(.*)', // Sign-in page and sub-paths
|
||||||
|
'/sign-up(.*)', // Sign-up page and sub-paths
|
||||||
|
'/api/public(.*)', // Public API routes
|
||||||
|
'/api/webhooks(.*)', // Webhook endpoints
|
||||||
|
'/about', // Static pages
|
||||||
|
'/pricing',
|
||||||
|
'/contact',
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative: Define protected routes instead
|
||||||
|
*
|
||||||
|
* Uncomment this pattern if you prefer to explicitly protect
|
||||||
|
* specific routes rather than inverting the logic:
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
const isProtectedRoute = createRouteMatcher([
|
||||||
|
'/dashboard(.*)',
|
||||||
|
'/profile(.*)',
|
||||||
|
'/admin(.*)',
|
||||||
|
'/api/private(.*)',
|
||||||
|
])
|
||||||
|
|
||||||
|
export default clerkMiddleware(async (auth, request) => {
|
||||||
|
if (isProtectedRoute(request)) {
|
||||||
|
await auth.protect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Pattern: Protect all routes except public ones
|
||||||
|
*
|
||||||
|
* CRITICAL:
|
||||||
|
* - auth.protect() MUST be awaited (async in v6)
|
||||||
|
* - Without await, route protection will not work
|
||||||
|
*/
|
||||||
|
export default clerkMiddleware(async (auth, request) => {
|
||||||
|
if (!isPublicRoute(request)) {
|
||||||
|
await auth.protect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher Configuration
|
||||||
|
*
|
||||||
|
* Defines which paths run middleware.
|
||||||
|
* This is the recommended configuration from Clerk.
|
||||||
|
*/
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Skip Next.js internals and static files
|
||||||
|
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||||
|
|
||||||
|
// Always run for API routes
|
||||||
|
'/(api|trpc)(.*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced: Role-Based Protection
|
||||||
|
*
|
||||||
|
* Protect routes based on user role or organization membership:
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
|
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
|
||||||
|
const isOrgRoute = createRouteMatcher(['/org(.*)'])
|
||||||
|
|
||||||
|
export default clerkMiddleware(async (auth, request) => {
|
||||||
|
// Admin routes require 'admin' role
|
||||||
|
if (isAdminRoute(request)) {
|
||||||
|
await auth.protect((has) => {
|
||||||
|
return has({ role: 'admin' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization routes require organization membership
|
||||||
|
if (isOrgRoute(request)) {
|
||||||
|
await auth.protect((has) => {
|
||||||
|
return has({ permission: 'org:member' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other routes use default protection
|
||||||
|
if (!isPublicRoute(request)) {
|
||||||
|
await auth.protect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||||
|
'/(api|trpc)(.*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Troubleshooting:
|
||||||
|
*
|
||||||
|
* 1. Routes not protected?
|
||||||
|
* - Ensure auth.protect() is awaited
|
||||||
|
* - Check matcher configuration includes your routes
|
||||||
|
* - Verify middleware.ts is in project root
|
||||||
|
*
|
||||||
|
* 2. Infinite redirects?
|
||||||
|
* - Ensure sign-in/sign-up routes are in isPublicRoute
|
||||||
|
* - Check NEXT_PUBLIC_CLERK_SIGN_IN_URL in .env.local
|
||||||
|
*
|
||||||
|
* 3. API routes returning HTML?
|
||||||
|
* - Verify '/(api|trpc)(.*)' is in matcher
|
||||||
|
* - Check API routes are not in isPublicRoute if protected
|
||||||
|
*
|
||||||
|
* Official Docs: https://clerk.com/docs/reference/nextjs/clerk-middleware
|
||||||
|
*/
|
||||||
114
templates/nextjs/server-component-example.tsx
Normal file
114
templates/nextjs/server-component-example.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Server Component with Clerk Auth
|
||||||
|
*
|
||||||
|
* Demonstrates using auth() and currentUser() in Server Components
|
||||||
|
*
|
||||||
|
* CRITICAL (v6): auth() is now async - must use await
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export default async function DashboardPage() {
|
||||||
|
/**
|
||||||
|
* Option 1: Lightweight auth check
|
||||||
|
*
|
||||||
|
* Use auth() when you only need userId/sessionId
|
||||||
|
* This is faster than currentUser()
|
||||||
|
*/
|
||||||
|
const { userId, sessionId } = await auth()
|
||||||
|
|
||||||
|
// Redirect if not authenticated (shouldn't happen if middleware configured)
|
||||||
|
if (!userId) {
|
||||||
|
redirect('/sign-in')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option 2: Full user object
|
||||||
|
*
|
||||||
|
* Use currentUser() when you need full user data
|
||||||
|
* Heavier than auth(), so use sparingly
|
||||||
|
*/
|
||||||
|
const user = await currentUser()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-8">
|
||||||
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<p>
|
||||||
|
<strong>User ID:</strong> {userId}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Session ID:</strong> {sessionId}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Email:</strong>{' '}
|
||||||
|
{user?.primaryEmailAddress?.emailAddress}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Name:</strong> {user?.firstName} {user?.lastName}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Access public metadata */}
|
||||||
|
{user?.publicMetadata && (
|
||||||
|
<div>
|
||||||
|
<strong>Role:</strong>{' '}
|
||||||
|
{(user.publicMetadata as any).role || 'user'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Route Example (app/api/user/route.ts)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const { userId } = await auth()
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await currentUser()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
userId,
|
||||||
|
email: user?.primaryEmailAddress?.emailAddress,
|
||||||
|
name: `${user?.firstName} ${user?.lastName}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected API Route with POST (app/api/items/route.ts)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { auth } from '@clerk/nextjs/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { userId } = await auth()
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
// Validate and process
|
||||||
|
// Example: save to database with userId
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
itemId: crypto.randomUUID(),
|
||||||
|
userId,
|
||||||
|
}, { status: 201 })
|
||||||
|
}
|
||||||
|
*/
|
||||||
215
templates/react/App.tsx
Normal file
215
templates/react/App.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* React App Component with Clerk Hooks
|
||||||
|
*
|
||||||
|
* Demonstrates:
|
||||||
|
* - useUser() for user data
|
||||||
|
* - useAuth() for session tokens
|
||||||
|
* - useClerk() for auth methods
|
||||||
|
* - Proper loading state handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useUser, useAuth, useClerk, SignInButton, UserButton } from '@clerk/clerk-react'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// Get user object (includes email, metadata, etc.)
|
||||||
|
const { isLoaded, isSignedIn, user } = useUser()
|
||||||
|
|
||||||
|
// Get auth state and session methods
|
||||||
|
const { userId, getToken } = useAuth()
|
||||||
|
|
||||||
|
// Get Clerk instance for advanced operations
|
||||||
|
const { openSignIn, signOut } = useClerk()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL: Always check isLoaded before rendering
|
||||||
|
*
|
||||||
|
* Why: Prevents flash of wrong content while Clerk initializes
|
||||||
|
* Source: https://clerk.com/docs/references/react/use-user
|
||||||
|
*/
|
||||||
|
if (!isLoaded) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-lg">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unauthenticated View
|
||||||
|
*/
|
||||||
|
if (!isSignedIn) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
||||||
|
<h1 className="text-4xl font-bold">Welcome</h1>
|
||||||
|
<p className="text-gray-600">Sign in to continue</p>
|
||||||
|
|
||||||
|
{/* Option 1: Clerk's pre-built button */}
|
||||||
|
<SignInButton mode="modal">
|
||||||
|
<button className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</SignInButton>
|
||||||
|
|
||||||
|
{/* Option 2: Custom button with openSignIn() */}
|
||||||
|
{/* <button
|
||||||
|
onClick={() => openSignIn()}
|
||||||
|
className="px-6 py-2 bg-blue-500 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticated View
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-8">
|
||||||
|
<header className="flex items-center justify-between mb-8">
|
||||||
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||||
|
|
||||||
|
{/* Clerk's pre-built user button (profile + sign out) */}
|
||||||
|
<UserButton afterSignOutUrl="/" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">User Information</h2>
|
||||||
|
|
||||||
|
<dl className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">User ID</dt>
|
||||||
|
<dd className="text-gray-900">{userId}</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">Email</dt>
|
||||||
|
<dd className="text-gray-900">
|
||||||
|
{user.primaryEmailAddress?.emailAddress}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">Name</dt>
|
||||||
|
<dd className="text-gray-900">
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Access public metadata */}
|
||||||
|
{user.publicMetadata && Object.keys(user.publicMetadata).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-700">Metadata</dt>
|
||||||
|
<dd className="text-gray-900">
|
||||||
|
<pre className="text-sm bg-gray-100 p-2 rounded">
|
||||||
|
{JSON.stringify(user.publicMetadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Example: Call protected API */}
|
||||||
|
<ProtectedAPIExample getToken={getToken} />
|
||||||
|
|
||||||
|
{/* Custom sign out button */}
|
||||||
|
<button
|
||||||
|
onClick={() => signOut()}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example Component: Calling Protected API
|
||||||
|
*/
|
||||||
|
function ProtectedAPIExample({ getToken }: { getToken: () => Promise<string | null> }) {
|
||||||
|
const [data, setData] = React.useState<any>(null)
|
||||||
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
const [error, setError] = React.useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchProtectedData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get fresh session token (auto-refreshes)
|
||||||
|
const token = await getToken()
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No session token available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call your API with Authorization header
|
||||||
|
const response = await fetch('https://your-worker.workers.dev/api/protected', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
setData(result)
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Protected API Call</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={fetchProtectedData}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Fetch Protected Data'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-4 bg-red-50 text-red-700 rounded">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto">
|
||||||
|
{JSON.stringify(data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Troubleshooting:
|
||||||
|
*
|
||||||
|
* 1. "Missing Publishable Key" error?
|
||||||
|
* - Check .env.local has VITE_CLERK_PUBLISHABLE_KEY
|
||||||
|
* - Restart dev server after adding env var
|
||||||
|
*
|
||||||
|
* 2. Flash of unauthenticated content?
|
||||||
|
* - Always check isLoaded before rendering
|
||||||
|
* - Show loading state while isLoaded is false
|
||||||
|
*
|
||||||
|
* 3. Token not working with API?
|
||||||
|
* - Ensure getToken() is called fresh (don't cache)
|
||||||
|
* - Check Authorization header format: "Bearer <token>"
|
||||||
|
* - Verify API is using @clerk/backend to verify token
|
||||||
|
*/
|
||||||
66
templates/react/main.tsx
Normal file
66
templates/react/main.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* React + Vite Entry Point with Clerk
|
||||||
|
*
|
||||||
|
* Place this in src/main.tsx
|
||||||
|
*
|
||||||
|
* Dependencies:
|
||||||
|
* - @clerk/clerk-react@^5.51.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { ClerkProvider } from '@clerk/clerk-react'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
// Get publishable key from environment
|
||||||
|
// CRITICAL: Must use VITE_ prefix for Vite to expose to client
|
||||||
|
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
|
||||||
|
|
||||||
|
if (!PUBLISHABLE_KEY) {
|
||||||
|
throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY in .env.local')
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
|
||||||
|
<App />
|
||||||
|
</ClerkProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With Dark Mode Support (using custom theme):
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
import { ClerkProvider } from '@clerk/clerk-react'
|
||||||
|
import { dark } from '@clerk/themes'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ClerkProvider
|
||||||
|
publishableKey={PUBLISHABLE_KEY}
|
||||||
|
appearance={{
|
||||||
|
baseTheme: dark,
|
||||||
|
variables: {
|
||||||
|
colorPrimary: '#3b82f6',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<App />
|
||||||
|
</ClerkProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment Variables:
|
||||||
|
*
|
||||||
|
* Create .env.local with:
|
||||||
|
* VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||||
|
*
|
||||||
|
* CRITICAL:
|
||||||
|
* - Must use VITE_ prefix (Vite requirement)
|
||||||
|
* - Never commit .env.local to version control
|
||||||
|
* - Use different keys for development and production
|
||||||
|
*/
|
||||||
125
templates/typescript/custom-jwt-types.d.ts
vendored
Normal file
125
templates/typescript/custom-jwt-types.d.ts
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Custom JWT Session Claims Type Definitions
|
||||||
|
*
|
||||||
|
* This file provides TypeScript type safety for custom JWT claims in Clerk.
|
||||||
|
* Place this in your project's types/ directory (e.g., types/globals.d.ts).
|
||||||
|
*
|
||||||
|
* After adding this, sessionClaims will have auto-complete and type checking
|
||||||
|
* for your custom claims.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```typescript
|
||||||
|
* import { auth } from '@clerk/nextjs/server'
|
||||||
|
*
|
||||||
|
* const { sessionClaims } = await auth()
|
||||||
|
* const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* Extend Clerk's CustomJwtSessionClaims interface with your custom claims.
|
||||||
|
*
|
||||||
|
* IMPORTANT: The structure must match your JWT template exactly.
|
||||||
|
*/
|
||||||
|
interface CustomJwtSessionClaims {
|
||||||
|
/**
|
||||||
|
* Custom metadata claims
|
||||||
|
*/
|
||||||
|
metadata: {
|
||||||
|
/**
|
||||||
|
* User's role in the application
|
||||||
|
* Maps to: {{user.public_metadata.role}}
|
||||||
|
*/
|
||||||
|
role?: 'admin' | 'moderator' | 'user'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether user has completed onboarding
|
||||||
|
* Maps to: {{user.public_metadata.onboardingComplete}}
|
||||||
|
*/
|
||||||
|
onboardingComplete?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's department
|
||||||
|
* Maps to: {{user.public_metadata.department}}
|
||||||
|
*/
|
||||||
|
department?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's permissions array
|
||||||
|
* Maps to: {{user.public_metadata.permissions}}
|
||||||
|
*/
|
||||||
|
permissions?: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization ID for multi-tenant apps
|
||||||
|
* Maps to: {{user.public_metadata.org_id}}
|
||||||
|
*/
|
||||||
|
organizationId?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization slug for multi-tenant apps
|
||||||
|
* Maps to: {{user.public_metadata.org_slug}}
|
||||||
|
*/
|
||||||
|
organizationSlug?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's role in organization
|
||||||
|
* Maps to: {{user.public_metadata.org_role}}
|
||||||
|
*/
|
||||||
|
organizationRole?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's email address (if included in template)
|
||||||
|
* Maps to: {{user.primary_email_address}}
|
||||||
|
*/
|
||||||
|
email?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's full name (if included in template)
|
||||||
|
* Maps to: {{user.full_name}}
|
||||||
|
*/
|
||||||
|
full_name?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User ID (if included in template)
|
||||||
|
* Maps to: {{user.id}}
|
||||||
|
* Note: Also available as 'sub' in default claims
|
||||||
|
*/
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example Usage in Next.js Server Component:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* import { auth } from '@clerk/nextjs/server'
|
||||||
|
*
|
||||||
|
* export default async function AdminPage() {
|
||||||
|
* const { sessionClaims } = await auth()
|
||||||
|
*
|
||||||
|
* // TypeScript knows about these properties now
|
||||||
|
* if (sessionClaims?.metadata?.role !== 'admin') {
|
||||||
|
* return <div>Unauthorized</div>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return <div>Admin Dashboard</div>
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Example Usage in Cloudflare Workers:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* import { verifyToken } from '@clerk/backend'
|
||||||
|
*
|
||||||
|
* const { data } = await verifyToken(token, { secretKey })
|
||||||
|
*
|
||||||
|
* // Access custom claims with type safety
|
||||||
|
* const role = data.metadata?.role
|
||||||
|
* const isAdmin = role === 'admin'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
42
templates/vite/package.json
Normal file
42
templates/vite/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$comment": "Vite + Clerk: package.json with increased header size limit",
|
||||||
|
"$description": "This template shows how to configure Vite dev server to handle Clerk's authentication handshake tokens, which can exceed the default 8KB Node.js header limit when using custom JWT claims.",
|
||||||
|
|
||||||
|
"name": "vite-clerk-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
|
||||||
|
"scripts": {
|
||||||
|
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
|
||||||
|
"dependencies": {
|
||||||
|
"@clerk/clerk-react": "^5.51.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
},
|
||||||
|
|
||||||
|
"$notes": [
|
||||||
|
"The key change is in the 'dev' script:",
|
||||||
|
"NODE_OPTIONS='--max-http-header-size=32768' increases the limit from 8KB to 32KB",
|
||||||
|
"",
|
||||||
|
"For Windows PowerShell, use:",
|
||||||
|
"\"dev\": \"cross-env NODE_OPTIONS=--max-http-header-size=32768 vite\"",
|
||||||
|
"And install: npm install -D cross-env",
|
||||||
|
"",
|
||||||
|
"This prevents '431 Request Header Fields Too Large' errors when:",
|
||||||
|
"- Testing Clerk authentication in development mode",
|
||||||
|
"- Using custom JWT claims that increase token size",
|
||||||
|
"- Clerk's __clerk_handshake parameter exceeds header limit",
|
||||||
|
"",
|
||||||
|
"Production deployments (Cloudflare, Vercel, Netlify) don't need this fix."
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user