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:
- Initialize the identity provider we will be authenticating with, in this case,
GoogleAuthProvider
. - Set up the loading state to true.
- Clear the failure and error states.
- Sign in.
- Populate the
localUser
Ref with the user object returned from Firebase. - 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.