DEV Community

loading...
Cover image for To the Stars with Quasar & Firebase - Email Authentication
Quasar

To the Stars with Quasar & Firebase - Email Authentication

adamkpurdy profile image Adam Purdy Updated on ・8 min read

Table Of Contents

  1. Introduction
  2. Firebase Console Setup
  3. Form View
  4. Update Server Connection File
  5. Services
  6. Route Guarding
  7. State/Vuex
  8. Summary
  9. Repository

1. Introduction

This article builds atop of the initial article, Initial Service & Structure published in the Quasar-Firebase series. We're going to look at the most common authentication type in most applications: Email.

  • 1.1 Assumptions

Before we get started, a few assumptions are in order. In this article, we're going to be breaking down the interactions between Firebase services, the serverConnection file, and our base service file. We'll be harnessing our store for state management, so the assumption here is that you have a decent grasp of Vuex and its inner workings. The repository has the complete code base, so that is always a place for a bit of study if you're a little weak in Vuex, as well as keeping Vuex's API docs close at hand.

Implementing Vuex within the context of Quasar can be initially achieved in one of two ways. The first is durining initial project creation via the Quasar CLI prompt:

Alt Text

Or, if you already have a pre-existing application you'll have to set the store up manually. Take a look at the docs for the setup.
You'll have to create the directory structure and use the index.js that is shown in the docs.

Also, keep in mind Quasar offers a nice set of commands via the CLI. One of them being new. You can simply create new modules via the CLI:

$ quasar new store myModule
Enter fullscreen mode Exit fullscreen mode

or

$ quasar n s myModule
Enter fullscreen mode Exit fullscreen mode

Always be sure to update your store's index file to add the new module.

This is also a good spot to point out that we're going to be adding a new context to our application.

Alt Text

Note: This repo already contains a working firebase API key. In order to set up your own project, you need to delete the "FIREBASE_CONFIG" attributes within the .quasar.env.json file and replace it with your own key from the first article.

Be sure to clone the repo and have the app to follow along with. Navigate to the respective app and run:

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

2. Firebase Console Setup

To set up email authentication, you first have to enable it in your project in your Firebase console:

  • In the Firebase console, click on the Authentication menu section.
    Alt Text

  • On the Sign-in method tab, enable the Email/Password sign-in method and click Save.
    Alt Text

3. Form View

Now a form for the user to register or login is needed. Create a page called Auth.vue. This page houses both registration and login, as well as a link to the, forgot password page.

$ quasar new page Auth
Enter fullscreen mode Exit fullscreen mode

In Auth.vue. we've created a basic user form for registering and logging in users.

/src/pages/Auth.vue

Notice the use of the Vuex convenience method mapActions.

  • 3.1 Forgot Password

Before we forget, the user needs to reset their password if needed. Using the quasar-cli create a new page:

$ quasar new page ForgotPassword
Enter fullscreen mode Exit fullscreen mode

/src/pages/forgotPassword.vue

Update our routes file, by adding a new route inside of our /auth path:

