Skip to content

Instantly share code, notes, and snippets.

@nicolasdao
Last active July 2, 2023 08:06
Show Gist options
  • Save nicolasdao/db6d1fb276fb948e81ecf74ade5d46af to your computer and use it in GitHub Desktop.
Save nicolasdao/db6d1fb276fb948e81ecf74ade5d46af to your computer and use it in GitHub Desktop.
Firebase guide. Keywords: firebase identity platform google

FIREBASE GUIDE

Table of contents

Concepts

Project vs Apps

IMHO, those two concepts are not intuitive. Out-of-the-box, you start with creating a project which can contain most of the promised features (e.g., hosting, DB). Those features are sitting at the project level, but that's actually not the best way to manage them. Though you are not forced to, you should be using Apps inside your project, and then use the Firebase features under each app. This allows, for example, to seperate your project in different environment (e.g., test, prod). To create a new App in a project:

  1. Log in to your project and click on Project Overview.
  2. Click on the Add app button.

Firebase CLI

npm install -g firebase-tools

Once it is installed, use the firebase command to manage the deployments and the configuration of your project.

NOTE: This tool can also be installed locally in each project to improve portability. Instead of installing the CLI with the command above, use:

 npm install firebase-tools --save-dev

However, with this approach, the firebase command is not accessible globally. To automate the project management via this tool, you will have to script it via the package.json scripts property.

Basic commands

Command Description
firebase login Connects the Firebase CLI to a Firebase account.
firebase logout Disconnects the Firebase CLI from the current Firebase account.
firebase login:list Checks the identity of the current conected Firebase account.
firebase projects:list Lists all the projects for the current connected Firebase account.
firebase deploy Deploys a website to the currently connected Firebase account.

Website hosting

Step 1 . Initialize your project

Basic hosting project setup

Run the following command, and answer the questions:

firebase init

This will create 3 files and at least 1 folder:

  • .firebaserc: Defines which firebase project you’re deploying your website to.
  • database.rules.json: DB rules.
  • firebase.json: Defines which firebase project you’re deploying your website to.
  • public folder: This is the folder where you should put all your static HTML.
  • .firebaserc:
{
  "projects": {
    "default": "vexd-website"
  }
}

