auhtor_image
Volodymyr Terebus

Front End Development Practice Lead

Chat System: Firebase, Authentication and Roles

Our goal for this project is to provide a chat system for visitors of the website so they can be easily assisted by the website’s owner in our support department. It is essential for the website’s owner to have the ability to customize the chat widget and have a powerful integration of support in the back office with all visitors and user’s information from the website’s database. This means that any of our chat system customers will have a customizable widget for their websites and their own desk/back office that integrates with their own services.

Existing Chat Systems

There are many existing chat systems at the moment, however, not all of them have the ability to keep close-knit integration with the following customer services:

As a result to creating this new chat system, we have also created a fast and extremely flexible solution that used technologies, such as, Typescript, Angular, Rollup.js, Firebase Database, Firebase Storage, Firebase Hosting, Firebase Functions, and Google Cloud SQL  (MySQL). The following selection will show and explain how we created this serverless chat system and the difficulties it has passed.

Chat System Components

As we overview the high-level system, we recognize that the chat system should always have the chat widget, the back office, and the back end.

The chat widget is the library that can easily integrate into our website and provide a chat window for the any visitors of our website. Chat widget is one single JavaScript file that can easily integrate into any sort of website. It can log in visitors, connect to Firebase services, such as database and storage, integrate chat windows and customize them, and send and receive messages. This library also has the ability to be managed from website scripts.

The back office is our website for our support team (all operators and administrators). Operators will support visitors and clients of the website and be managed by administrators. Everyone in the back office will communicate through internal chats. All business logic and backend communication is built in one chat widget core library and all user interface elements are done in Angular.

And the back end stores and manages users messages and integrates with third party services. This is the fully serverless part. It uses Firebase Storage, for storing images and/or files that are sent in chat; Firebase Hosting, for having the ability to store back office static files and chat widget library files; Google Cloud SQL (MySQL), for storing messages and archive for reporting; Firebase Database, for defining rules, storing text messages and user details; Firebase Functions, to extend the authentication process, archive, and system report; and Firebase Authentication for the authentication process.

 

Authetification process and widget library design

One of the most intriguing parts of this project is managing the chats and it’s users, so understanding the authentication process and widget library design is essential. Each website visitor can be as anonymous as any authenticated user in a customer’s website. There could be millions of users, therefore it’s not a good decision to create Firebase user for each website visitor. If a website visitor is made as anonymous as a Firebase user, it will make it impossible for the operators to handle each of the visitors messages in their chats.

One possible way to make website visitors as anonymous as Firebase users is to put all users on one Firebase user. For each visitor who logs in will generate custom tokens of the Firebase user. However, ensure to define claims according to the visitors data, such as their ID, level, role, etc.

See: https://firebase.google.com/docs/auth/admin/create-custom-tokens

Each operator and administrator in the back office will ensure it is safe to create a separate Firebase user. We must unify claims for our visitors,  so each operator and administrator will log in with their emails and passwords to generate the custom token, for the relative Firebase user. When you get into the Firebase Console simply enable the Only Email/Password feature under the sign-in provider column, and disable all other methods.

Generally, we will have a similar list of users. UserUID  ‘client will represent all visitors, while UserUID ‘internal%random% will represent all operators and administrators.

 

Visitors have less control than operators and operators have less control than administrators.

Visitors – {id:visitorID, role: ‘client’, level:1}

Operators ​- {id:userUID, role: ‘operator’, level:2}

Administrators – ​{id:userUID, role: ‘admin’, level:3}

 

Firebase isn’t ready to use the functionalities for this, however, it has the ability to implement itself on top of Firebase. The clients side first needs to retrieve the custom tokens from the Firebase Functions. Then, simply log yourself in by this token to Firebase.

 

 

In Firebase Database rules, we can use claims data, such as level and ID.

