Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user