Files
gh-jezweb-claude-skills-ski…/references/jwt-claims-guide.md
2025-11-30 08:24:03 +08:00

661 lines
17 KiB
Markdown

# 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