Allow single email to signup as multiple users in a Firebase project (Google Identity platform)

Sumit Sapkota
readytowork, Inc.
Published in
7 min readDec 14, 2022
single tenant vs multi-tenant

When using firebase as a default authentication service provider in our project we have noticed that an email can only be used once to signup a user. Most of the time, it’s okay. But when the project’s complexity grows, we might need multiple types of users with a single email address.

Let me explain it with an example,

Suppose, We have a system where we have a web app for admin and a mobile app for users. Admin can post content from the web app and users can browse those content in a mobile app.

And our system should allow registering admin and users with the same email address.

This is where multi-tenancy comes in.

Multi tenancy basically allows to reside different component/tenant within same resource.

Then what is multi-tenancy?

Multi-tenancy is an architecture in which a single instance of a software application serves multiple customers. Each customer is called a tenant. Tenants can be given the ability to customize some parts of the application and restrict from other parts.

GCP provides multi-tenancy features through the Google Identity Platform.

Identity Platform lets you add Google-grade authentication to your apps and services, making it easier to secure user accounts and securely manage credentials.

Multi-tenancy takes this concept one step further. Using tenants, you can create unique silos of users and configurations within a single Identity Platform project. These silos might represent different customers, business units, subsidiaries, or some other division. Multi-tenancy is most commonly used in business-to-business (B2B) apps.

Coming back to where we started

We have to create a system where multiple types of users can signup with a single email in our firebase project with the help of multi-tenancy.

Let’s take a look at an example

We will be creating two tenants namely clients and users. So, what happens when we signup two users with different tenant Id is it creates user according to the tenant Id and it maintains the individual state with the help of separating tenant Id. When the firebase token is generated while logging in, the provided token contains tenant Id in encoded form just like other properties as claims.

Prerequisites:

  • basic knowledge of firebase authentication
  • firebase project with GCP identity platform enabled

GCP Setup

  1. First things first, we need to enable Identity Platform from Google cloud console.

2. After enabling the identity platform, we have to enable the desired provider from the providers section. Here I have enabled Email/Password.

3. Now, let’s create multiple tenants according to our requirements. In this case, I am creating two tenants.

We can add multiple tenants which can consist of the same Tenant name and are separated by TenantID.

The setup related to GCP is almost completed. Now let’s take a look at code implementation.

Code Implementation

Here, all the operations are similar to the firebase user authentication handler. But instead of performing operations with the firebase client manager, we use the firebase client Tenant Manager.

We handle all operations like Create user, update user, delete user, send password reset email, set custom claims with firebase Tenant Manager instead of firebase client manager.

1. Creating user

Let us define a struct called FirebaseService

// FirebaseService structure
type FirebaseService struct {
client *auth.Client
logger infrastructure.Logger
env infrastructure.Env
}
// CreateUser creates a new user with email and password
func (fb *FirebaseService) CreateUser(newUser models.FirebaseAuthUser) (string, error) {
TenantID := fb.env.ClientTenantID
if newUser.Role == constants.RoleUser {
TenantID = fb.env.UserTenantID
}
tenantClient, err := fb.client.TenantManager.AuthForTenant(TenantID)
if err != nil {
fb.logger.Zap.Errorf("error initializing tenant client: %v\n", err)
return "", err
}

params := (&auth.UserToCreate{}).
Email(newUser.Email).
Password(newUser.Password).
DisplayName(newUser.DisplayName)

if newUser.Enabled == 0 {
params = params.Disabled(true)
}
if newUser.Enabled == 1 {
params = params.Disabled(false)
}
// All tenant-specific operations are exposed via auth.TenantClient
user, err := tenantClient.CreateUser(context.Background(), params)
if err != nil {
fb.logger.Zap.Errorf("error creating user: %v\n", err)
return "", err

}

claims := map[string]interface{}{
"role": newUser.Role,
"fb_uid": user.UID,
"id": newUser.UserId,
}
err = tenantClient.SetCustomUserClaims(context.Background(), user.UID, claims)
if err != nil {
return "Error setting custom user claims", err
}
return user.UID, err
}

Here we can see, initially in CreateUser function we get role of user and assign tenant ID as per role. This is to differentiate either “client or user” is being created.

After that we initialize the tenant client with below line of code

tenantClient, err := fb.client.TenantManager.AuthForTenant(TenantID)

and then finally we create using with the help of tenant client we just created.

user, err := tenantClient.CreateUser(context.Background(), params)
if err != nil {
fb.logger.Zap.Errorf("error creating user: %v\n", err)
return "", err
}

2. Updating User

