Simple Google Authentication using Vue 3 and Firebase

Firebase Authentication is an easy way for users to sign in to your app without having to remember passwords. It also allows users to use multiple devices at once.

If you are planning on using Vue 3 and Firebase for your next project, then you probably would need to set up some kind of authentication system. In this tutorial, we will be building a simple google authentication composable using the composition-api.

Our focus will mainly be building a feature using the composition-api. We will not focus so much on the UI part.

The source code for this tutorial can be found on github.

Project Setup using Vite

Scaffolding a new project

To kick-start our tutorial we need to scaffold a new Vue project using Vite.

First, we start by running the command:

npm create vite@latest

This will prompt us to type in our project name, and after that we can choose which framework to use, we will be using Vue of course. The next option is whether to use Typescript or not, this is up to you!

Finally, all that is left is to run npm i inside the created folder to install the required dependencies.

We also need to add an alias, @, which will map to src/ by adding a few lines in vue.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  }
})

We can then start our app by running:

npm run dev

Adding Firebase

Since we’re using Firebase in this small project, we need to set it up also, we can start by installing it:

npm i firebase

Then, we need to initialize a new app using our credentials, which can be found in the Firebase dashboard.

We have to store these credentials as environment variables in a .env.local file. That way, the credentials will not be committed to git since that file is ignored:

# fill them with your own credentials
VITE_API_KEY=
VITE_AUTH_DOMAIN=
VITE_PROJECT_ID=
VITE_STORAGE_BUCKET=
VITE_MESSAGING_SENDER_ID=
VITE_APP_ID=

Vite will recognize only the environment variables that start with VITE_.

It is important to note that this is merely a best practice. These credentials are public anyway and can be easily found if one digs a bit in the network tab. To secure our Firebase app in general we need to use Firebase Security Rules.

It is also a good practice to create a .env file, which will have exactly the same environment variables as in .env.local but without any values. This file can then be committed with git and shared. It will be easier for others to copy it, rename it, and then populate the sensitive data.

Next, we will create a new service file firebase.service.ts under src/services, where we will export a setup method that will do the initialization.

import { initializeApp } from 'firebase/app';

const config = {
  apiKey: import.meta.env.API_KEY,
  authDomain: import.meta.env.AUTH_DOMAIN,
  projectId: import.meta.env.PROJECT_ID,
  storageBucket: import.meta.env.STORAGE_BUCKET,
  messagingSenderId: import.meta.env.MESSAGING_SENDER_ID,
  appId: import.meta.env.APP_ID
};

export const setup = () => { initializeApp(firebaseConfig) }

import.meta.env will contain all env variables that start with VITE_.

Finally, we need to call this method from our main file, before we set up a new Vue app:

import { createApp } from 'vue'
import App from './App.vue'
import { setup as setupFirebase } from '@/services/firebase.service'


setupFirebase()

createApp(App).mount('#app')

Vue 3 Auth composable

We can now finally start with the fun part, writing Vue 3 composables! We will leverage the power of the composition-api to create an auth feature, which will:

  • Keep track of auth state and the logged-in user data.
  • Provide a method to trigger Google authentication.
  • Provide a method to log the user out.

To get started, we will create a new composable under src/composables/auth called auth.composable.ts.

Next, we can set up our auth states!

Auth state

Our auth states will be simple, we will keep track of 4 things:

  • isLoading: is the user currently being authenticated?
  • hasFailed: to know if authentication has failed.
  • localUser: will hold the user’s data returned by Firebase.
  • localError: is the error that occurred during authentication, if the authentication has failed.

First, let’s implement and export the composable method, useAuth, with our initial state assigned to default values:

import { User } from 'firebase/auth'
import { Ref, ref } from 'vue'

const localUser: Ref<User | null> = ref(null)

export const useAuth = () => {
  const isLoading = ref(false)
  const hasFailed = ref(false)  
  const localError: Ref<unknown> = ref(null)

  return {
    isLoading,
    hasFailed,
    user: localError,
    error: localError
  }
}

Every state will be a Ref in this case, since we need it to be reactive.

Notice how the localUser ref is outside of our useAuth method, that way it will be “global” and the value will be the same in each component where we use this composable. This works, however, a more sophisticated solution in a larger project would be to use a global state library like Pinia.

Then, we can confirm that this works, by creating a GoogleLogin.vue component, importing the composable there, and using it:

<script setup lang="ts">
  import { useAuth } from '@/composables/auth/auth.composable'

  const auth = useAuth()
</script>

<template>
  <div>
    <ul>
      <li>hasFailed: {{ auth.hasFailed }}</li>
      <li>isLoading: {{ auth.isLoading }}</li>
      <li>error: {{ auth.error }}</li>
      <li>user: <pre>{{ auth.rawUser }}</pre>
      </li>
    </ul>
  </div>
</template>

Finally, we can use that component in our App.vue main component:

<script setup lang="ts">
  import GoogleLogin from './components/GoogleLogin/GoogleLogin.vue'; 
</script>

<template>
  <div :class="$style.wrapper">
    <GoogleLogin />
  </div>
</template>

<style module>
  .wrapper {
    padding: 20px;
  }
</style>

It should render something like this:

Authenticating users

