Android Login and Registration with Firebase Cloud Functions and Firestore

To provide customized services and personal information to users, registration and login functionality needs to be part of android applications. While you can implement registration and login using various federated login providers such as Google sign in, Facebook sign in, Twitter, etc., allowing users to use existing accounts of the sign-in providers to login to your app, providing custom registration and login gives an additional option for your users to login to your app.

Firebase provides authentication framework, using that you can implement email and password authentication. But if you want more control over registration form, you need to implement custom registration and login.

In this post, you can learn how to implement custom registration and login for android applications using Firebase cloud functions and Firebase firestore database.

Registration and Login Firebase Cloud Functions

Let’s write and deploy cloud functions which can handle https registration and login requests. To write cloud functions, first we need to setup cloud functions project. To do that, install node.js and npm, then install firebase command line using npm install -g firebase-tools command, then login to firebase using firebase login command, then in the folder where you want to create cloud functions project, issue firebase init functions command to create project structure, files and dependencies. For detailed instructions, please see setting up cloud functions project.

Open index.js file and create account registration and login functions, below is the code. Registration function first validates the input, if there are any validation errors, it’ll send error messages, if user doesn’t exist in firestore database, it adds user and sends response message.

Login function first validates the input, then using the input email, it reads password from firestore. If the password in firestore matches to what user entered, then it will generate a token, will create a token record in firestore database and will send the token to client.

If you have any other cloud functions which can provide data to only logged in users, then make sure that your android app sends the token in the request to the functions so that they can identify the user and provide requested functionality. In the below code, there is an accountBalance function which performs token verification.

Node.js modules such as uuid-token-generator, validator and cors are used in the example cloud functions.