Where:

  • projects defines all the Firebase Hosting projects where your website can be deployed.
  • default is an alias that is the default alias used when you deploy using the firebase deploy command.
  • vexd-website is our firebase project id. You can find it in your firebase online console under Hosting (this is the https://vexd-website.firebaseapp.com).

Multiple environment with aliases

This is how you configure your project so you can deploy it to different environment:

  1. Create new hosting environment in Firebase (e.g. quivers-admin-test for test, quivers-admin-demo for demo, quivers-admin-prod for prod).
  2. Define an alias for each of the above environment in your .firebaserc file.
{
  "projects": {
    "default": "vexd-website",
    "test": "quivers-admin-test",
    "demo": "quivers-admin-demo",
    "prod": "quivers-admin-prod"
  }
}

To switch to any a specific environment, use the following command:

firebase use test  

To deploy to that specific environment, use:

firebase deploy

Ignoring files

Update the firebase.json as follow:

{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "_site",
    "ignore": [
       "firebase.json",
       "firebase-debug.log",
       "**/.*"
     ]
}

Step 2. Deploying To Firebase

firebase deploy

If you want to set up a specific environment, or deploy to a specific environment, use aliases as described in the Multiple environment with aliases section.

Step 3. Custom Domain

Prerequisite

You simply need to deploy something on your firebase project first (cf. previous Deploying To Firebase).

Verify Your Custom Domain Using TXT Records

Once you’ve deployed something on your firebase hosting, firebase will easily provide you 2 TXT Records files:

  • Click on Hosting for your specific project.
  • Click on Custom Domain, and there enter your custom domain, then click Verify.
  • Add your 2 TXT records to your DNS. Browse to your domain provider (e.g. GoDaddy), and add those 2 new records for your specific domain.
  • Wait a few hours before Google confirms that your custom domain has been verified.

Configure Your DNS To Point To Your Firebase URI

Push notifications

WARNING: To test web notifications in your browser, make sure the web notifications have been turned on in your OS. To know more about this topic, please refer to the How to turn web notifications on/off in MacOS? section.

Design and architecture

Firebase hosts a proprietary Pub/Sub BaaS called the Firebase Communication Messaging (FCM). The recommended architecture to build push notifications with Firebase requires three components:

  1. The FCM
  2. The Firebase SDK installed and configured on each client (i.e., Android, iOS, Web).
  3. The Firebase SDK installed integrated in your backend logic to push notifications based on certain events.

Each client (#2) can subscribe to specific topics. The server can also contact them privately using their unique REGISTRATION ID. In that case, your logic needs to be able to associate REGISTRATION IDs to users in your own data store. When your logic needs to notify a specific user, it performs a lookup to determine the user's REGISTRATION IDs so it knows where to push the notification.

Getting started

Step 1 - Setting up the FCM

  1. Login to your Firebase account and select/create your project.
  2. If you already have an app in your project, select it. Otherwise, create one as follow:
    1. In the default project page, click on the Add app button.
    2. Select a platform. Unless you're aiming to use that app for other purposes than puch notifications, it does not matter which platform you use. Push notifications are uses the same architecture regardless of the platform.
    3. Fill up the details and then click the Register app button.
  3. Select your app and click on the cog wheel to navigate to the app page.
  4. In the app page, select the Cloud Messaging tab.

Step 2 - Setting up the Firebase SDK in your backend

  1. Install the Firebase Admin SDK
    npm install firebase-admin
    
  2. In your code, initialize the Firebase Admin object using one the following four official strategies + 1 that personal approach that I found more portable:
    1. Using a service-account.json file:
      1. To acquire this credentials file, please refer to the How to get the service account JSON file? section.
      2. Set up an GOOGLE_APPLICATION_CREDENTIALS environment variable that contains the absolute path to the service account JSON file on your local machine. This variable is automatically set up in Firebase Cloud Function.
      3. Initialize the admin SDK in your code as follow:
      const firebase = require('firebase-admin')
      
      firebase.initializeApp({
      	credential: firebase.credential.applicationDefault(),
      	databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
      })
    2. Explicitly defining the creds in your code (deprecated as it could expose accesses to your account):
      const firebase = require('firebase-admin')
      const serviceAccount = {
      	"type": "service_account",
      	"project_id": "YOUR-PROJECT-ID",
      	"private_key_id": "***************",
      	"private_key": "-----BEGIN PRIVATE KEY-----\n***********\n-----END PRIVATE KEY-----\n",
      	"client_email": "***********.iam.gserviceaccount.com",
      	"client_id": "***************",
      	"auth_uri": "https://accounts.google.com/o/oauth2/auth",
      	"token_uri": "https://oauth2.googleapis.com/token",
      	"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
      	"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/*********.iam.gserviceaccount.com"
      }
      firebase.initializeApp({
      	credential: firebase.credential.cert(serviceAccount),
      	databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
      })
    3. Using an OAUTH2 refresh token:
      const firebase = require('firebase-admin')
      const refreshToken = '*************'
      firebase.initializeApp({
      	credential: firebase.credential.refreshToken(refreshToken),
      	databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
      })
    4. Using a FIREBASE_CONFIG and an GOOGLE_APPLICATION_CREDENTIALS environment variables:
      1. This FIREBASE_CONFIG variable is automatically set up in Firebase Cloud Function. This variable can either contain a JSON string or the absolute path to the config. This config contains all the information except the credential which is defined by the GOOGLE_APPLICATION_CREDENTIALS.
      2. Initialize the admin SDK in your code as follow:
      const firebase = require('firebase-admin')
      
      firebase.initializeApp()
    5. Base64 encode the service account JSON file into an environment variable:
      1. const credsBase64 = Buffer.from(JSON.stringify(creds)).toString('base64')
      2. Store that value in an environment variable GOOGLE_FIREBASE_KEY
      3. Use this environment variable to configure the firebase SDK:
      const firebase = require('firebase-admin')
      const serviceAccount = JSON.parse(Buffer.from(process.env.GOOGLE_FIREBASE_KEY, 'base64').toString())
      firebase.initializeApp({
      	credential: firebase.credential.cert(serviceAccount)
      })
  3. Code the push notifications. More details under the Code snippets annex section called Push notification in NodeJS.

Step 3 - Setting up the Firebase SDK in your client apps

  1. Install the Firebase SDK:
    npm install firebase
    
  2. Add the following property to the manifest.json: "gcm_sender_id": "103953800507". This number is the same for all mobile apps in the world.
  3. Create a service worker firebase-messaging-sw.js (code in the annex firebase-messaging-sw.js)
  4. Register that service worker as soon as you can in your code:
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker
        .register("../firebase-messaging-sw.js")
        .then(registration => {
          console.log(`Registration successful, scope is: ${registration.scope}`);
        })
        .catch(err => {
          console.log(`Service worker registration failed, error: ${err}`);
        });
    }
  5. Create a helper to synchronise your logged in user's registration token with your backend (code available in the annex notificationService.js).
  6. Use the setUpPushNotification (from the code above) on each page that need to make sure that push notification are turned on.

Firebase authentication

TL;DR: Use the Firebase client SDK, but do not use Firebase Authentication. Use its parent Google Cloud Identity Platform instead.

Though Firebase Authentication is a service that can be used and configured to manage your users, do not use it. Instead, use Google Cloud Identity Platform. Identity Platform is the actual underlying service behind Firebase Authentication. The Identity Platform offers more functionalities. IMHO, the most important additional feature is multy-tenancy, which allows to create multiple user pools in the same Google project. Without this feature, all your environments (if you decide to maintain multiple environments in the same project) have to share the same user pool, which is a huge constraints in terms of testing.

Official doc:

Authentication workflow

Using Firebase authentication is quite easy as long as you understand how an OIDC workflow works, and how Firebase authentication is supposed to be used in your app. I won't explain the OIDC workflow here, as this is outside of the scope of this document. Instead, this section will focus on explaining how Firebase authentication is supposed to be used in your app.

  1. In your GCP project, a new Identity Platform's user pool is configured. This is called tenant. That tenant is configured with the various identity provider you wish to support (e.g., email/password, Facebook, Google).
  2. In your app, the Firebase Client SDK instantiates a new Firebase client with your tenant's details (i.e., apiKey, authDomain and tenantId). That client exposes many IdP APIs to sign up and sign in your user. Upon successfull sign in/up, the following a Firebase User object is returned to your client app. That user object should be persisted in the app session because is contains useful pieces of data as well as useful APIs:
    • User details (e.g., first name, last name, email).
    • getIdToken method that can always access an up-to-date Identity Platform user pool's ID token. This token is a short-lived JWT that proves that your user is successfully authenticated.
  3. Use that ID token to safely login to your App server. That App server will in turn:
    1. Verify the ID token with the Firebase Admin SDK. This helps to get the

Quick start

Enable the Identity Platform

Enable Identity Platform in your Google Project here.

Creating an new tenant

Though it is possible to create a user pool using the default configuration, it is recommended to use tenants instead. Tenants allows to have more than one user pool per project. This is usefull when you wish to host multiple environments (e.g., test, prod) in the same project.

  1. Enable the multi-tenants feature:
    • Click on the Tenants in the menu.
    • Click on Settings, select the Security tab and then click on the Allow tenants button.
  2. Create a new tenant.

Create a JS client

In your client, use one of those two approaches to create the Firebase client:

  1. Using the CDN, add at the end of the body tag:
<!DOCTYPE html>
<html>
<head>
	<title>Firebase test</title>
</head>
<body>
	<h1>Sign in</h1>
	<script src="https://www.gstatic.com/firebasejs/7.18.0/firebase-app.js"></script>
	<script src="https://www.gstatic.com/firebasejs/7.18.0/firebase-auth.js"></script>
	<script>
		// Your web app's Firebase configuration
		var firebaseConfig = {
			apiKey: "your-api-key",
			authDomain: "your-domain"
		}
		// Initialize Firebase
		firebase.initializeApp(firebaseConfig)

		// Sets the tenant
		firebase.auth().tenantId = 'your-tenant-id'

		const signIn = false
		
		const execAuth = signIn 
			? firebase.auth().signInWithEmailAndPassword('[email protected]', 'HelloWorld') 
			: firebase.auth().createUserWithEmailAndPassword('[email protected]', 'HelloWorld')

		execAuth
			.then(result => {
				console.log('HOORAY - WE ARE IN')
				// Get the ID token
				result.user.getIdToken().then(idToken => {
					console.log(idToken)
				})
			})
			.catch(err => {
				console.log('OOPS - SOMETHING FAILED')
				if (err.code == 'auth/email-already-in-use')
					console.log('THAT EMAIL ALREADY EXISTS')
				
				if (err.code == 'auth/user-not-found')
					console.log('THAT USER DOES NOT EXIST')

				if (err && err.message) {
					console.log(err.code)
					console.log(err.message)
				}
			})

	</script>
</body>
</html>
  1. Using NodeJS, after installing the Firebase package with npm i firebase:
// Firebase App (the core Firebase SDK) is always required and must be listed first
import * as firebase from 'firebase/app'

// Add the Firebase products that you want to use
import 'firebase/auth'

// The same code as above

Create a publicly available web server that can verify the ID Token

This is the part that sucks with Firebase Authentication. You cannot use the ID token to access a protected web server. Instead, the web server must be public and you must manually verify the ID token:

  1. Get the service-account.json that can safely decode the ID token:
    1. Log in to the Firebase account linked to your Google project.
    2. Click on the cogwheel next to Project Overview and then click on Project settings.
    3. Click on the Service accounts tab.
    4. Click on the Generate new private key button.
    5. Save that JSON file somewhere safe.
  2. Configure the Firebase Admin SDK with the JSON file acquired previously as expained in the Step 2 - Setting up the Firebase SDK in your backend section.
  3. Use the following code snippet to decode the ID token:
    try {
    	const decodedToken = await firebase.auth().verifyIdToken(token)
    	console.log(JSON.stringify(decodedToken, null, '  '))
    } catch (err) {
    	console.log(err)
    }
    The value of the decoded token should be similar to this:
    {
      "iss": "https://securetoken.google.com/your-project-id",
      "aud": "your-project-id",
      "auth_time": 1597625474,
      "user_id": "Dew23tmZFGGNbddhkbTYUGU",
      "sub": "p0TtmZFGGfdsHEKHn4AKmYAk4",
      "iat": 1597625474,
      "exp": 1597629074,
      "email": "[email protected]",
      "email_verified": false,
      "firebase": {
        "identities": {
          "email": [
            "[email protected]"
          ]
        },
        "sign_in_provider": "password",
        "tenant": "your-tenant-id"
      },
      "uid": "Dew23tmZFGGNbddhkbTYUGU"
    }

Adding custom claims on the ID token

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

Adding a custom federated identity provider

https://firebase.googleblog.com/2016/10/authenticate-your-firebase-users-with.html

FAQ

How to get the service account JSON file?

  1. Login to your Firebase project.
  2. Click on the setting cog wheel, and select Project settings
  3. Select the Service accounts tab.
  4. Click on the Generate a new private key button.

How to set up the Firebase Admin SDK?

Please refer to the Step 2 - Setting up the Firebase SDK in your backend section.

How to get the hosting URL?

There are no commands to get the piece of information. Instead, the convention is as follow: https://PROJECTID.web.app

How to get the Firebase token for automated deployment?

When the user is not logged to Firebase yet and runs firebase deploy, an interactive page opens in the default browser to authenticate the user. In a CI environment, this is not possible. Instead, a token must be acquired and the --non-interactive flag must be used. To acquire a Firebase token, execute the following command:

firebase login:ci

Once the token is acquired, the deployment command in the CI environment is similar to the following:

firebase deploy --token=$FIREBASE_TOKEN --project $YOUR_PROJECT_ID --non-interactive

How to turn web notifications on/off in MacOS?

To quickly test if your MacOS has been configured to allowed web notifications:

  1. Open your browser and then open the console in the developer tools.
  2. Execute this command: new Notification('Hello')

If a notification appears, then MacOS allows web notitications.

To turn web notifications on/off in MacOS:

  1. Open the System Preferences....
  2. Select Notifications
  3. Select your browser app and there toggle the switch on or off.

How to ignore files when deploying a static website?

Please refer to the Ignoring files section.

Annex

Code snippets

Push notification in NodeJS

NOTE: The following code assumes that a service-account.json credential file has been encoded using base64 into an environment variable called GOOGLE_FIREBASE_KEY.

const firebase = require('firebase-admin')

if (!process.env.GOOGLE_FIREBASE_KEY)
	throw new Error(`Missing required environment variable GOOGLE_FIREBASE_KEY (required for push notifications)`)

const serviceAccount = JSON.parse(Buffer.from(process.env.GOOGLE_FIREBASE_KEY, 'base64').toString())

// Configure the firebase SDK with the right credentials
firebase.initializeApp({
	credential: firebase.credential.cert(serviceAccount)
})

/**
 * Pushes a message to one or many client (Android app, iOS app, Web app)
 * 
 * @param  {Object} data				Message data
 * @param  {Array}  registrationTokens	Device tokens. Usually, a token is associated with a specific user for a specific device.
 * @return {Void}
 */
const pushToClient = async (data, registrationTokens) => {
	if (!data)
		throw new Error(`Missing required 'data' argument`)
	if (!registrationTokens)
		throw new Error(`Missing required 'registrationTokens' argument`)
	if (!Array.isArray(registrationTokens))
		throw new Error(`Wr0ng argument exception. 'registrationTokens' must be an array`)
	if (!registrationTokens[0])
		throw new Error(`Wrong argument exception. 'registrationTokens' contain at least one token`)

	const message = registrationTokens.length === 1 ? { data, token:registrationTokens[0] } : { data, tokens:registrationTokens }

	try {
		const response = await registrationTokens.length === 1 ? firebase.messaging().send(message) : firebase.messaging().sendMulticast(message)
		if (response && response.failureCount && response.failureCount > 0) {
			const failedTokens = response.responses.reduce((acc,res,idx) => {
				if (!res.success)
					acc.push(registrationTokens[idx])
				return acc
			},[])
			const errorMsg = `Out of ${registrationTokens.length} registration token${registrationTokens.length > 1 ? 's' : ''}, ${response.failureCount} failed: ${failedTokens}`
			console.log(`ERROR - ${errorMsg}`)
			throw new Error(errorMsg)
		}
	} catch(err) {
		const errorMsg = `Failed to push notification ${JSON.stringify(data)} to registration tokens ${registrationTokens}`
		console.log(`ERROR - ${errorMsg}. Details: ${err.stack}`)
		throw new Error(errorMsg)
	}
}

/**
 * Pushes a message to a topic
 * 
 * @param  {Object} data	Message data
 * @param  {String} topic	
 * @return {Void}
 */
const pushToTopic = async (data, topic) => {
	if (!data)
		throw new Error(`Missing required 'data' argument`)
	if (!topic)
		throw new Error(`Missing required 'topic' argument`)

	try {
		await firebase.messaging().send({
			data,
			topic
		})
	} catch(err) {
		const errorMsg = `Failed to push notification ${JSON.stringify(data)} to topic ${topic}`
		console.log(`ERROR - ${errorMsg}. Details: ${err.stack}`)
		throw new Error(errorMsg)
	}
}

module.exports = {
	pushToClient,
	pushToTopic
}

firebase-messaging-sw.js

importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-messaging.js')

const HOST = 'achiko-wallet'
const APP_URL = 'https://achiko-wallet.web.app'

const firebaseConfig = {
	apiKey: "************",
	authDomain: "************.firebaseapp.com",
	databaseURL: "https://************.firebaseio.com",
	projectId: "************",
	storageBucket: "************.appspot.com",
	messagingSenderId: "************",
	appId: "************",
	measurementId: "************"
}
// Initialize Firebase
firebase.initializeApp(firebaseConfig)

const messaging = firebase.messaging()

messaging.setBackgroundMessageHandler(function(payload) {
	const promiseChain = clients
		.matchAll({
			type: "window",
			includeUncontrolled: true,
		})
		.then((windowClients) => {
			for (let i = 0; i < windowClients.length; i++) {
				const windowClient = windowClients[i]
				windowClient.postMessage(payload)
			}
		})
		.then(() => {
			// console.log('SERVICE WORKER: showNotification')
			// console.log(JSON.stringify(payload.data))

			if (payload && payload.data && typeof(payload.data) === 'object') {
				const { title, body } = payload.data
				if (title) {
					// console.log('Message received in service worker')
					return registration.showNotification(title, { body })
				}
			}
		})
	return promiseChain
})

// Uncomment this code to do something after the notification has been clicked.
self.addEventListener("notificationclick", event => {
	clients.openWindow(APP_URL)	
})

notificationService.js

import axios from "axios";
import { BASE_API_URL, APP_URL } from "constant";
import * as firebase from "firebase/app";
import "firebase/messaging";

const initializedFirebaseApp = firebase.initializeApp({
	apiKey: "**************",
	authDomain: "**************",
	databaseURL: "**************.firebaseio.com",
	projectId: "**************",
	storageBucket: "**************.appspot.com",
	messagingSenderId: "**************",
	appId: "**************",
	measurementId: "**************"
})

const messaging = initializedFirebaseApp.messaging()

const refreshPushNotificationToken = async (authToken: string, notificationToken: string) => {
  if (!authToken || !notificationToken) {
    console.log(
      `WARN: Skip refreshing the push notification token. Missing ${
        !authToken ? "authToken" : "notificationToken"
      }`
    );
    return;
  }
  const headers = {
    Authorization: `Bearer ${authToken}`,
    "Content-Type": "application/json",
  };

  const body = {
    deviceId: navigator.userAgent,
    token: notificationToken,
  };

  try {
    await axios
      .create({
        baseURL: BASE_API_URL,
        headers,
      })
      .put("push-notification", body);
    // console.log(`Push notification token successfully synched ${notificationToken}`);
  } catch (err) {
    console.log(`Failed to sync push notification token. ${err}`);
  }
};

const setUpPushNotification = async (authToken: string) => {
  if (!authToken) {
    // console.log(`WARN: Skip refreshing the push notification token. Missing authToken`);
    return;
  }
  await messaging
    .requestPermission()
    .then(async () => {
      const token = await messaging.getToken();
      if (token) await refreshPushNotificationToken(authToken, token);
    })
    .catch(err => {
      console.log("Unable to get permission to notify.", err);
    });

  navigator.serviceWorker.addEventListener("message", message => {
    // console.log("CLIENT: Message received");
    if (
      Notification &&
      Notification.permission === "granted" &&
      message &&
      message.data &&
      message.data.data &&
      typeof message.data.data === "object"
    ) {
      const { title, body } = message.data.data;
      if (title) {
        // console.log(`SHOW message now...`);
        const notification = new Notification(title, {
          body,
          icon:
            "https://static.wixstatic.com/media/631ee9_28b1b979cb974ef7a1ecc004aac44f70%7Emv2.png/v1/fill/w_32%2Ch_32%2Clg_1%2Cusm_0.66_1.00_0.01/631ee9_28b1b979cb974ef7a1ecc004aac44f70%7Emv2.png",
        });
        notification.onclick = event => {
          event.preventDefault(); // prevent the browser from focusing the Notification's tab
          window.open(APP_URL, "_blank");
        };
      }
    }
  });
};

export { refreshPushNotificationToken, setUpPushNotification };

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment