How to use cloud function between Android and Firestore with HTTP

Firebase offers a wide range of solutions that help you easily integrate features such as user authentication and database query into your mobile and web application seamlessly. Although integrating Cloud Firestore directly into your mobile app client may seem hassle-free to interact with database, this may pose security risk as your app could manipulated to gain unauthorised access to your business secret lies inside your app. Integrating Cloud Firestore directly into your mobile app also might cause you to spend more time if you want to expand into web application and need to have the same service. Based on these reasons, it might be a good idea to place your backend services onto the cloud.

Fortunately, Cloud functions allows you to do that. You can deploy backend functions such as database queries, heavy computations and it can be triggered using http request response method.

I have created a simple android app and cloud function script to show you the concept behind it.

Firstly, we will need to install Firebase CLI:

npm install -g firebase-tools

Login and authenticate yourself via browser using command:

firebase login

Initialise a project and start installing all the dependencies:

firebase init functions

During initialisation, you will be asked to select a new or existing Firebase project. There will be option to select between Javascript or Typescript for your script language and option of enabling ESLint which helps to look for potential issues with your script. For this example, i have selected Javascript and disable ESLint.

After initialisation is done, go to folder functions and you will be writing your backend script in index.js

To deploy the script, simple run:

firebase deploy --only functions

From the terminal, you can see logs of the functions in the script being deployed:

Project Console: https://console.firebase.google.com/project/testfire-6b175/overview
DAVIDs-MBP:functions davidcheah$ firebase deploy --only functions
=== Deploying to 'testfire-6b175'...
i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (42.53 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function getAccountInfo(us-central1)...
i  functions: updating Node.js 6 function register(us-central1)...
i  functions: updating Node.js 6 function login(us-central1)...
i  functions: updating Node.js 6 function destroySession(us-central1)...
✔  functions[getAccountInfo(us-central1)]: Successful create operation.
Function URL (getAccountInfo): https://us-central1-testfire-6b175.cloudfunctions.net/getAccountInfo
✔  functions[destroySession(us-central1)]: Successful update operation.
✔  functions[login(us-central1)]: Successful update operation.
✔  functions[register(us-central1)]: Successful update operation.
✔  Deploy complete!
Project Console: https://console.firebase.google.com/project/testfire-6b175/overview
DAVIDs-MBP:functions davidcheah$ firebase deploy --only functions

For this example, user will be shown a login/registration screen. First the user registers into Firestore and the backend script checks for any existing account created.

exports.register = functions.https.onRequest((req, res) => {
  if (req.method === 'PUT') {
    res.status(403).send('Forbidden!');
    return;
  }
  cors(req, res, () => {
    let name = req.query.name;
    //validations
    if (!name) {
      res.status(200).send("Please enter name.");
      return;
    }
    let email = req.query.email;
    if (!email) {
      res.status(200).send("Please enter email.");
      return;
    }
    let password = req.query.password;
    if (!password) {
      res.status(200).send("Please enter password.");
      return;
    }
    if (!validator.isLength(password, 3)) {
      res.status(200).send("Please enter valid password.");
      return;
    }
    if(!validator.isEmail(email)){
      res.status(200).send("Please enter valid email.");
      return;
    }

    //check if user already exists in firestore
    var userRef = admin.firestore().collection('users')
var userExists;
    userRef.where('email', '==', email).get()
    .then(snapshot => {
      userExists = snapshot.size;
      console.log(`user by email query size ${userExists}`);
      //send error if user exists
      if(userExists && userExists > 0){
        res.status(200).send("Account exists with same email Id.");
        return;
      }
      //add user to database
      admin.firestore().collection('users').add({
        name: name,
        email: email,
        password: password
      }).then(ref => {
        console.log('add user account', ref.id);
        res.status(200).send("User account created.");
        return; 
      });
})
    .catch(err => {
      console.log('error getting user by email', err);
      res.status(200).send("System error, please try again.");
    });
});
});

Once successful, it will direct user into login screen, where user enters email and password. When the backend checks for a match, it will send a response with unique token string which the app will store in Shared Preference memory. This token is also stored in Firestore.

exports.login = functions.https.onRequest((req, res) => {
  if (req.method === 'PUT') {
    res.status(403).send('Forbidden!');
    return;
  }
  cors(req, res, () => {
    let email = req.query.email;
    //validation
    if (!email) {
      res.status(200).send("Please enter email.");
      return;
    }
    if(!validator.isEmail(email)){
      res.status(200).send("Please enter valid email.");
      return;
    }
    let password = req.query.password;
    if (!password) {
      res.status(200).send("Please enter password.");
      return;
    }
    if (!validator.isLength(password, 3)) {
      res.status(200).send("Please enter valid password.");
      return;
    }
//get password from db and match it with input password
    var userRef = admin.firestore().collection('users')
userRef.where('email', '==', email).get()
    .then(snapshot => {
      if(snapshot.size > 1){
        res.status(200).send("Invalid account.");
        return;
      }
      snapshot.forEach(doc => {
        console.log(doc.id, '=>', doc.data().name);
        var userPass = doc.data().password;
//if password matches, generate token, save it in db and send it
        if(userPass && password == userPass){
          const tokgenGen = new TokenGenerator(256, TokenGenerator.BASE62);
          const tokenStr = tokgenGen.generate();
//save token in db to use for other client request's authentication verification
          var tokenData = { email: doc.data().email};
          admin.firestore().collection('tokens').doc(tokenStr).set(tokenData);
res.status(200).send("token:"+tokenStr );
        }else{
          res.status(200).send("Invalid email/password.");
        }
      });
    })
    .catch(err => {
      console.log('error getting user by email', err);
      res.status(200).send("System error, please try again.");
    });
});
});

After login is successful, user is directed into account info screen where the balance will be shown. In the background, the app retrieves the unique token string and sends to backend script requesting to get account info. The backend script gets the token and checks for match in Firestore. Once it is a match, user account info will be release.

exports.getAccountInfo = functions.https.onRequest((req, res) => {
    if (req.method === 'PUT') {
    res.status(403).send('Forbidden!');
    return;
    }
    cors(req, res, () => {
        let token = req.query.token;
        var tokenData = token;
    //validation
    if (!token) {
      res.status(200).send("Please login");
      return;
    } else {
            var tokenDoc = admin.firestore().collection('tokens').doc(token);
            tokenDoc.get()
            .then(doc => {
                //if token exists then send data otherwise error response
                if (!doc.exists) {
                    console.log('Invalid token');
                    res.status(200).send("Invalid token");
                } else {
                    console.log('valid token');
                    // TODO: Add code to get information from db
                    // Lets assume we get account balance from db
                    var accountBal = '$200';
                    res.status(200).send(accountBal);
                }
            });
        }
  });
});

Once user is done, logout can be performed to clear any token in the app Shared Preference memory and also in Firestore.

exports.destroySession = functions.https.onRequest((req, res) => {
    if (req.method === 'PUT') {
        res.status(403).send('Forbidden!');
        return;
    }
    cors(req, res, () => {
        let token = req.query.token;
        var tokenData = token;
        //validation
        if (!token) {
            res.status(200).send("Please login");
            return;
        } else {
            // Delete token entry from db
            var tokenDoc = admin.firestore().collection('tokens').doc(token).delete();
            tokenDoc.delete();
            res.status(200).send("Delete success");  
        }
    });
});

Here is how it looks like on the app and in Firestore:

Both Android and cloud function source code are available in my github:

About the author

Founder of tattweicheah.com. Loves music, sport and most importantly software development.

Leave a Reply

Your email address will not be published. Required fields are marked *