// UpdateUser -> update firebase user.
func (fb *FirebaseService) UpdateFirebaseAuthUser(uid string, newData models.FirebaseAuthUser) error {
TenantID := fb.env.UserTenantID
if newData.Role == constants.RoleClient || newData.Role == constants.RoleClientAdmin || newData.Role == constants.RoleClientGeneral {
TenantID = fb.env.ClientTenantID
}
tenantClient, err := fb.client.TenantManager.AuthForTenant(TenantID)
if err != nil {
fb.logger.Zap.Errorf("error initializing tenant client: %v\n", err)
return err
}
user, err := tenantClient.GetUser(context.Background(), uid)
if err != nil {
return err
}
authParams := (&auth.UserToUpdate{})
if newData.Email != "" && newData.Email != user.Email {
authParams = authParams.Email(newData.Email)
}
if newData.Password != "" {
authParams = authParams.Password(newData.Password)
}
if newData.DisplayName != "" && newData.DisplayName != user.DisplayName {
authParams = authParams.DisplayName(newData.DisplayName)
}
if newData.Enabled == 0 {
authParams = authParams.Disabled(true)
}
if newData.Enabled == 1 {
authParams = authParams.Disabled(false)
}
if newData.Role != "" {
claims := map[string]interface{}{
"role": newData.Role,
"fb_uid": uid,
"id": newData.UserId,
}
err = tenantClient.SetCustomUserClaims(context.Background(), uid, claims)
if err != nil {
fb.logger.Zap.Info("Error setting custom user claims::::", err.Error())
return err
}
}

_, err = tenantClient.UpdateUser(context.Background(), uid, authParams)
if err != nil {
fb.logger.Zap.Error("Update User Firebaser (UpdateUserAuth) ::", err.Error())
return err
}
return nil
}

If we see above block of code and compare with first block there is not much of difference.

Similar to creating user, we get the updated value of user and then update it with help of tenant manager.

_, err = tenantClient.UpdateUser(context.Background(), uid, authParams)
if err != nil {
fb.logger.Zap.Error("Update User Firebaser (UpdateUserAuth) ::", err.Error())
return err
}

3. Set Custom Claims

Claims are some variables which are stored as key value pair while creating user and can be extracted later through firebase tokens.

In order to set custom claims for the multi-tenant user.

First, let’s create the user with tenant client and after user is created we set custom claim defined as map to the created user thorough tenant client.

// All tenant-specific operations are exposed via auth.TenantClient
user, err := tenantClient.CreateUser(context.Background(), params)
if err != nil {
fb.logger.Zap.Errorf("error creating user: %v\n", err)
return "", err

}

claims := map[string]interface{}{
"role": newUser.Role,
"fb_uid": user.UID,
"id": newUser.UserId,
}
err = tenantClient.SetCustomUserClaims(context.Background(), user.UID, claims)
if err != nil {
return "Error setting custom user claims", err
}
return user.UID, err

4. Generate password reset email

func (fb *FirebaseService) GeneratePasswordResetEmailForUser(email string) (string, error) {
tenantClient, err := fb.client.TenantManager.AuthForTenant(fb.env.UserTenantID)
if err != nil {
fb.logger.Zap.Errorf("error initializing tenant client: %v\n", err)
return "", err
}
actionCodeSettings := &auth.ActionCodeSettings{}
actionCodeSettings.URL = "http://localhost:3000"
actionCodeSettings.HandleCodeInApp = true

resp, err := tenantClient.PasswordResetLinkWithSettings(context.Background(), email, actionCodeSettings)

return resp, err
}

Same goes here, first we initialize tenant client and call method “PasswordResetLinkWithSettings” through tenant client.

After creating multiple users with single email address, we can update email, change passwords for individual email. There will be no side effects to other email if one is changed.

For the frontend implementation:

Here I am using Next.js and the config will be as shown below:

import { getApps, initializeApp } from "firebase/app"
import { getAuth } from "firebase/auth"
import { getAnalytics } from "firebase/analytics"

if (!getApps.length) {
initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGE_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
})
}

const tenantId = process.env.CLIENT_TENANT_ID

const auth = getAuth()
auth.tenantId = tenantId

We simply attach our desired tenant ID with the firebase auth instance. After this we will be able to login to the created multi-tenant user simply using signInWithEmailAndPassword() method available by firebase client library.

Flutter Implementation

In order to allow to login the multi tenant user in flutter.

we can set up code as below, we can see we are attaching the user tenant id to firebase auth instance.

Future<UserCredential> login(Map<String, dynamic>? data) {
firebaseAuth.tenantId = Config.userTenantId;
return firebaseAuth.signInWithEmailAndPassword(
email: data!["email"],
password: data["password"],
);
}

Testing in Postman

After creating our user, we can log in through postman to get an id token.

Send Post request to the below URL

https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={{FirebaseAPIKey}}

with body:

{
"email": "test.dev19@mailinator.com",
"password": "password",
"returnSecureToken": true,
"tenantId":"user-2pn4v"
}

and it will return a id token that can be used to verify and extract info such as tenant id, claims, and others.

Note: here I am passing tenantID as “user-2pn4v”. You must pass the tenant ID with which the user is created otherwise it will throw an error like “USER NOT FOUND”

Thank you for reading !!!

References:

--

--

Sumit Sapkota
readytowork, Inc.

Full Stack Developer. Golang, React.js, Next.js, Flutter, GCP