Skip to content

Firebase

Firebase requires a custom OAuth Authentication Strategy. This is because one is not provided to us, by the default Grant configuration Feathers uses for OAuth.

Since Firebase does not provide a UI for us to redirect to, we use flow #2 outlined in OAuth Flow.

Authentation Setup

Update config/default.json:

json
{
  "authentication": {
    "oauth": {}
  },
  "firebase": {
    "type": "THIS SHOULD BE YOUR SERVICE ACCOUNT",
    "project_id": "GENERATED UNDER FIREBASE CONSOLE",
    "...": "..."
  }
}

Note: Since Firebase can be used for more than just authentication, we'll store our service account in the root of our config. Otherwise, if preferred, you can store under authentication.oauth.

Authentication Strategy

Create a file under src/firebase.js:

js
const firebase = require('firebase-admin');
const { OAuthStrategy } = require('@feathersjs/authentication-oauth');
const { NotAuthenticated } = require('@feathersjs/errors');

const logger = require('./logger');

function initialize(app){
  const firebaseConfig = app.get('firebase');

  // Initialize app
  try {
    firebase.initializeApp({
      credential: firebase.credential.cert(firebaseConfig)
    });
  } catch (e) {
    console.log('erorr initializing firebase', e);
  }
}

class FirebaseStrategy extends OAuthStrategy {

  async authenticate(authentication, params){
    logger.debug('firebase:strategy:authenticate');
    return super.authenticate(authentication, params);
  }

  async getProfile(data, _params){
    const firebase = require('firebase-admin');
    let user;

    try {
      user = await firebase.auth().verifyIdToken(data.access_token);
    } catch(e){
      logger.error(e);
      throw new NotAuthenticated();
    }

    logger.debug(`firebase:strategy:getProfile:successful ${user.user_id}`);

    return {
      email: user.email,
      id: user.user_id
    };
  }

  async getEntityData(profile) {
    const baseData = await super.getEntityData(profile);

    return {
      ...baseData,
      email: profile.email
    };
  }
}

module.exports = { initialize, FirebaseStrategy };

Now we can edit src/authentication.js

js
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { expressOauth } = require('@feathersjs/authentication-oauth');

const { FirebaseStrategy } = require('./firebase');

module.exports = app => {
  const authentication = new AuthenticationService(app);

  authentication.register('firebase', new FirebaseStrategy());

  app.use('/authentication', authentication);
  app.configure(expressOauth());
};

Building frontend

To save time, you can leverage the pre-built UI provided by Firebase UI.

Create auth page

First, create a public/firebase_auth.html file that initializes everything we'll need for our different auth components.

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Firebase Authentication Example</title>
  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/7.21.0/firebase-app.js"></script>

  <!-- TODO: Add SDKs for Firebase products that you want to use
       https://firebase.google.com/docs/web/setup#available-libraries -->
  <script src="https://www.gstatic.com/firebasejs/7.21.0/firebase-auth.js"></script>

  <!-- Firebase UI -->
  <script src="https://www.gstatic.com/firebasejs/ui/4.6.1/firebase-ui-auth.js"></script>
  <link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.6.1/firebase-ui-auth.css" />

</head>
<body>
  <!-- The surrounding HTML is left untouched by FirebaseUI.
      Your app may use that space for branding, controls and other customizations.-->
  <h1>Welcome to My Awesome App</h1>

  <!-- Optionally show a preparing state, until the guest or member app is ready. Usually after authentication is determined -->
  <div id="app-preparing"></div>

  <!-- App for guests to auth, etc. -->
  <div id="app-guest" style="display: none;">
    <div id="firebaseui-auth-container"></div>
    <div id="loader">Loading...</div>
  </div>

  <!-- App for members only -->
  <div id="app-member" style="display: none;"></div>

  <!-- Custom -->
  <script src="//unpkg.com/@feathersjs/client@^4.3.0/dist/feathers.js"></script>
  <script src="/socket.io/socket.io.js"></script>
  <script src="/client.js"></script>
</body>
</html>

Initialize client w/Firebase auth

Now, let's make a public/client.js file where all of our JavaScript will live.

Be sure to update firebaseConfig with the one provided from your Firebase Console. Additionally, checkout Firebase UI docs for more information on customizing ui.start. This includes theming options, all providers supported by Firebase & more.

js
let client, ui;

init();

function init(){
  initializeFeathers();
  initializeAuth();
  initializeFirebase();
}

function initializeFeathers(){
  // Establish a Socket.io connection
  const socket = io();
  // Initialize our Feathers client application through Socket.io
  // with hooks and authentication.
  client = feathers();

  client.configure(feathers.socketio(socket));
  // Use localStorage to store our login token
  client.configure(feathers.authentication());
}

// Either re-authenticate existing session, or start Firebase UI
async function initializeAuth(){
  try {
    await client.reAuthenticate();
    showMemberApp();
  } catch(e){
    // Error re-authenticating, so let's start Firebase UI
    showGuestApp();
  }

  // No longer need to prepare anything
  document.getElementById('app-preparing').style.display = 'none';
}

function initializeFirebase(){
  // Your web app's Firebase configuration
  // For Firebase JS SDK v7.20.0 and later, measurementId is optional
  var firebaseConfig = {
    // Copy this from your Firebase Console
    // Under Project Settings -> Web App
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

  // Initialize the FirebaseUI Widget using Firebase.
  ui = new firebaseui.auth.AuthUI(firebase.auth());
}


async function showMemberApp(){
  // Get user information
  const { user } = await client.get('authentication');

  // Hide Guest App
  document.getElementById('app-guest').style.display = 'none';

  // Show member app
  document.getElementById('app-member').style.display = 'block';
  document.getElementById('app-member').innerHTML = `Logged in as, ${user.email}. <a href="#" id="logout">Logout</a>`;

}

function showGuestApp(){
  // Hide & clear member app
  document.getElementById('app-member').style.display = 'none';
  document.getElementById('app-member').innerHTML = '';

  // Show Guest app
  document.getElementById('app-guest').style.display = 'block';
  startFirebaseUI();
}

function startFirebaseUI(){
  ui.start('#firebaseui-auth-container', {
    callbacks: {
      signInSuccessWithAuthResult: function(authResult, redirectUrl) {
        // User successfully signed in.
        // Return type determines whether we continue the redirect automatically
        // or whether we leave that to developer to handle.
        firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(async function(idToken) {
          await client.authenticate({
            strategy: 'firebase',
            access_token: idToken,
          });
          showMemberApp();
        });

        return false;
      },
      uiShown: function() {
        // The widget is rendered.
        // Hide the loader.
        document.getElementById('loader').style.display = 'none';
      }
    },
    // Will use popup for IDP Providers sign-in flow instead of the default, redirect.
    signInFlow: 'popup',
    credentialHelper: firebaseui.auth.CredentialHelper.NONE, // disable accountchooter.com helper
    signInOptions: [
      firebase.auth.EmailAuthProvider.PROVIDER_ID,
      firebase.auth.FacebookAuthProvider.PROVIDER_ID,
      firebase.auth.TwitterAuthProvider.PROVIDER_ID,
    ],
    // Other config options...
  });
}

const addEventListener = (selector, event, handler) => {
  document.addEventListener(event, async ev => {
    if (ev.target.closest(selector)) {
      handler(ev);
    }
  });
};

// "Logout" button click handler
addEventListener('#logout', 'click', async () => {
  await client.logout();

  showGuestApp();
});

Now you should be able to visit your Firebase auth at the

http://localhost:3030/firebase_auth.html

page locally and authenticate w/any Firebase Providers you've set up in your Firebase Project 🔥

Released under the MIT License.