To get authenticated, we need to use signInWithPopup which will open a popup to let us choose the google account we want to log in with, or, signInWithRedirect which will redirect us to a new page and then redirect us back to our app.

To keep things simple, we will use signInWithPopup. However, please note that Firebase recommends actually using redirect login since pop-up has inconsistent behavior on mobile devices.

We will expose a new method from our composable, loginWithGoogle. Within, we will do a few things:

  1. Initialize the identity provider we will be authenticating with, in this case, GoogleAuthProvider.
  2. Set up the loading state to true.
  3. Clear the failure and error states.
  4. Sign in.
  5. Populate the localUser Ref with the user object returned from Firebase.
  6. Handle errors 😎.
import {
  getAuth,
  GoogleAuthProvider,
  signInWithPopup,
  User
} from 'firebase/auth'
import { Ref, ref } from 'vue'

// ...code

export const useAuth = () => {
  //.. code

  const auth = getAuth()

  const loginInWithGoogle = async (): Promise<void> => {
    const provider = new GoogleAuthProvider()

    isLoading.value = true
    hasFailed.value = false
    localError.value = null

    try {
      const result = await signInWithPopup(auth, provider)

      localUser.value = result.user
    } catch(error) {
      hasFailed.value = true
      localError.value = error
    } finally {
      isLoading.value = false
    }
  }

  return {
    // ...code
    loginInWithGoogle,
  }
}

We can now use this method inside our component by calling it when we click on a login button:

<script setup lang="ts">
  import { useAuth } from '@/composables/auth/auth.composable'

  const auth = useAuth()

  const handleLoginWithGoogleClick = () => {
    auth.loginInWithGoogle()
  }
</script>

<template>
  <div>
    <button @click="handleLoginWithGoogleClick">Login with Google</button>

    <br />
    <br />
    <br />

    <ul>
      <li>hasFailed: {{ auth.hasFailed }}</li>
      <li>isLoading: {{ auth.isLoading }}</li>
      <li>error: {{ auth.error }}</li>
      <li>user: {{ auth.user }}</li>
    </ul>
  </div>
</template>

When we click on the button we should be able to log in:

While we’re choosing an account we can see that isLoading is true. Once we choose an account we will be logged in and the user object will be populated.

Watching the authentication state

Since we have logged in, our access token will be stored in the browser. If we leave the website and come back again we will still be authenticated, however, that state is not currently reflected in our app. When we reload we have to log in again, even though we’re technically still logged in. To solve this we need to listen to auth state changes from Firebase.

Firebase provides a method called watchAuthState which will accept a callback and call it when the authentication state changes with the user object when the user is/or gets logged in, and null when the user is not logged in or gets logged out.

The method also returns another method that unregisters this watcher.

import {
  // ...code
  onAuthStateChanged,
} from 'firebase/auth'
import { Ref, ref } from 'vue'

// ..global state
let unwatchAuthState: Unsubscribe = () => {}

export const useAuth = () => {
  // ...code 

  const auth = getAuth()  

  const watchAuthState = () => {
    unwatchAuthState()

    unwatchAuthState = onAuthStateChanged(auth, user => {
      if (user) {
        localUser.value = user
      } else {
        localUser.value = null
      }
    })
  }

  // ...code

  return {
    // ...code
    watchAuthState,
    unwatchAuthState,
  }
}

We call the un-watch method first before watching any auth changes. This way, we will only have one watcher at all times.

Now we have to register this method as early as we need to know the user auth state. That can be done in the main file, for example. In our case, we will use the onBeforeMount lifecycle hook inside our GoogleLogin.vue component to register this watcher:

<script setup lang="ts">
  import { useAuth } from '@/composables/auth/auth.composable'
  import { onBeforeMount, onUnmounted } from 'vue'

  const auth = useAuth()

  onBeforeMount(() => {
    auth.watchAuthState()
  })

  onUnmounted(() => {
    auth.unwatchAuthState()
  })

  const handleLoginWithGoogleClick = () => {
    auth.loginInWithGoogle()
  }
</script>

Notice that we also call auth.unwatchAuthState() when the component gets destroyed (unmounted), this is a best practice and a good thing to do to unregister event handlers and watchers in general when the component is destroyed in order to prevent memory leaks!

Logging out

The final feature in our composable would be to log out. To do that we need to expose a new method called logout, which calls the signOut method from Firebase, and resets the localUser Ref:

import {

  // ...code
  signOut,
} from 'firebase/auth'
import { Ref, ref } from 'vue'

export const useAuth = () => {
  // ...code

  const logout = async () => {
    await signOut(auth)

    localUser.value = null
  }

  return {

    // ...code
    logout,
  }
}

Next, we can call this method when we click on a logout button, we can add that inside App.vue component:

<script setup lang="ts">
  import GoogleLogin from './components/GoogleLogin/GoogleLogin.vue';
  import { useAuth } from '@/composables/auth/auth.composable'

  const auth = useAuth()

  const handleLogoutClick = () => auth.logout()
</script>

<template>
  <div :class="$style.wrapper">
    <button @click="handleLogoutClick">Logout</button>

    <GoogleLogin />
  </div>
</template>

<style module>
  .wrapper {
    padding: 20px;
  }
</style>

Conclusion

In this tutorial we covered only Google authentication, adding more identity providers would be a matter of exposing new methods that use the identity providers classes from Firebase.

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19