{
  "rules": {
    // By default, make all data private unless specified otherwise.
    ".read": false,
    ".write": false,
    "room-messages": {
      "$roomId": {
        // A list of messages by room, viewable by authorized users
      }
    },
    "users": {
      // A list of users and their associated metadata, 
      // which can be updated by the single user or a moderator
      ".write": "auth.token.level>2",
      ".read": "auth.token.level>1",
      "$userId": {
        ".write": "$userId===auth.token.uid || auth.token.level>1",
        ".read": "$userId===auth.token.uid || auth.token.level>1"
      }
    },
    "moderators": {
      ".read": "auth.token.level>1",
      "$userId": {
        ".read": "$userId===auth.token.uid || auth.token.level>1"
      }
    }
  }
}

 

The widget will send a request to “LoginExternalUser” to the Firebase Function with the user information and you will receive, “ExternalClientID” and “CustomToken”, to authorize to Firebase.

Back office users will fill in their emails and passwords, send a request to “LoginInternalUser” to the Firebase Function, and receive “InternalClientID” and “CustomToken” to authorize to Firebase.

 

The code you see below briefly describes this process.

 

Due to this implementation, we understand roles and other claims of users in chat widget script and the back office script can properly handle user interface behavior.

// chat.widget.js
loginExternal() {
  return sendToFirebaseFunctions("/loginExternal",
    {
      userId: this.userId,
      brandId: this.brandId,
      userName: this.userName,
      customAnonymousSession: this.customAnonymousSession,
    })
    .then(resp => {
      const jwt = parseJwt(resp.token);
      if (jwt.claims.role === 'client') {
        // do something
      }
      return this.auth.signInWithCustomToken(resp.token);
    });
}

// backOfficeChatCore.js
loginInternal() {
  return sendToFunctions("/loginInternal",
    {
      email: this.email,
      password: this.password
    })
    .then(resp => {
      const jwt = parseJwt(resp.token);
      if (jwt.claims.role === 'agent') {
        // do something
      }
      return this.auth.signInWithCustomToken(resp.token);
    });
}

// firebase-functions.js
loginExternal(userId, brandId, userName, customAnonymousSession) {
  const isAnonymous = !userId || userId === -1 || userId === '-1';
  let externalClientId = null;
  // setup externalClientId ...

  const claims = {
    role: 'client',
    uid: externalClientId,
    level: 0
  };
  if (isAnonymous) {
    claims.isAnonymous = true;
  }

  return admin.auth()
    .createCustomToken('client', claims)
    .then(customToken => {
      return {
        token: customToken,
        id: externalClientId,
        firebaseConfig: clientFirebaseConfig
      };
    });
}

// only for check login by email/pasword like a client side code
const clientApp = firebase.initializeApp(clientFirebaseConfig, 'client');

loginInternal(email, password) {
  return clientApp.auth()
    .signInWithEmailAndPassword(email, password)
    .then(userRecord => {
      clientApp.auth().signOut();
      return admin.database().ref('/moderators/' + userRecord.uid)
        .once('value')
        .then(snapshot => {
          const claims = snapshot.val().claims;
           claims.uid = userRecord.uid;
           return admin.auth().createCustomToken(userRecord.uid, claims)
             .then(customToken => {
               return {
                 token: customToken,
                 id: userRecord.uid,
                 firebaseConfig: clientFirebaseConfig
               };
             })
         })
      })
}
Conclusions

This chat system is a simple solution that solves many issues of managing Firebase users and provides excellent features, such as the role feature. The positive aspects of this project is that it is simple, easy to use, and provides role services. However, the negative aspects to this project are that we need to use Firebase Function and use additional requests to log in to Firebase.

In the next excerpt, we will explain and show how to manage all rooms and messages in the chat system.

References

https://firebase.google.com/docs/functions/

https://firebase.google.com/docs/auth/admin/create-custom-tokens

https://firebase.google.com/docs/auth/admin/

https://firebase.google.com/docs/auth/web/custom-auth