This error occurs when custom claim attributes passed to setCustomUserClaims() contain invalid data types or exceed size limits. Fix it by ensuring claims are JSON-serializable and under 1000 bytes.
This error is thrown by Firebase Admin SDK when you attempt to set custom claims on a user account with invalid data. Custom claims are used to store additional user information that can be accessed in Firebase security rules and ID tokens for authorization purposes. The Firebase Admin SDK validates custom claims to ensure they meet strict requirements: they must be JSON-serializable, cannot exceed 1000 bytes in total size, and cannot use reserved claim names. When any of these validation rules are violated, the auth/invalid-claims error is raised. Custom claims are powerful for implementing role-based access control, but they require careful handling since they're embedded in the user's ID token and can only be set from privileged server environments using the Admin SDK.
Check that your custom claims object contains only valid JSON types: strings, numbers, booleans, arrays, objects, and null.
Incorrect - contains invalid types:
import { getAuth } from 'firebase-admin/auth';
// ❌ These will cause auth/invalid-claims error
await getAuth().setCustomUserClaims(uid, {
role: 'admin',
createdAt: new Date(), // Date object - not JSON-serializable
callback: () => {}, // Function - not allowed
metadata: undefined, // undefined - not JSON-serializable
settings: new Map() // Map object - not serializable
});Correct - only JSON-serializable values:
import { getAuth } from 'firebase-admin/auth';
// ✅ Valid custom claims
await getAuth().setCustomUserClaims(uid, {
role: 'admin',
createdAt: Date.now(), // Number timestamp
permissions: ['read', 'write'], // Array of strings
settings: { // Plain object
theme: 'dark',
notifications: true
},
departmentId: 'eng-001'
});Firebase limits custom claims to 1000 bytes. Calculate the size before setting claims to avoid this error.
Check payload size:
import { getAuth } from 'firebase-admin/auth';
const customClaims = {
role: 'admin',
permissions: ['read', 'write', 'delete'],
departmentId: 'engineering',
teamIds: ['team1', 'team2', 'team3']
};
// Calculate size in bytes
const claimsSize = Buffer.from(JSON.stringify(customClaims)).length;
if (claimsSize > 1000) {
console.error(`Custom claims too large: ${claimsSize} bytes (max 1000)`);
// Reduce the payload - remove less critical data
} else {
await getAuth().setCustomUserClaims(uid, customClaims);
console.log(`Custom claims set successfully (${claimsSize} bytes)`);
}If payload is too large, reduce data:
// Instead of storing full arrays, use IDs or flags
const optimizedClaims = {
role: 'admin', // Keep essential data
deptId: 'eng', // Use shorter keys
perms: 7 // Use bitmask instead of array: 111 (read|write|delete)
};Do not use OIDC standard claims or Firebase reserved names in your custom claims object.
Reserved OIDC claims to avoid:
- iss (issuer)
- aud (audience)
- auth_time (authentication time)
- user_id (user identifier)
- sub (subject)
- iat (issued at)
- exp (expiration time)
- firebase (Firebase namespace)
Incorrect - uses reserved names:
await getAuth().setCustomUserClaims(uid, {
role: 'admin',
iat: Date.now(), // ❌ Reserved OIDC claim
firebase: { // ❌ Reserved by Firebase
tenant: 'main'
}
});Correct - uses custom names:
await getAuth().setCustomUserClaims(uid, {
role: 'admin',
assignedAt: Date.now(), // ✅ Custom name
appTenant: 'main', // ✅ Custom name
customPermissions: ['read'] // ✅ Custom name
});Implement proper error handling and validation before setting custom claims.
Complete example with validation:
import { getAuth } from 'firebase-admin/auth';
interface CustomClaims {
role: string;
permissions: string[];
departmentId?: string;
metadata?: Record<string, string | number | boolean>;
}
async function setUserClaims(uid: string, claims: CustomClaims) {
try {
// Validate claim structure
const validRoles = ['user', 'admin', 'moderator'];
if (!validRoles.includes(claims.role)) {
throw new Error(`Invalid role: ${claims.role}`);
}
// Ensure JSON-serializable
const serialized = JSON.stringify(claims);
const parsed = JSON.parse(serialized);
// Check size
const size = Buffer.from(serialized).length;
if (size > 1000) {
throw new Error(`Claims too large: ${size} bytes (max 1000)`);
}
// Set claims
await getAuth().setCustomUserClaims(uid, claims);
console.log('Custom claims set successfully');
// Force token refresh on client
return { success: true, message: 'Claims updated. User must refresh token.' };
} catch (error: any) {
if (error.code === 'auth/invalid-claims') {
console.error('Custom claim attributes are malformed:', error.message);
return { success: false, error: 'Invalid claim format' };
}
throw error;
}
}
// Usage
await setUserClaims('user123', {
role: 'admin',
permissions: ['read', 'write'],
departmentId: 'engineering'
});After successfully setting custom claims, users need to refresh their ID tokens to access the new claims.
Server-side (Admin SDK):
import { getAuth } from 'firebase-admin/auth';
// Set custom claims
await getAuth().setCustomUserClaims(uid, { role: 'admin' });
// Revoke existing tokens to force refresh
await getAuth().revokeRefreshTokens(uid);
console.log('Claims updated. User must sign in again.');Client-side (force token refresh):
import { getAuth } from 'firebase/auth';
async function refreshUserToken() {
const auth = getAuth();
const user = auth.currentUser;
if (user) {
// Force token refresh
const idToken = await user.getIdToken(true);
// Decode to verify claims (optional)
const decodedToken = await user.getIdTokenResult();
console.log('Custom claims:', decodedToken.claims);
return decodedToken.claims;
}
}Note that it can take up to one hour for custom claims to propagate to existing tokens unless you force a refresh.
Custom Claims vs Firestore for User Data:
Custom claims are stored in the user's ID token, which means they're available immediately in security rules and Cloud Functions without an additional database lookup. However, the 1000-byte limit means they should only contain essential authorization data (roles, permissions, tenant IDs).
For larger user metadata, store it in Firestore or Realtime Database instead:
// Store minimal auth data in custom claims
await getAuth().setCustomUserClaims(uid, {
role: 'admin',
orgId: 'org_123'
});
// Store detailed metadata in Firestore
await db.collection('users').doc(uid).set({
profile: { name: 'Jane', email: '[email protected]' },
preferences: { theme: 'dark', language: 'en' },
teams: ['team1', 'team2', 'team3'],
lastLogin: admin.firestore.FieldValue.serverTimestamp()
});Testing Custom Claims Locally:
When using the Firebase Emulator Suite, custom claims work the same way as in production. You can test claim validation by intentionally passing invalid data:
// In Cloud Functions or backend tests
import { initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
const app = initializeApp();
// This should throw auth/invalid-claims
try {
await getAuth().setCustomUserClaims('test-uid', {
role: 'admin',
created: new Date() // Invalid - Date object
});
} catch (error: any) {
console.log('Expected error:', error.code); // auth/invalid-claims
}Performance Considerations:
Setting custom claims is an administrative operation that writes to Firebase's backend. Avoid calling setCustomUserClaims() on every request or in tight loops. Instead:
1. Set claims only when user roles actually change
2. Cache the result on the client after successful refresh
3. Use Firestore for frequently changing data
4. Batch role updates if processing multiple users
Security Best Practices:
Never expose your Admin SDK credentials to client applications. Custom claims must always be set from:
- Cloud Functions with Admin SDK
- Your own trusted backend server
- Firebase Admin SDK in a secure server environment
Client applications can read custom claims from the decoded ID token but cannot set them.
messaging/UNSPECIFIED_ERROR: No additional information available
How to fix "messaging/UNSPECIFIED_ERROR: No additional information available" in Firebase Cloud Messaging
App Check: reCAPTCHA Score Too Low
App Check reCAPTCHA Score Too Low
storage/invalid-url: Invalid URL format for Cloud Storage reference
How to fix invalid URL format in Firebase Cloud Storage
auth/missing-uid: User ID identifier required
How to fix "auth/missing-uid: User ID identifier required" in Firebase
auth/invalid-argument: Invalid parameter passed to method
How to fix "auth/invalid-argument: Invalid parameter passed to method" in Firebase