Setup Firebase Email Authentication (Web) with Whitelisted emails using Vue.js and Vue Router

December 14, 2019

Setup Firebase (Web)

Create a Firebase project: https://console.firebase.google.com

Register your app with Firebase: Firebase Console -> Project Overview -> + Add app -> Web

Enable Firebase Console -> Authentication -> Sign-in Method: Email / Password

NOTE: You can pre-create users at Users tab and click on Add User.

Add Firebase SDK: https://firebase.google.com/docs/web/setup#add-sdks-initialize. You can do so via Firebase Hosting, CDN, or module bundlers/Node.js.

Vue.js

Since I am using vue-cli, I will use module bundlers.

Go to Vue.js project directory.

npm install --save firebase
npm install --save firebaseui

Edit main.js

import * as firebase from 'firebase/app'

// import 'firebase/analytics';
import 'firebase/auth'
import * as firebaseui from 'firebaseui'
import 'firebaseui/dist/firebaseui.css'

const firebaseConfig = {
  apiKey: "AIza...",
  authDomain: "....firebaseapp.com",
  databaseURL: "https://....firebaseio.com",
  projectId: "...",
  storageBucket: "....appspot.com",
  messagingSenderId: "1020...",
  appId: "1:1020..."
};

firebase.initializeApp(firebaseConfig)

NOTE: Get firebaseConfig from Firebase Console -> Project Overview (Click on Gear Icon) -> Project Settings -> General -> Your apps -> Select your Web apps -> Firebase SDK snippet -> Config.

Edit main.js after firebase.initializeApp(firebaseConfig).

const app = {
  name: 'My App',
  version: '1.0.1',
  // isAuthenticated: firebase.auth().currentUser !== null
  isAuthenticated: false
}

Vue.prototype.$app = app

let initUi = false

function renderUi() {
  if (!initUi) {
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    initUi = true
  }
}

firebase.auth().onAuthStateChanged(user => {
  // user is not enough, as we need custom claims as well
  // app.isAuthenticated = user !== null

  if (user) {
    user.getIdTokenResult()
      .then((idTokenResult) => {
        app.isAuthenticated = idTokenResult.claims.authorized

        renderUi()
      })
  }
  else {
    app.isAuthenticated = false
    renderUi()
  }

  // onAuthStateChanged is slightly delayed,
  // thus we wait for authentication before render to make sure router navigation guard works
  // renderUi
})

Vue Router

I am using vue-router, so I am using Navigation Guards for authentication.

Edit router/index.js

  • If not authorized, redirect to /signin.
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Page from '../views/Page.vue'
import SignIn from '../views/SignIn.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/page/:id',
    component: Page
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/signin',
    name: 'signin',
    component: SignIn
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  }
})

import * as firebase from 'firebase/app'

router.beforeEach((to, from, next) => {
  if (to.fullPath === '/about') {
    next()
    return
  }

  const isAuthenticated = Vue.prototype.$app.isAuthenticated
  if (!isAuthenticated && to.fullPath !== '/signin') next('/signin')
  else next()
})

export default router

SignIn Component

Edit views/SignIn.vue

<template>
  <div>
    <div class="alert alert-danger" role="alert" v-if="error">
      {{ error }} <button type="button" class="btn btn-primary" @click="signIn">Try again</button>
    </div>
    <div id="firebaseui-auth-container"></div>
  </div>
</template>

<script>
import * as firebase from 'firebase/app'
import * as firebaseui from 'firebaseui'

export default {
  name: 'signin',
  data() {
    return {
      error: null,
      from: null,
      uiConfig: {
        callbacks: {
          // @ts-ignore
          signInSuccessWithAuthResult: (authResult, redirectUrl) => {
            // this.$app.isAuthenticated = true
            // this.$router.replace(this.from || '/')

            const user = authResult.user
            this.handleSignIn(user)

            return false // redirect if true
          },
          uiShown() {
             // login from is shown
          }
        },
        // signInFlow: 'popup',
        // signInSuccessUrl: '/',
        signInOptions: [
            {
              provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
              requireDisplayName: false
            }
        ]
      }
    }
  },
  methods: {
    signIn() {
      this.$ui.start('#firebaseui-auth-container', this.uiConfig)
    },
    handleSignIn(user) {
      user.getIdTokenResult(true)
        .then((idTokenResult) => {
          this.$app.isAuthenticated = idTokenResult.claims.authorized

          if (!this.$app.isAuthenticated) { // custom claims not updated/received yet
            // refresh again
            console.log('refresh again')
            this.handleSignIn(user)
          }
          else {
            this.$router.replace(this.from || '/')
          }
        })
        .catch((error) => {
          if (error.code == 'auth/user-token-expired') {
            this.error = `${user.email} is not authorized.`
            this.$app.isAuthenticated = false
            // this.signIn()
          }
        })
    }
  },
  mounted() {
    this.$ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth())
    this.signIn()
  },
  beforeRouteEnter(to, from, next) {
    next(vm => {
      vm.from = from
    })
  }
}
</script>

Cloud Functions

We need a cloud functions to which listen to user account creation and whitelist the emails

  • set custom claims if email is valid
  • delete account if email is invalid
const functions = require('firebase-functions')

const admin = require('firebase-admin')
admin.initializeApp()

// TODO: retrieve authorizeEmails from firestore instead
const authorizeEmails = [
  'test@mydomain.com',
  'admin@mydomain.com'
];

exports.authorizeUserByEmail = functions.auth.user().onCreate(async (user) => {
  const uid = user.uid
  const email = user.email

  if (authorizeEmails.includes(email)) {
    await admin.auth().setCustomUserClaims(uid, {authorized: true})
    console.log(`setCustomUserClaims: ${email}`)
    return true
  }
  else {
    await admin.auth().deleteUser(uid)
    console.log(`Delete user: ${email}`);
    return false
  }
});

NOTE: Learn about write and deploy cloud functions.

More

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.