const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(functions.config().firebase); const cors = require('cors')({origin: true}); const validator = require('validator'); const TokenGenerator = require('uuid-token-generator'); 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, 7)) { 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 eamil 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."); }); }); }); 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, 7)) { 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."); }); }); }); exports.accountBalance = functions.https.onRequest((req, res) => { if (req.method === 'PUT') { res.status(403).send('Forbidden!'); return; } cors(req, res, () => { let token = req.query.token; if (!token) { res.status(200).send("Please login"); return; } 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'); //get account balance from db and send it.. var accountBal = '$200'; res.status(200).send(accountBal); } }); }); });

Firestore Security Rules

Since we are using firestore database to store user accounts data, we need to secure it. To do that we need to define security rules in Firebase console. Below security rules make users and events firestore collections private and allow only Firestore server client libraries to access it.

service cloud.firestore { match /databases/{database}/documents { match /users/** { allow read, write: if false; } match /tokens/** { allow read, write: if false; } match /{document=**} { allow read, write: if true; } } }

Android Registration and Login Form Example

Registration screen

Login screen

Android Registration and Login Example

Registration and login activity first checks to see if login-token exists in the shared preferences. If token exists, it will show main activity. If token doesn’t exist, it will show register account screen. Once user submit registration form, it will take the input and send it to register https cloud function using OkHttp and handle the response.

In the case of login, request is sent to login cloud function. On receiving login token, it will store the token in the shared preferences.

In the case of accessing account balance cloud function, example app will read the token from shared preferences and send the token in the request to the cloud function. If token doesn’t exist in shared perferences, it will show registration and login screen to user.

Below is complete example android app code.

Base Activity

import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import zoftino.com.firestore.R; public abstract class BaseAction extends AppCompatActivity { private static final String TAG = "BaseAction"; @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.acc_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.acc_bal_m: accountBal(); return true; case R.id.regiser_m: loginRegister(); return true; default: return super.onOptionsItemSelected(item); } } void accountBal(){ Intent i = new Intent(); i.setClass(this, AccountBalance.class); startActivity(i); } private void loginRegister(){ Intent i = new Intent(); i.setClass(this, RegistrationLoginActivity.class); startActivity(i); } boolean isLogin(){ String token = getLoginToken(); if(token == null || token.isEmpty()){ return false; }else{ return true; } } String getLoginToken(){ SharedPreferences sharedPref = PreferenceManager. getDefaultSharedPreferences(this.getApplication()); String token = sharedPref.getString(getString(R.string.auth_token), null); return token; } void sendMessageToCloudFunction(HttpUrl.Builder httpBuider) { OkHttpClient httpClient = new OkHttpClient(); Request request = new Request.Builder(). url(httpBuider.build()).build(); httpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "error response firebase cloud function"); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(BaseAction.this, "Action failed please try gain.", Toast.LENGTH_SHORT).show(); } }); } @Override public void onResponse(Call call, Response response) { ResponseBody responseBody = response.body(); String resp = ""; if (!response.isSuccessful()) { Log.e(TAG, "action failed"); resp = "Failed perform the action, please try again"; } else { try { resp = responseBody.string(); Log.e(TAG, "Response " + resp); } catch (IOException e) { resp = "Problem in reading response"; Log.e(TAG, "Problem in reading response " + e); } } runOnUiThread(responseRunnable(resp)); } }); } abstract Runnable responseRunnable(final String responseStr); }

Registration and Login Activity

import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import okhttp3.HttpUrl; import zoftino.com.firestore.R; public class RegistrationLoginActivity extends BaseAction { private static final String TAG = "RegLoginActivity"; private String FIREBASE_CLOUD_FUNCTION_REG_URL = "https://us-central1-your-project.cloudfunctions.net/register"; private String FIREBASE_CLOUD_FUNCTION_LOGIN_URL = "https://us-central1-your-project.cloudfunctions.net/login"; private String loginEmail; private ViewGroup regLayout; private ViewGroup loginLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(isLogin()){ accountBal(); } setContentView(R.layout.registration_login_layout); Toolbar tb = findViewById(R.id.toolbar); setSupportActionBar(tb); tb.setSubtitle("Registration/Login"); regLayout = findViewById(R.id.registration_layout); regLayout.setVisibility(View.GONE); loginLayout = findViewById(R.id.login_layout); setListeners(); } private void setListeners() { //registration Button registrationButton = findViewById(R.id.register_b); registrationButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { registration(); } }); //takes to login screen from reg screen Button regToLogin = findViewById(R.id.reg_login_b); regToLogin.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { regLayout.setVisibility(View.GONE); loginLayout.setVisibility(View.VISIBLE); } }); //login Button loginButton = findViewById(R.id.login_b); loginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { login(); } }); //takes to reg screen from login screen Button loginToReg = findViewById(R.id.login_reg_b); loginToReg.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { regLayout.setVisibility(View.VISIBLE); loginLayout.setVisibility(View.GONE); } }); } private void registration() { EditText nameEt = findViewById(R.id.name); EditText emailEt = findViewById(R.id.email); EditText passwordEt = findViewById(R.id.password); String name = nameEt.getText().toString(); String email = emailEt.getText().toString(); String password = passwordEt.getText().toString(); HttpUrl.Builder httpBuider = prepareRegRequestBuilder(name, email, password); sendMessageToCloudFunction(httpBuider); } private void login() { EditText emailEt = findViewById(R.id.email_login); EditText passwordEt = findViewById(R.id.password_login); loginEmail = emailEt.getText().toString(); String password = passwordEt.getText().toString(); HttpUrl.Builder httpBuider = prepareLoginRequestBuilder(loginEmail, password); sendMessageToCloudFunction(httpBuider); } private HttpUrl.Builder prepareRegRequestBuilder(String name, String email, String password) { HttpUrl.Builder httpBuider = HttpUrl.parse(FIREBASE_CLOUD_FUNCTION_REG_URL).newBuilder(); httpBuider.addQueryParameter("name", name); httpBuider.addQueryParameter("email", email); httpBuider.addQueryParameter("password", password); return httpBuider; } private HttpUrl.Builder prepareLoginRequestBuilder(String email, String password) { HttpUrl.Builder httpBuider = HttpUrl.parse(FIREBASE_CLOUD_FUNCTION_LOGIN_URL).newBuilder(); httpBuider.addQueryParameter("email", email); httpBuider.addQueryParameter("password", password); return httpBuider; } Runnable responseRunnable(final String responseStr) { Runnable resRunnable = new Runnable() { public void run() { Log.d(TAG, responseStr); //login success if(responseStr.contains("token")){ //retrieve token from response and save it in shared preference //so that token can be sent in the request to services String tokenStr[] = responseStr.split(":"); Log.d(TAG, tokenStr[1]); SharedPreferences sharedPref = PreferenceManager. getDefaultSharedPreferences( RegistrationLoginActivity.this.getApplication()); SharedPreferences.Editor editor = sharedPref.edit(); editor.putString(getString(R.string.auth_email), loginEmail); editor.putString(getString(R.string.auth_token), tokenStr[1]); editor.commit(); Toast.makeText(RegistrationLoginActivity.this, "Login Successful.", Toast.LENGTH_SHORT).show(); restUi(); accountBal(); }else if(responseStr.contains("account created")){ Toast.makeText(RegistrationLoginActivity.this, "Account created, login now.", Toast.LENGTH_SHORT).show(); restUi(); showLogin(); }else { Toast.makeText(RegistrationLoginActivity.this, responseStr, Toast.LENGTH_SHORT).show(); } } }; return resRunnable; } private void restUi(){ ((EditText)findViewById(R.id.name)).setText(""); ((EditText)findViewById(R.id.email)).setText(""); ((EditText)findViewById(R.id.password)).setText(""); ((EditText)findViewById(R.id.email_login)).setText(""); ((EditText)findViewById(R.id.password_login)).setText(""); } private void showLogin(){ regLayout.setVisibility(View.GONE); loginLayout.setVisibility(View.VISIBLE); } }

Registration and Login Activity Layout

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/layoutBg" android:orientation="vertical"> <android.support.constraint.ConstraintLayout android:id="@+id/registration_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="16dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp"> <android.support.design.widget.TextInputLayout android:id="@+id/name_l" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> <android.support.design.widget.TextInputEditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Name"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:id="@+id/email_l" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/name_l"> <android.support.design.widget.TextInputEditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Email"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:id="@+id/password_l" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/email_l"> <android.support.design.widget.TextInputEditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textWebPassword" android:hint="Password"/> </android.support.design.widget.TextInputLayout> <Button android:id="@+id/register_b" android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/Widget.AppCompat.Button.Colored" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/password_l" android:text="Register"/> <Button android:id="@+id/reg_login_b" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/register_b" style="?android:attr/borderlessButtonStyle" android:text="Already a member? Login." /> </android.support.constraint.ConstraintLayout> <android.support.constraint.ConstraintLayout android:id="@+id/login_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="16dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp"> <android.support.design.widget.TextInputLayout android:id="@+id/email_login_l" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> <android.support.design.widget.TextInputEditText android:id="@+id/email_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Email"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:id="@+id/password_login_l" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/email_login_l"> <android.support.design.widget.TextInputEditText android:id="@+id/password_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textWebPassword" android:hint="Password"/> </android.support.design.widget.TextInputLayout> <Button android:id="@+id/login_b" android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/Widget.AppCompat.Button.Colored" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/password_login_l" android:text="Login"/> <Button android:id="@+id/login_reg_b" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/login_b" style="?android:attr/borderlessButtonStyle" android:text="Not a member? Sign up now." /> </android.support.constraint.ConstraintLayout> </LinearLayout> </LinearLayout>

Account Balance Activity

import android.content.Intent; import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.util.Log; import android.widget.TextView; import okhttp3.HttpUrl; import zoftino.com.firestore.R; public class AccountBalance extends BaseAction { private static final String TAG = "AccountBalance"; private String FIREBASE_CLOUD_FUNCTION_ACCOUNT_BAL_URL = "https://us-central1-your-project.cloudfunctions.net/accountBalance"; TextView accBal; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.account_balance); Toolbar tb = findViewById(R.id.toolbar); setSupportActionBar(tb); tb.setSubtitle("Account Balance"); accBal = findViewById(R.id.account_balance); getAccountBlance(); } private void getAccountBlance() { String token = getLoginToken(); //send user to login screen if no token if(token == null || token.isEmpty()){ Intent i = new Intent(); i.setClass(this, RegistrationLoginActivity.class); startActivity(i); } Log.d(TAG, "token "+token); HttpUrl.Builder httpBuider = HttpUrl.parse(FIREBASE_CLOUD_FUNCTION_ACCOUNT_BAL_URL).newBuilder(); httpBuider.addQueryParameter("token", token); sendMessageToCloudFunction(httpBuider); } Runnable responseRunnable(final String responseStr) { Runnable resRunnable = new Runnable() { public void run() { Log.d(TAG, "Response "+responseStr); accBal.setText("Account Balance :"+responseStr); } }; return resRunnable; } }

Menu

<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/acc_bal_m" android:icon="@drawable/account_balance" android:title="account_balance" app:showAsAction="always"></item> <item android:id="@+id/regiser_m" android:icon="@drawable/account" android:title="Login/Register" app:showAsAction="always"></item> </menu>

Firestore Database

Cloud Functions Logs

To learn cloud functions, please see Firebase cloud functions cloud storage trigger android example and Firebase cloud functions firestore trigger android example and Firebase cloud functions realtime database android example.