In this tutorial, you will learn how to implement user authentication in a Vue 3 application using Firebase Authentication. We’ll cover both Email/Password authentication and Google Sign-In, two of the most commonly used login methods in modern web applications.
By the end of this tutorial, you will have a fully functional Vue 3 app with:
-
Firebase Email/Password sign up and login
-
Google login integration
-
User session management
-
Route protection using Vue Router navigation guards
We’ll be using the Composition API, Vite for the development environment, and the latest Firebase JavaScript SDK (version 10 or higher). This tutorial is ideal for beginners who want to quickly and efficiently integrate authentication into their Vue apps.
Prerequisites
Before starting, make sure you have the following installed:
1. Create a Vue 3 Project with Vite
Open your terminal and run:
npm create vite@latest vue3-firebase-auth -- --template vue cd vue3-firebase-auth npm install
Then install the necessary dependencies:
npm install firebase vue-router@4
2. Initialize Firebase Project
-
Go to Firebase Console
-
Click "Add project" and follow the setup wizard
-
Once your project is created, go to Build > Authentication, then:
-
Click "Get started."
-
Enable Email/Password and Google sign-in methods
-
Next, click the gear icon ⚙️ in the sidebar → Project settings → General tab. Scroll down to your apps, choose Web, and register a new app. You’ll be given your Firebase config object like this:
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "your-app.firebaseapp.com",
projectId: "your-app",
storageBucket: "your-app.appspot.com",
messagingSenderId: "XXXXXX",
appId: "YOUR_APP_ID"
};
Copy this config—you’ll need it in the next step.
3. Set Up Firebase in Vue
Create a new file called firebase.js
in the src
folder:
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "your-app.firebaseapp.com",
projectId: "your-app",
storageBucket: "your-app.appspot.com",
messagingSenderId: "XXXXXX",
appId: "YOUR_APP_ID"
}
// Initialize Firebase
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
const provider = new GoogleAuthProvider()
export { auth, provider }
>br />4. Create Authentication Pages
We’ll create two pages:
-
LoginView.vue
– for login with email and Google -
RegisterView.vue
– for new user registration
Create a new folder src/views/
and add the following files:
📄 src/views/LoginView.vue
<template>
<div class="login">
<h2>Login</h2>
<form @submit.prevent="loginWithEmail">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
<button @click="loginWithGoogle">Login with Google</button>
<p>
Don't have an account?
<router-link to="/register">Register</router-link>
</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { auth, provider } from '../firebase'
import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'
import { useRouter } from 'vue-router'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
const loginWithEmail = async () => {
try {
await signInWithEmailAndPassword(auth, email.value, password.value)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
const loginWithGoogle = async () => {
try {
await signInWithPopup(auth, provider)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
</script>
<style scoped>
.login {
max-width: 400px;
margin: auto;
}
.error {
color: red;
}
</style>
📄 src/views/RegisterView.vue
<template>
<div class="register">
<h2>Register</h2>
<form @submit.prevent="register">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Register</button>
</form>
<p>
Already have an account?
<router-link to="/login">Login</router-link>
</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { auth } from '../firebase'
import { createUserWithEmailAndPassword } from 'firebase/auth'
import { useRouter } from 'vue-router'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
const register = async () => {
try {
await createUserWithEmailAndPassword(auth, email.value, password.value)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
</script>
<style scoped>
.register {
max-width: 400px;
margin: auto;
}
.error {
color: red;
}
</style>
5. Set up Routing with Vue Router
Now, let’s create routes and enable navigation between login, register, and dashboard.
📄 src/router.js
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from './views/LoginView.vue'
import RegisterView from './views/RegisterView.vue'
import DashboardView from './views/DashboardView.vue'
import { auth } from './firebase'
const requireAuth = (to, from, next) => {
const user = auth.currentUser
if (user) {
next()
} else {
next('/login')
}
}
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: LoginView },
{ path: '/register', component: RegisterView },
{
path: '/dashboard',
component: DashboardView,
beforeEnter: requireAuth
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
📄 src/views/DashboardView.vue
<template>
<div>
<h2>Welcome, {{ user?.email }}</h2>
<button @click="logout">Logout</button>
</div>
</template>
<script setup>
import { auth } from '../firebase'
import { signOut } from 'firebase/auth'
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
const user = ref(null)
const router = useRouter()
onMounted(() => {
user.value = auth.currentUser
})
const logout = async () => {
await signOut(auth)
router.push('/login')
}
</script>
6. Register the Router in main.js
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
Update App.vue:
<template>
<v-app>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script setup>
// No logic needed here
</script>
<style>
/* Optional global styles */
body {
margin: 0;
font-family: Roboto, sans-serif;
}
</style>
< br />7. Auth State Persistence and Global User State
Let’s listen to Firebase’s auth state changes using onAuthStateChanged
. We'll set this up in a global store-style pattern to keep track of the user and control routing.
🧠 Step 1: Create an Auth Store (src/stores/auth.js
)
// src/stores/auth.js
import { ref } from 'vue'
import { onAuthStateChanged } from 'firebase/auth'
import { auth } from '../firebase'
const currentUser = ref(null)
const isAuthReady = ref(false)
onAuthStateChanged(auth, (user) => {
currentUser.value = user
isAuthReady.value = true
})
export function useAuth() {
return { currentUser, isAuthReady }
}
This will:
-
Keep track of the current user
-
Ensure we don’t try to render protected pages before the auth state is determined
🧠 Step 2: Guard Navigation with Auth Store
Update your route guard in router.js
to wait for the auth state to initialize.
// src/router.js
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from './views/LoginView.vue'
import RegisterView from './views/RegisterView.vue'
import DashboardView from './views/DashboardView.vue'
import { useAuth } from './stores/auth'
const requireAuth = (to, from, next) => {
const { currentUser, isAuthReady } = useAuth()
const waitForAuth = () => {
if (isAuthReady.value) {
currentUser.value ? next() : next('/login')
} else {
setTimeout(waitForAuth, 50)
}
}
waitForAuth()
}
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: LoginView },
{ path: '/register', component: RegisterView },
{
path: '/dashboard',
component: DashboardView,
beforeEnter: requireAuth
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
🧠 Step 3: Use the Auth Store in Components (Optional)
Instead of calling auth.currentUser
directly, you can now import and use the global user state:
import { useAuth } from '../stores/auth'
const { currentUser } = useAuth()
✅ Firebase Default Persistence Mode
By default, Firebase uses local
persistence (i.e., user stays signed in across tabs and sessions). If you want to explicitly set it, update it firebase.js
like this:
import { initializeApp } from 'firebase/app'
import {
getAuth,
GoogleAuthProvider,
setPersistence,
browserLocalPersistence
} from 'firebase/auth'
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
const provider = new GoogleAuthProvider()
// Set local persistence explicitly
setPersistence(auth, browserLocalPersistence)
export { auth, provider }
That’s it! Now your app will:
-
Persist login state across refreshes
-
Wait for Firebase to determine the auth state before routing
-
Allow global access to the current user
8. Add Vuetify to Style the App
📦 Step 1: Install Vuetify 3
Vuetify 3 works seamlessly with Vite and Vue 3.
npm install vuetify@^3.5 @mdi/font sass sass-loader -D
Then update your main.js
:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// Vuetify setup
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
import '@mdi/font/css/materialdesignicons.css'
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
aliases,
sets: { mdi },
},
})
createApp(App).use(router).use(vuetify).mount('#app')
9. Update Auth Pages with Vuetify Components
✨ Replace LoginView.vue
with Vuetify UI
<template>
<v-container class="fill-height" fluid>
<v-row justify="center" align="center">
<v-col cols="12" sm="8" md="4">
<v-card>
<v-card-title class="text-h5 text-center">Login</v-card-title>
<v-card-text>
<v-form @submit.prevent="loginWithEmail">
<v-text-field
v-model="email"
label="Email"
type="email"
required
></v-text-field>
<v-text-field
v-model="password"
label="Password"
type="password"
required
></v-text-field>
<v-btn type="submit" color="primary" block>Login</v-btn>
</v-form>
<v-divider class="my-4"></v-divider>
<v-btn color="red darken-1" block @click="loginWithGoogle">
<v-icon start>mdi-google</v-icon> Login with Google
</v-btn>
<v-alert type="error" v-if="error" class="mt-4">{{ error }}</v-alert>
</v-card-text>
<v-card-actions class="justify-center">
<router-link to="/register">Don't have an account? Register</router-link>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref } from 'vue'
import { auth, provider } from '../firebase'
import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'
import { useRouter } from 'vue-router'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
const loginWithEmail = async () => {
try {
await signInWithEmailAndPassword(auth, email.value, password.value)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
const loginWithGoogle = async () => {
try {
await signInWithPopup(auth, provider)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
</script>
✨ Similarly, update RegisterView.vue
with Vuetify
<template>
<v-container class="fill-height" fluid>
<v-row justify="center" align="center">
<v-col cols="12" sm="8" md="4">
<v-card>
<v-card-title class="text-h5 text-center">Register</v-card-title>
<v-card-text>
<v-form @submit.prevent="register">
<v-text-field
v-model="email"
label="Email"
type="email"
required
></v-text-field>
<v-text-field
v-model="password"
label="Password"
type="password"
required
></v-text-field>
<v-btn type="submit" color="primary" block>Register</v-btn>
</v-form>
<v-alert type="error" v-if="error" class="mt-4">{{ error }}</v-alert>
</v-card-text>
<v-card-actions class="justify-center">
<router-link to="/login">Already have an account? Login</router-link>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { ref } from 'vue'
import { auth } from '../firebase'
import { createUserWithEmailAndPassword } from 'firebase/auth'
import { useRouter } from 'vue-router'
const email = ref('')
const password = ref('')
const error = ref('')
const router = useRouter()
const register = async () => {
try {
await createUserWithEmailAndPassword(auth, email.value, password.value)
router.push('/dashboard')
} catch (err) {
error.value = err.message
}
}
</script>
🧑💼 Optional: Style DashboardView.vue with Vuetify
<template>
<v-container>
<v-card class="pa-4 text-center">
<v-card-title>Welcome, {{ user?.email }}</v-card-title>
<v-card-text>
<v-btn color="primary" @click="logout">Logout</v-btn>
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { auth } from '../firebase'
import { signOut } from 'firebase/auth'
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
const user = ref(null)
const router = useRouter()
onMounted(() => {
user.value = auth.currentUser
})
const logout = async () => {
await signOut(auth)
router.push('/login')
}
</script>
Run the Vue 3 app:
npm run dev
Now, the Vue 3 app looks like this:
10. Conclusion
In this tutorial, you've learned how to build a modern authentication system in Vue 3 using Firebase. We covered how to:
-
Set up a Vue 3 project with Vite
-
Configure Firebase for Email/Password and Google Sign-In
-
Create login, registration, and protected dashboard pages
-
Manage authentication state and session persistence
-
Style the UI using Vuetify 3 components
With these fundamentals, you can now expand this app further by adding profile pages, email verification, password reset, or even Firestore for user data.
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about Vue, you can take the following cheap course:
-
Vuex Vuex with Vue Js Projects to Build Web Application UI
-
Vue 3 and Deno: A Practical Guide
-
Vue 3 Fundamentals Beginners Guide 2023
-
Vue 3, Nuxt. js and NestJS: A Rapid Guide - Advanced
-
Master Vuejs from scratch (incl Vuex, Vue Router)
-
Laravel 11 + Vue 3 + TailwindCSS: Fullstack personal blog.
Happy coding! 🔥