const routes = [
...

   {
    path: '/auth',
    component: () => import('layouts/MyLayout.vue'),
    children: [
      {
        path: 'forgotPassword',
        name: 'ForgotPassword',
        component: () => import('pages/ForgotPassword.vue')
      },
...
]
Enter fullscreen mode Exit fullscreen mode

4. Update Server Connection File

Now that the form is in place, we need to start to augment a few more files. First, let's take a look at our serverConnection file:

/src/boot/serverConnection.js

import firebaseService from '../services/firebase'

export default ({ store, Vue }) => {
  const config = process.env.environments.FIREBASE_CONFIG
  firebaseService.fBInit(config)

  // Tell the application what to do when the 
  // authentication state has changed
  firebaseService.auth().onAuthStateChanged((user) => {
    firebaseService.handleOnAuthStateChanged(store, user)
  }, (error) => {
    console.error(error)
  })

  Vue.prototype.$fb = firebaseService
  store.$fb = firebaseService
}
Enter fullscreen mode Exit fullscreen mode

There are two new statements, one for handling the change of the authentication state, and one for controlling the application's rendering process concerning the route.

Here is where it gets a little tricky, and one of the challenging pieces when dealing with Firebase with Quasar. Just a little tinkering is needed to handle this scenario. The next section will talk specifically about what the mechanics are in our base service to halt the rendering of our application while Firebase is initializing.

5. Services

A couple of additions are needed now that we're going to be authenticating users. One is a new email service, and the other is modifying our base service.

  • 5.1 Create the Email Service

Take a look at the new service: email.

/src/services/firebase/email.js

This service is very straight forward and provides an interface to the Firebase auth methods themselves. Add the reference of the email service into our firebaseService object in our index.js file.

/src/services/firebase/index.js

  • 5.2 Update the Base Service

Next, we have some updates on our base service. As mentioned at the end of section 4, things are a little tricky when working with Firebase and not having access to when the Vue app is being initialized. Looking back at our serverConnection boot file, we can now investigate the two statements in the function.

When Firebase's authentication state changes, we pass a store reference and the user state over to the method, handleOnAuthStateChanged.

/** Handle the auth state of the user and
 * set it in the auth store module
 * @param  {Object} store - Vuex Store
 * @param  {Object} currentUser - Firebase currentUser
 */
export const handleOnAuthStateChanged = (store, currentUser) => {
  const initialAuthState = isAuthenticated(store)
  // Save to the store
  store.commit('auth/setAuthState', {
    isAuthenticated: currentUser !== null,
    isReady: true
  })

  // If the user loses authentication route
  // them to the login page
  if (!currentUser && initialAuthState) {
    store.dispatch('auth/routeUserToAuth')
  }
}
Enter fullscreen mode Exit fullscreen mode

First, we want to make an initial authentication check regarding the user with our method, isAuthenticated. Next, we will set an auth object into our auth store module describing the state of the user and the application.

store.commit('auth/setAuthState', {
  isAuthenticated: currentUser !== null,
  isReady: true
})
Enter fullscreen mode Exit fullscreen mode

Doing this moves the internal state away from the service and in a global location in our store. As an application grows in complexity, relying on state in our store is a cleaner and more consistent way.

A convenience if, in the event a user loses their authentication, we route them immediately to the login screen.

if (!currentUser && initialAuthStateSet) {
  store.dispatch('common/routeUserToAuth')
}
Enter fullscreen mode Exit fullscreen mode

You can test this by going in dev tools and removing the auth token provided by Firebase
Alt Text

Next, we have our ensureAuthIsInitialized method that will be utilized in our route guard coming up in our next section.

/**
 * Async function providing the application time to
 * wait for firebase to initialize and determine if a
 * user is authenticated or not with only a single observable
 */
export const ensureAuthIsInitialized = async (store) => {
  if (store.state.auth.isReady) return true
  // Create the observer only once on init
  return new Promise((resolve, reject) => {
    // Use a promise to make sure that the router will eventually show the route after the auth is initialized.
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      resolve()
      unsubscribe()
    }, () => {
      reject(new Error('Looks like there is a problem with the firebase service. Please try again later'))
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

This is what allows us to stay on a requiresAuth page and not route us back to login. Also, note the use of declaring the observable as an assigned function, and then later calling it. unsubscribe() allows us to resolve the auth service once it's done initializing, and then unsubscribe listening to the observable which is useful when dealing with the navigational guards. If this isn't set up this way, you'll end up with multiple observables in memory every time your user is being routed around your app.

6. Route Guarding

Here we are guarding our routes.

/src/router/index.js

  /**
    Setup the router to be intercepted on each route.
    This allows the application to halt rendering until
    Firebase is finished with its initialization process,
    and handle the user accordingly
  **/
  Router.beforeEach(async (to, from, next) => {
    const { ensureAuthIsInitialized, isAuthenticated } = firebaseServices
    try {
      // Force the app to wait until Firebase has
      // finished its initialization, and handle the
      // authentication state of the user properly
      await ensureAuthIsInitialized(store)
      if (to.matched.some(record => record.meta.requiresAuth)) {
        if (isAuthenticated(store)) {
          next()
        } else {
          next('/auth/login')
        }
      } else if ((to.path === '/auth/register' && isAuthenticated(store)) ||
        (to.path === '/auth/login' && isAuthenticated(store))) {
        next('/user')
      } else {
        next()
      }
    } catch (err) {
      Notify.create({
        message: `${err}`,
        color: 'negative'
      })
    }
  })
Enter fullscreen mode Exit fullscreen mode

The route guard can grow and get more complicated as your needs change. Still, the most important thing here is to halt the application from routing until the Firebase service is done initializing by calling ensureAuthIsInitialized.

Note the check if the user is authenticated and trying to navigate to either the login or registration route. The application will not let the user go to either one of those routes and move them directly to the user page for a better user experience.

7 State/Vuex

Since Vuex is available from the initial project creation, we place the authenticated state in our store back in the base service file in the handleOnAuthStateChanged method.

export default {
  isAuthenticated: false,
  isReady: false
}
Enter fullscreen mode Exit fullscreen mode
  • 7.2 Auth Actions

Earlier we mentioned the use of mapActions in our Auth.vue page. Take a look at the methods that can be available via mapActions.

/src/store/auth/actions.js

Also, be sure to take a look at our mutation for our auth store that we are using in our base service during our handleOnAuthStateChanged.

/src/store/auth/mutations.js

8. Summary

Building from our first article, we now have a starting point in which we can successfully create and log in users against the Firebase SDK, as well as reset their password. After a user is successfully authenticated, they are routed to a protected route that is only accessible once a user is authenticated correctly. We've also augmented our base service to allow the Firebase SDK to finish it's initialization process so that we will not route a user back to the login screen if they are already correctly authenticated upon browser refresh.

8. Repository

Quasar-Firebase Repo: Email Authentication

Up next: User Profile

Discussion (2)

pic
Editor guide
Collapse
engineervix profile image
Victor Miti

Thanks @adamkpurdy for this excellent and well written series. It has been very helpful for me! I'm trying to add the phone sign-in option and I'm facing some challenges. Here's how I did it:

1. Create a phone.js file in the services/firebase directory, with the following content:

import firebase from 'firebase/app'
import 'firebase/auth'

/**
 * https://firebase.google.com/docs/reference/js/firebase.auth.Auth.html#signinwithphonenumber
 *
 * @param {String} phoneNumber - A Valid phone number
 * @param {String} applicationVerifier - For abuse prevention
 *
 * @return {Promise} ConfirmationResult
 */
export const authenticateWithPhone = async (phoneNumber, applicationVerifier) => {
  return firebase.auth().signInWithPhoneNumber(phoneNumber, applicationVerifier)
}

Enter fullscreen mode Exit fullscreen mode

2. Add the following to services/firebase/base.js

/**
 * firebase.auth.RecaptchaVerifier
 * https://firebase.google.com/docs/reference/js/firebase.auth.RecaptchaVerifier
 */
export const RecaptchaVerifier = (container, params) => {
  return new firebase.auth.RecaptchaVerifier(container, params)
}
Enter fullscreen mode Exit fullscreen mode

3. Update services/firebase/index.js

import * as base from './base.js'
import * as email from './email.js'
import * as phone from './phone.js'

// export default Object.assign({}, base, email, phone)
Enter fullscreen mode Exit fullscreen mode

4. Add the following to store/auth/actions.js

export const authenticateUser = async function ($root, payload) {
  const $fb = this.$fb
  const { phoneNumber, applicationVerifier } = payload
  return $fb.authenticateWithPhone(phoneNumber, applicationVerifier)
}
Enter fullscreen mode Exit fullscreen mode

Now, In my Signup.vue file, I have a <div id="recaptcha-container"></div> in the <template> section, and the <script> section looks like this

import { mapActions } from "vuex";
import { VueTelInput } from "vue-tel-input";
import "vue-tel-input/dist/vue-tel-input.css";
export default {
  name: "Signup",
  data() {
    return {
      phone: "",
      applicationVerifier: "",
      otp: ""
    };
  },
  methods: {
    ...mapActions("auth", ["authenticateUser"]),
    onSubmit() {
      const { phone, applicationVerifier } = this;
      this.$refs.phoneAuthForm.validate().then(async success => {
        if (success) {
          // do something;
          try {
            await this.authenticateUser({ phone, applicationVerifier });
            this.$router.push({ path: "/" });
          } catch (err) {
            console.error(err);
            this.$q.notify({
              message: `An error has occured: ${err}`,
              color: "negative"
            });
          } finally {
            this.$q.loading.hide();
          }
        }
      });
    },
    initReCaptcha() {
      window.recaptchaVerifier = this.$fb.RecaptchaVerifier(
        "recaptcha-container",
        {
          size: "invisible",
          // eslint-disable-next-line no-unused-vars
          callback: response => {
            // reCAPTCHA solved, allow signInWithPhoneNumber.
            // this.onSubmit();
          }
        }
      );
      this.applicationVerifier = window.recaptchaVerifier;
    }
  },
  created: function() {
    this.initReCaptcha();
  },
  components: {
    VueTelInput
  }
};
Enter fullscreen mode Exit fullscreen mode

However, when I run this, I get the following error in the browser console: Error in created hook: "Error: reCAPTCHA container is either not found or already contains inner elements!"

Any advice on how I can fix this?

Here's a screenshot:

screenshot

Collapse
adamkpurdy profile image
Adam Purdy Author

Hi Victor. I used phone auth a while back on another project. I did a few things differently with success by using the mounted lifecycle method to assign the reCaptcha to the Vue instance.

mounted () {
    this.recaptchaVerifier = this.$fb.recaptcha('phoneNumberSubmit')
  }
Enter fullscreen mode Exit fullscreen mode

Also, based on your code it looks like your passing only a single argument in your authenticateUser call. Where your function signature is set up for two arguments. Try removing your object data structure and just pass the two arguments.

I did a couple of other things differently, but that might be what's holding you up. I have plans on releasing a phone auth article in the future once Quasar v2 gets out of beta. If you have any other problems feel free to come and find me in the Quasar discord. #firebase => @adam (EN/US)