Search code examples
node.jsfirebaseazure-blob-storagefirebase-admin

Fetching serviceaccount.json from Azure blob storage to use with Firebase admin sdk on Node.js Express backend


I've created Node.js Express backend for one of my side projects. It uses Firebase admin sdk and uses servicesacount.json file to access the initializeApp method in Firebase. Everything works fine when I keep it inside the local directry.

const serviceAccount = require("path/to/serviceAccountKey.json");
initializeApp({
  credential: cert(serviceAccount),
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com"
});

I initialize a method to fetch the serviceaccount.json from Azure blob storage below,

import { BlobServiceClient } from '@azure/storage-blob';

import dotenv from 'dotenv';

dotenv.config();

async function fetchServiceAccount() {
    try {
        const blobServiceClient = BlobServiceClient.fromConnectionString(`${process.env.AZURE_STORAGE_CONNECTION_STRING}`);
        const containerClient = blobServiceClient.getContainerClient(`${process.env.AZURE_STORAGE_CONTAINER}`);
        const blobClient = containerClient.getBlockBlobClient(`${process.env.AZURE_STORAGE_BLOB}`);

        const downloadResponse = await blobClient.download();
        const blobContents = await streamToBuffer(downloadResponse.readableStreamBody as NodeJS.ReadableStream);
        console.log(blobContents)

        const serviceAccount : object = JSON.parse(blobContents.toString());

        return serviceAccount;
    } catch (err) {
        console.log('error on fetchserviceaccount: ', err);
    }


}

async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {

    return new Promise((resolve, reject) => {
        const chunks: any[] = [];
        stream.on('data', (chunk: any) => chunks.push(chunk));
        stream.on('error', (error: Error) => reject(error));
        stream.on('end', () => resolve(Buffer.concat(chunks)));
    });
}

export default fetchServiceAccount;

Here, I'm returning the service account file as a JSON object. but it seems on my code the file fetching never happen. Here in the serviceaccount fetching the value is also returned as a promise and for initializeApp method, it requre a file.

import * as admin from 'firebase-admin';

import dotenv from 'dotenv';


import  fetchServiceAccount from './serviceAccount';  // importing the fetching method
import { initializeApp } from 'firebase/app';

dotenv.config();

const serviceAccount =
  async () => {

    return await fetchServiceAccount();
  } // follow async await approach to get the serviceaccount,json to a variable

const firebaseConfig: object = {
  apiKey: process.env.REACT_APP_FIREBASE_APIKEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTHDOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECTID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGEBUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGINGSENDERID,
  appId: process.env.REACT_APP_FIREBASE_APPID,
  measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENTID,
};

const firebaseApp = initializeApp(firebaseConfig);
console.log(serviceAccount);
const firebaseAdmin = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
  // Add other Firebase configuration options if required
});

const fireStore = firebaseAdmin.firestore();

export default {
  firebaseAdmin,
  firebaseApp,
  fireStore
};

I'm exporting firebaseadmin , firebaseapp and firestore seperatly because in my project structure collections, and different routes are defined in seperate files so the relevent admin functions are obtained from this export.

Here is how my files are structured in the node js express server,

-src
    -collections
        UserCollection.ts // collections for firestore database 
    -controller
        authentication.controller.ts // methods on login , signup for different routes
    -firebase
        firebase.ts // export firebaseadmin, firebase app and firestore
        fetchServiceAccount.ts
    -routes
    -middleware
    -types
    server.ts
.env
package.json

I'm getting this error in my console,

  errorInfo: {
    code: 'app/invalid-credential',
    message: 'Service account must be an object.'
  },
  codePrefix: 'app'

I would like to know is the way my files structure are correct and anybody can share there way of getting the serviceaccount.json to work correctly in the project. also has anyone tried to add these serviceaccount values from environment variables, would appriciate any help you can provide


Solution

  • Here inside the method where I export firebaseadmin, I'm returning a promise so It won't match with the initializeApp function in Firebase admin.

    const serviceAccount =
      async () => {
    
        return await fetchServiceAccount();
      } // follow async await approach to get the serviceaccount,json to a variable, but this will only returns a promise rather than the actual serviceaccount object.
    

    so I needed to change that file as below. by doing this it will await the results of fetchServiceAccount function and insert the resoled value to initialieApp method. I've removed firebaseApp export because it doesn't require async await approach so I'll export it as a seperate module.

    import * as admin from 'firebase-admin';
    import dotenv from 'dotenv';
    import fetchServiceAccount from './serviceAccount';
    
    
    dotenv.config();
    
    const initializeFirebase = async () => {
      const serviceAccount = await fetchServiceAccount(); // Await the result of fetchServiceAccount
    
      const firebaseAdmin = admin.initializeApp({
        credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
        // Add other Firebase configuration options if required
      });
    
      const fireStore = firebaseAdmin.firestore();
    
      return {
        firebaseAdmin,
        fireStore
      };
    };
    
    export default initializeFirebase;
    

    based on my file structure, I need to use InitializeFirebase method on seperate files. to overcome this issue I've created a seperate function as below. by following this it will check whether there is an initialize Firebase admin instance if so it will use that and return the values else it will initialize and return the values as below.

    import firebase from "./firebase";
    import initializeFirebase from "./firebaseAdmin";
    import admin from 'firebase-admin';
    
    const firebaseAcc = async () => {
        const firebaseApp = firebase().firebaseApp;
        if(admin.apps.length === 0){
    
            const firebase = await initializeFirebase();
            const firebaseAdmin = firebase.firebaseAdmin;
            const fireStore = firebase.fireStore;
        
            return{
                firebaseAdmin,
                firebaseApp,
                fireStore
            }
        }
        const firebaseAdmin = admin.app();
        const fireStore = admin.app().firestore();
    
        return{
            firebaseAdmin,
            firebaseApp,
            fireStore
        }
    }
    
    export default firebaseAcc
    

    When declaring collections to use with Firebase Firestore, It's best to define them with async await approach as below.

    import firebaseAcc from "../firebase/importFirebase";
    
    const initializemyCollection = async () => {
      const firebaseInstance = await firebaseAcc();
      const db= firebaseInstance.fireStore;
      const myCollection = db.collection("*enter collection name Here*");
      return {myCollection};
    };
    
    export default initializemyCollection;
    

    now we can use them in different routes that we define.

    import {
      Request,
      Response,
      NextFunction
    } from 'express';
    
    import initializeUserCollection from '../collections/UserCollection';
    import firebaseAcc from '../firebase/importFirebase';
    
    const userLogin = async (
      req: Request,
      res: Response,
      next: NextFunction
    ) => {
      const {firebaseAdmin} = await firebaseAcc();
      const { userCollection } = await initializeUserCollection();
      /*implement your code below*/
    }
    export {
      userLogin
    }
    

    Hope this Helps anyone that encounters this issue.