Firebase Testing with Multi-Tenancy

1,333 views
Skip to first unread message

Martin Dennemark

unread,
Jan 21, 2021, 6:27:03 AM1/21/21
to Firebase Google Group
Hi,
I am tryting to get multi-tenancy to work within firebase emulator suite.
I can use the tenant manager, but it seems when I try to call it within a local cloud function, it leads to a dead end with a 404 error.
In my cloud function I want to change a custom claim of another user, but want to make sure the user is part of the same tenant as the one who is calling the function. I can give the calling user a tenantId (through initializeTestApp), but within the cloud function I need to call admin.auth().tenantManager().authForTenant(tenantId) to be able to find a user by mail. And it seems that this part is not implemented into the emulation suite. Am I mistaken?
What would be a good workaround? 

I am wondering if it is easier at the end to just implement tenancy through custom claims. But it would be preferable to use the Google Identity Platform implementation..

Best,
Martin

michael griffith

unread,
Jan 21, 2021, 9:51:51 AM1/21/21
to fireba...@googlegroups.com
Martin, 

I'd love to know more about how you're setting this up.  We tried to use the google multi-tenancy feature for our app thinking that when a tenant added data to the cloud firestore, it would only be readable/writable by someone in that tenacy.  It didn't end up being the case and we implemented our own tenancy capabilities using an org attribute that we're storing on each user's token via custom claim.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/a4499e30-9027-4764-af38-c8267b8758e3n%40googlegroups.com.

Martin D.

unread,
Jan 21, 2021, 10:20:09 AM1/21/21
to Firebase Google Group
Hey,
actually that seems to be exactly the same case for me. I am also close to implementing my own tenancy through custom claims.

Besides not being able to use the admin.auth().tenantManager features, such as authForTenant, I also was not allowed to set clientapp.auth().tenantId  . However, through firebase/rules-unit-testing I can initialize an app through initializeTestApp({projectId: id, auth: {uid: ..., tenantId: ..., customToken:...}). In this way I can test firestore security rules for tenancy. However, cloud functions do not work. First of all because tenantManager does not seem to be supported. Furthermore, my test setup was complaining, that an API key was missing. I could get the api key if I would use a normal initializeApp( apiKey:...) setup, but then I would be missing tenantId again. For initializeTestApp I had to go into the node_module and find the position, where the initializeTestApp wraps around initializeApp and extend the auth options with an apiKey: "fakeApiKey", since the options only had databaseName and projectId options.

Hope this gives a better overview..

michael griffith

unread,
Jan 21, 2021, 10:31:31 AM1/21/21
to fireba...@googlegroups.com
seems about right.  For the Firebase engineers paying attention to this, it would be nice to be able to more easily integrate multi-tenancy into an app without jumping through a lot of hoops. Maybe I am missing something but if you set the tenant id and login, I would expect the  firebase engine to logically separate the data storage in both object storage and firecloud storage without extra steps required by the developer. :( 

Hiranya Jayathilaka

unread,
Jan 21, 2021, 8:42:41 PM1/21/21
to fireba...@googlegroups.com
Access control for Firestore and Storage is based on security rules. You can access the tenant ID in your security rules to implement any tenant-aware access control logic:

match /tenants/{tenantId}/posts/{postId} {
  allow read, write: if request.auth.token.firebase.tenant == tenantId;
}



--

Hiranya Jayathilaka | Software Engineer | h...@google.com | 650-203-0128

Martin D.

unread,
Jan 22, 2021, 5:28:28 AM1/22/21
to Firebase Google Group
Hi Hiranya,
thanks for your reply!
I have now seperated my testing logic. I am testing firestore rules with fake auth through initializeTestApp, since I can easily set the tenantId. The tenantId I am using is provided by Google Identity Platform and as far as I know the security rule would be auth.token.tenantId. So this works now.
For the cloud functions I am now trying to follow the unit testing of cloud functions with online connection https://firebase.google.com/docs/functions/unit-testing
So my local test functions communicate with the online auth and therefore tenantManager now seems to work.
Hope I get along with this solution..

wekaso...@gmail.com

unread,
Jan 22, 2021, 5:28:53 AM1/22/21
to Firebase Google Group
I am implementing multi-tenancy through custom claims as I found the documentation on google/firebase auth multi-tenancy was very light.  
I couldn't really find any good examples and it seemed to be in its infancy and I felt like I would run into too many problems.  I may be wrong here.
If you know of any good reading/resources on this I would be very interested.

Martin D.

unread,
Jan 25, 2021, 5:04:58 AM1/25/21
to Firebase Google Group
Hi,
unfortunately I am also missing reading resources. I am currently considering to implement my own tenancy rather in the form of groups, so each user can have multiple groups/tenants. Since the multi-tenancy of google seperates the users by tenant and communication inbetween tenants is not convenient, it does not seem to be the solution I am looking for. I was wondering, if your multi-tenancy approach through custom claims would allow this? I have the feeling, that I should not use custom claims for this groups approach, since it would require a structure like customClaim: { tenantId: { user: true, admin: false}}, so each tenant would need its own id on the user with its roles. The other alternative would be firestore-based roles. If you have any recommendations, I would be happy to hear about them :)

wekaso...@gmail.com

unread,
Jan 25, 2021, 2:48:50 PM1/25/21
to Firebase Google Group
Hi Martin,

I am allowing my users to belong to multiple groups (cross tenancy).  In my case I call my users called "members" and they can belong to multiple tenants (or organisations/clubs).  
It depends how many tenants your user can belong to as to whether custom claims is a good fit. 

I use the following custom claim { memberTenantIds: ["tenant1" , "tenant2", "tenant3"] }.  You get 1000 bytes for the custom claims so believe that would be around 1000 characters which is enough for my purposes.
I will likely limit the number of tenants a user/member can belong to to maybe 10.  I also use { adminTenantIds: ["tenant1" , "tenant2", "tenant3"] }  however I may actually restrict this to 1 tenant per admin.

If you do go this route I am happy to share my firestore rules and tests with you if you think it would be helpful. e.g. 
match /tenants/{tenantId}/newsItems/{newsItem} {
  allow read: if isLoggedIn() && isMemberOfTenant(tenantId);
}
function isMemberOfTenant(tenantId) {
      return tenantId in request.auth.token.memberTenantIds; 
    }

Martin D.

unread,
Jan 26, 2021, 10:04:04 AM1/26/21
to Firebase Google Group
Hey! 
Thanks for the information! This is really helpful!
Currently I am adapting a roles structure from the docs https://firebase.google.com/docs/firestore/solutions/role-based-access
It will require more reads and since it uses a map structure it might restrict the amount of users to 200 per tenant.
Your example rules look familiar, I just had to use get(resource... ) instead of the token.
To be honest, the customClaim structure seems more sound. It would be very kind of you if you could share your rules and tests with me. I guess I can learn especially from the tests.
 You are assigning the tenants via firebase functions right? 
Best,
Martin 

Martin D.

unread,
Jan 26, 2021, 12:49:41 PM1/26/21
to Firebase Google Group
Btw. I found this structure. It gives a good overview, but has its inconveniences, too, because of denormalization. https://medium.com/firebase-developers/how-to-build-a-team-based-user-management-system-with-firebase-6a9a6e5c740d

John Rodkey

unread,
Jan 28, 2021, 12:04:49 AM1/28/21
to fireba...@googlegroups.com
Here is my take on multi-tenancy within Firestore and using rules to keep you protected.  My example may not work for you but it is to spark ideas.

A good general structure for mutli-tenancy will be 
users_collection
  '2314234': {
    name: 'John Doe',
    active_org: {
      name: 'Test Org',
      id: '13tA03234ze'
    }
    ...more_user_data
  } // end of users_collection

user_tokens // name this whatever you'd like and nobody writes to this collection. LOCK IT DOWN READ_ONLY
  '2314234': {
    org: '13tA03234ze',
    role: 'admin',
    update_time: last_updated_timestamp
  }

orgs_collection
  '13tA03234ze' : {
    name: 'Test Org',
    ...more_org_data_if_needed
    org_users_subcollection { // this isn't technically nested but best example
      '2314234' : {
        org_name: 'Test Org', // saved so user has access when they retrieve all orgs they are members of
        org_id: '13tA03234ze', //save again so user has access to activate org
        name: 'John Doe',
        role: 'admin', // this should only be changed here and by approved users
        status: 'active' // this could be a boolean whatever or perhaps you have a disabled role... 
      }
    }
  }

Now an organization has an easy way to show its list of users and their permissions by calling its sub-collection, and you can gather all of the organizations the user belongs to by doing a collection group query - info.  

But, now we need to address the rules - I generally prefer to keep this data in the Firebase Auth Token and for a few reasons.
  1. I generally need the active_org_id for storage and firestore rules
  2. Lowers my queries on the db as I can store the data there.
Don't store too much data in the custom auth token, I generally prefer to add the following
  org: '13tA03234ze',
  role: 'admin'

To set up the custom claims I use a Firebase Function which listens for changes to the user_tokens collection. Whenever data there changes it will set the new custom claims.

The updates to user_tokens is only done with Firebase Functions - we have a function that listens for users changes and if the user changes their active_org, the function will query the org_users_subcollection and retrieve the data and persist the role & org_id to the user_tokens.  

And, we have another Firebase Function that listens to org_users_subcollection, and if data changes (i.e., role), it will query the user_tokens, and if the org_id  of the org_users_subcollection matches the org_id of the user_tokens we update the user_tokens with the new role value and we are sync. 

And, best of all, you can now also sync your client-side real-time by using a Firestore listener to read user_tokens for the currently authenticated user and if that listener pushes new data (i.e., a role update) you would use the firebase auth client to refresh the id_token by calling user.getIdToken(true) from within your onAuthStateChanged handler for example. 

I think that about covers it from our end... hope it helps a bit.


    

wekaso...@gmail.com

unread,
Jan 28, 2021, 5:21:59 AM1/28/21
to Firebase Google Group
Nice one.  
That is similar to my current structure but with some ideas I was still actively considering.  

1. I assume for your data model that users can only belong to one org at a time?
Due to user_tokens only having a map and not an array of maps.  My model uses an array as users can belong to multiple tenants at the same time.

2.  I had been thinking about doing a readonly collection like that  ( user_tokens ) to update the custom claims with a trigger.  However I wasn't sure about the idea of showing my internal security to any users that can read the data client side, I liked the idea of keeping it hidden.  I realize it is secure though. Firebase actually now have an extension in Alpha that does this for you so I guess they are all for it.  Also it provides transparency as to what is in the custom claims as otherwise they are invisible.  

3.  The real time nature of the user_tokens is a great idea as custom claims take up to an hour to propagate to the client unless you force them to refresh I believe.  Not really an issue for my current project but potentially handy for future projects.

Thanks for that.  

I may put up my model for multi-tenancy with the ability to belong to multiple tenants at the same time once I have worked on it a bit more.

John Rodkey

unread,
Jan 28, 2021, 8:36:46 AM1/28/21
to fireba...@googlegroups.com
Our model fully supports a user being a member of multiple organizations.  We purposely have the user_token specific to the organization they have active.

You'll run into issues if you attempt to add too much data in the user_token, this is a restriction of custom claims not Firestore.

Martin D.

unread,
Jan 28, 2021, 9:24:03 AM1/28/21
to Firebase Google Group
Hi John,
thanks for your contribution! Y our solution seems pretty well thought of! Smart move with the trigger on your readonly collection. The approach seems more scalable in comparsion to wekaso's (thats now how I will call you... ;) )
I tried using collectionGroups for queries, too, but decided that wekaso's approach with a limited number of tenants suits me, too. So my custom claims can carry the tenant list as well as I can denormalize data easily through call of cloud functions to assign roles. Still need to test my rules, but I have a subcollection of members within a tenant tenant/{tid}/members/{mid} which is readonly for members, so only cloud functions can actually change it. Let's see where I will reach my limits with this approach..

John Rodkey

unread,
Jan 28, 2021, 9:42:16 AM1/28/21
to fireba...@googlegroups.com
Happy to help! We Love Firebase and Firestore 

wekaso...@gmail.com

unread,
Jan 28, 2021, 4:22:38 PM1/28/21
to Firebase Google Group
Hi John,

Ah I missed that.  So when a user switches accounts it updates their custom claims realtime to be only for the active organization.
That is a good way to get around the custom claims 1000 byte restriction.
That would also switch the user on all devices to the active organization.
It is a very well thought out good solution.  

For me a user can only belong to up to 10 orgs and they may need to access data from multiple orgs at the same time so I think I will have to stick to the using the array or orgs.
Although this method has limitations to the number of orgs you could belong to.

John Rodkey

unread,
Jan 28, 2021, 5:40:57 PM1/28/21
to fireba...@googlegroups.com
Exactly correct :).  Yes, our users do not have multiple organizations active at once.  They activate their organization and they are set to work within that confine

wekaso...@gmail.com

unread,
Feb 4, 2021, 5:16:17 AM2/4/21
to Firebase Google Group
Hi John,

I implemented your solution for custom claims: 
"To set up the custom claims I use a Firebase Function which listens for changes to the user_tokens collection. Whenever data there changes it will set the new custom claims."

This works well for me except when a new user signs up (their account is created by a cloud function).  It appears the delay of the trigger is a bit long.
So after the account creation when I log the user in and then go to load some data the custom claims have still not updated.
Rather than delaying the login by waiting for the update I just opted to create the custom claims directly on the user in this case while also updating user_tokens to keep it consistant.
It works fine for updating the an existing users permissions though.

Just thought I would add this observation in case anyone else is using this method.
Reply all
Reply to author
Forward
0 new messages