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

17 KiB

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)
  2. User Property Shortcodes
  3. Organization Claims
  4. Metadata Access
  5. Advanced Template Features
  6. Creating JWT Templates
  7. TypeScript Type Safety
  8. Common Use Cases
  9. Limitations & Gotchas
  10. Official Documentation

Default Claims (Auto-Included)

Every JWT generated by Clerk automatically includes these claims. These cannot be overridden in custom templates.

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "all_public": "{{user.public_metadata}}",
  "all_unsafe": "{{user.unsafe_metadata}}"
}

Result:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "full_name": "{{user.last_name}} {{user.first_name}}",
  "greeting": "Hello, {{user.first_name}}!",
  "email_with_name": "{{user.full_name}} <{{user.primary_email_address}}>"
}

Result:

{
  "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:

{
  "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:

{
  "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

{
  "has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
  "is_complete": "{{user.public_metadata.profileComplete || false}}"
}

Result:

{
  "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:

{
  "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:

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:

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:

export {}

declare global {
  interface CustomJwtSessionClaims {
    metadata: {
      role?: 'admin' | 'moderator' | 'user'
      onboardingComplete?: boolean
      department?: string
      organizationId?: string
    }
  }
}

Usage:

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:

{
  "email": "{{user.primary_email_address}}",
  "role": "{{user.public_metadata.role || 'user'}}",
  "permissions": "{{user.public_metadata.permissions}}"
}

Backend Verification:

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:

{
  "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:

// 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):

{
  "email": "{{user.primary_email_address}}",
  "app_metadata": {
    "provider": "clerk"
  },
  "user_metadata": {
    "full_name": "{{user.full_name}}",
    "avatar_url": "{{user.image_url}}"
  }
}

Usage:

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):

{
  "sub": "{{user.id}}",
  "groups": ["org:admin", "org:member"]
}

Grafbase Configuration:

[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):

{
  "email": "{{user.primary_email_address}}",
  "bio": "{{user.public_metadata.bio}}",  // 6KB bio field
  "all_metadata": "{{user.public_metadata}}"  // Entire object
}

Good Example (Under limit):

{
  "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

{
  "valid": "{{user.first_name}}",
  "invalid": "{{user.i_dont_exist}}"
}

Result:

{
  "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


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