ZOFTINO.COM android and web dev tutorials

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

android registration form example

Login screen

android login form example

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

android custom registration login firestore db example

Cloud Functions Logs

android custom registration login firebase cloud functions example

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.