Firebase Admin SDK: Set / Merge Custom User Claims
Asked Answered
K

2

5

Does Firebase have any trick like { merge: true } to set extra/more custom claims without delete/override the old variables?

Step to reproduce:

admin.auth().setCustomUserClaims(uid, { a: 'value' }) // Run this first
admin.auth().setCustomUserClaims(uid, { b: 'value' }) // Then run this after

Result:

{ b: 'value'}

Expected result:

{ a: 'value', b: 'value' }

Or I did something wrong?

Kite answered 9/8, 2019 at 11:29 Comment(0)
K
13

The Firebase documentation for setCustomUserClaims states:

  • customUserClaims: Object
    The developer claims to set. If null is passed, existing custom claims are deleted. Passing a custom claims payload larger than 1000 bytes will throw an error. Custom claims are added to the user's ID token which is transmitted on every authenticated request. For profile non-access related user attributes, use database or other separate storage systems.

It isn't entirely clear from this description, but the statement, "If null is passed, existing custom claims are deleted," provides a hint that the custom claims are completely overwritten with each call to setCustomUserClaims.

Therefore, custom claims need to be set as follows:

claims = {
  a: 'value',
  b: 'value'
}

admin.auth().setCustomUserClaims(uid, claims) 

Workaround: addCustomUserClaims

A helper function could be created to merge in new claims.

async function addCustomUserClaims(uid, claims) {
  const user = await admin.auth().getUser(uid)
  let updated_claims = user.customClaims || {}

  for (let property in claims) {
    if (Object.prototype.hasOwnProperty.call(claims, property)) {
      updated_claims[property] = claims[property]
    }
  }
  await admin.auth().setCustomUserClaims(uid, updated_claims)
}
Khalilahkhalin answered 9/8, 2019 at 11:43 Comment(8)
Great answer @ChristopherPeisert!Disario
@ChristopherPeisert Thanks for a perfect answer!Kite
I think you need user.customClaims || {} if current user dont have any claim beforeKite
Thanks. And since you using let and const so in for() use let also? pleaseKite
Update a bit about eslint got error Do not access Object.prototype method 'hasOwnProperty' from target object no-prototype-builtins maybe we can use if (property in claims) instead? what do you think?Kite
The example code has been updated based on the eslint recommendation for Disallow use of Object.prototypes builtins directly. PREVIOUS: claims.hasOwnProperty(property). REVISED: Object.prototype.hasOwnProperty.call(claims, property)Khalilahkhalin
Perfect! Thanks!Kite
Since it's 3 years+ now, look at note here (it might already improved?) (if yes update the answer when you have time please) firebase.google.com/docs/auth/admin/…Kite
F
5

Christopher Peisert's answer is correct, but it can be done much more cleanly as

admin.auth().getUser(uid).then(({customClaims: oldClaims}) =>
    admin.auth().setCustomUserClaims(uid, { ...oldClaims, b: 'value' }))

If you want to abstract this logic into a function, it can be done as

function addCustomUserClaims(uid, claims) {
    return admin.auth().getUser(uid).then(({customClaims}) =>
        admin.auth().setCustomUserClaims(uid, { ...customClaims, ...claims }))
}

or equivalently* as

const addCustomUserClaims = (uid, claims) => 
    admin.auth().getUser(uid).then(({customClaims}) =>
        admin.auth().setCustomUserClaims(uid, { ...customClaims, ...claims }))
Furriery answered 27/6, 2020 at 23:26 Comment(4)
When you do getUser you get a user object, not customClaims. So you should update the answer to be user.customClaims instead of just customClaims. Also, you should validate the customClaims object because it can be null, so doing { ...customClaims, ...claims } in this case produces an error.Lingual
@HelderEsteves We destructure the customClaims property from user using ({customClaims}). Although there is no user object, this stores user.customClaims in the let-scoped variable customClaims. Also the spread operator {...null} is valid syntax and raises no errors.Furriery
I may be wrong but I think the abstracted function needs to be asyncBarometrograph
@JigarPatel It does not. It does return a Promise (which means it is asynchronous in some sense), but we don't need the async modifier. Adding async does two things 1. Allows you to use the await keyword inside the body 2. Wraps the result in a Promise if it is not already a Promise. Neither of these applies, so it's not necessary, but adding async would not break it either.Furriery

© 2022 - 2024 — McMap. All rights reserved.