How to fix auth/invalid-claims: Custom claim attributes are malformed in Firebase

FirebaseINTERMEDIATEMEDIUMUpdated Dec 22, 2025· 133 views

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.

Quick fix

Fixes auth/invalid-claims: Custom claim attributes are malformed

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
});
  1. Verify custom claims contain only JSON-serializable types
  2. Ensure payload size is under 1000 bytes
  3. Avoid reserved claim names
  4. Add error handling and validation
firebase — error
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
});
auth/invalid-claims: Custom claim attributes are malformed

What this error means

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.

Typical symptoms

  • Error "auth/invalid-claims: Custom claim attributes are malformed" when calling setCustomUserClaims()
  • Custom claims not appearing in user ID tokens after attempted update
  • Authentication operations failing during user role assignment
  • Cloud Functions or backend services throwing validation errors when setting user permissions

Common causes

  1. Passing non-JSON-serializable types like Date objects, functions, undefined, or class instances
  2. Custom claims payload exceeding the 1000-byte size limit
  3. Including reserved OIDC claim names like "iss", "aud", "auth_time", "sub", "iat", or "exp"
  4. Including Firebase reserved names like "user_id" or "firebase" in custom claims
  5. Nested objects containing circular references or non-serializable values
  6. Attempting to set claims with special characters or malformed JSON structure

How to fix "auth/invalid-claims: Custom claim attributes are malformed"

1Verify custom claims contain only JSON-serializable types

Check that your custom claims object contains only valid JSON types: strings, numbers, booleans, arrays, objects, and null.

Incorrect - contains invalid types:

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

typescript
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'
});
2Ensure payload size is under 1000 bytes

Firebase limits custom claims to 1000 bytes. Calculate the size before setting claims to avoid this error.

Check payload size:

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

typescript
// 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)
};
3Avoid reserved claim names

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:

typescript
await getAuth().setCustomUserClaims(uid, {
  role: 'admin',
  iat: Date.now(),        // ❌ Reserved OIDC claim
  firebase: {             // ❌ Reserved by Firebase
    tenant: 'main'
  }
});

Correct - uses custom names:

typescript
await getAuth().setCustomUserClaims(uid, {
  role: 'admin',
  assignedAt: Date.now(),      // ✅ Custom name
  appTenant: 'main',           // ✅ Custom name
  customPermissions: ['read']  // ✅ Custom name
});
4Add error handling and validation

Implement proper error handling and validation before setting custom claims.

Complete example with validation:

typescript
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'
});
5Refresh user tokens after updating claims

After successfully setting custom claims, users need to refresh their ID tokens to access the new claims.

Server-side (Admin SDK):

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

typescript
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.

Advanced notes

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:

typescript
// 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: 'jane@example.com' },
  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:

typescript
// 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.

Frequently asked questions

What does "auth/invalid-claims: Custom claim attributes are malformed" mean?
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.
How do I fix "auth/invalid-claims: Custom claim attributes are malformed"?
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.
What causes "auth/invalid-claims: Custom claim attributes are malformed"?
Passing non-JSON-serializable types like Date objects, functions, undefined, or class instances Custom claims payload exceeding the 1000-byte size limit Including reserved OIDC claim names like "iss", "aud", "auth_time", "sub", "iat", or "exp"
Is the "auth/invalid-claims: Custom claim attributes are malformed" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading