Overview
Hi everyone! ๐
In this article, I'll walk you through how to integrate Firebase Authentication into a Nuxt application. We'll cover everything from the basics of Firebase Auth to building a complete authentication system with login, registration, and protected routes.
Firebase Auth is honestly one of the best authentication solutions out there, it's secure, scalable, and saves you from writing tons of boilerplate code. Plus, it integrates beautifully with Nuxt.js!
Let's start! ๐ค
Choose Your Authentication Method
Firebase Authentication supports multiple sign-in methods, and you can choose the ones that fit your app:
- Email/Password: Classic authentication approach
- Google: Sign in with Google accounts
- Facebook: Social login with Facebook
- GitHub: Perfect for developer-focused apps
- Phone: SMS-based authentication
- Anonymous: For guest users
In this article, I'll show you how to implement email/password authentication and Google sign-in, but the concepts apply to all methods! ๐ฏ
Setup Firebase Project
Before we dive into the code, let's set up our Firebase project.
To setup the project, you can retrieve this article where I talk about it.
Enable Authentication
- In your Firebase project, go to "Authentication"
- Click "Get started"
- Go to "Sign-in method" tab
- Enable "Email/Password"
- Enable "Google" (you'll need to configure OAuth consent screen)
Setting Up Nuxt.js Project
Now let's create our Nuxt.js application and integrate Firebase Auth.
Step 1: Create Nuxt Project
npx nuxi@latest init firebase-auth-app cd firebase-auth-app npm install
Step 2: Install Firebase
npm install firebase
Step 3: Install Additional Dependencies
For better UX, let's install some helpful packages:
npm install @nuxtjs/tailwindcss @headlessui/vue @heroicons/vue
Add Tailwind to your nuxt.config.ts
:
export default defineNuxtConfig({ devtools: { enabled: true }, modules: ['@nuxtjs/tailwindcss'] })
Configure Firebase in Nuxt.js
Let's set up Firebase in our Nuxt application properly.
Step 1: Create Firebase Plugin
Create plugins/firebase.client.ts
:
import { initializeApp } from 'firebase/app' import { getAuth } from 'firebase/auth' export default defineNuxtPlugin(() => { const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, projectId: process.env.FIREBASE_PROJECT_ID, storageBucket: process.env.FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, appId: process.env.FIREBASE_APP_ID } const app = initializeApp(firebaseConfig) const auth = getAuth(app) return { provide: { firebase: app, auth } } })
Step 2: Environment Variables
Create .env
file in your project root:
FIREBASE_API_KEY=your-api-key FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com FIREBASE_PROJECT_ID=your-project-id FIREBASE_STORAGE_BUCKET=your-project.appspot.com FIREBASE_MESSAGING_SENDER_ID=123456789 FIREBASE_APP_ID=your-app-id
Step 3: Create Auth Composable
Create composables/useAuth.ts
:
import { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, User } from 'firebase/auth' export const useAuth = () => { const { $auth } = useNuxtApp() const user = ref<User | null>(null) const loading = ref(true) // Sign in with email and password const signIn = async (email: string, password: string) => { try { const userCredential = await signInWithEmailAndPassword($auth, email, password) return { user: userCredential.user, error: null } } catch (error: any) { return { user: null, error: error.message } } } // Register with email and password const register = async (email: string, password: string) => { try { const userCredential = await createUserWithEmailAndPassword($auth, email, password) return { user: userCredential.user, error: null } } catch (error: any) { return { user: null, error: error.message } } } // Sign in with Google const signInWithGoogle = async () => { try { const provider = new GoogleAuthProvider() const userCredential = await signInWithPopup($auth, provider) return { user: userCredential.user, error: null } } catch (error: any) { return { user: null, error: error.message } } } // Sign out const logout = async () => { try { await signOut($auth) user.value = null await navigateTo('/login') } catch (error) { console.error('Logout error:', error) } } // Listen to auth state changes const initAuth = () => { onAuthStateChanged($auth, (firebaseUser) => { user.value = firebaseUser loading.value = false }) } return { user: readonly(user), loading: readonly(loading), signIn, register, signInWithGoogle, logout, initAuth } }
Create Authentication Pages
Now let's build our authentication UI! ๐จ
Step 1: Login Page
Create pages/login.vue
:
<template> <div class="min-h-screen flex items-center justify-center bg-gray-50"> <div class="max-w-md w-full space-y-8"> <div> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> Sign in to your account </h2> </div> <form class="mt-8 space-y-6" @submit.prevent="handleSignIn"> <div class="space-y-4"> <div> <label class="block text-sm font-medium text-gray-700">Email</label> <input v-model="form.email" type="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /> </div> <div> <label class="block text-sm font-medium text-gray-700">Password</label> <input v-model="form.password" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /> </div> </div> <div v-if="error" class="text-red-600 text-sm">{{ error }}</div> <div> <button type="submit" :disabled="loading" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50" > {{ loading ? 'Signing in...' : 'Sign in' }} </button> </div> <div> <button type="button" @click="handleGoogleSignIn" class="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Sign in with Google </button> </div> <div class="text-center"> <NuxtLink to="/register" class="text-indigo-600 hover:text-indigo-500"> Don't have an account? Sign up </NuxtLink> </div> </form> </div> </div> </template> <script setup lang="ts"> definePageMeta({ layout: false }) const { signIn, signInWithGoogle } = useAuth() const form = reactive({ email: '', password: '' }) const loading = ref(false) const error = ref('') const handleSignIn = async () => { loading.value = true error.value = '' const { user, error: authError } = await signIn(form.email, form.password) if (user) { await navigateTo('/dashboard') } else { error.value = authError || 'Failed to sign in' } loading.value = false } const handleGoogleSignIn = async () => { const { user, error: authError } = await signInWithGoogle() if (user) { await navigateTo('/dashboard') } else { error.value = authError || 'Failed to sign in with Google' } } </script>
Step 2: Registration Page
Create pages/register.vue
:
<template> <div class="min-h-screen flex items-center justify-center bg-gray-50"> <div class="max-w-md w-full space-y-8"> <div> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> Create your account </h2> </div> <form class="mt-8 space-y-6" @submit.prevent="handleRegister"> <div class="space-y-4"> <div> <label class="block text-sm font-medium text-gray-700">Email</label> <input v-model="form.email" type="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /> </div> <div> <label class="block text-sm font-medium text-gray-700">Password</label> <input v-model="form.password" type="password" required minlength="6" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /> </div> <div> <label class="block text-sm font-medium text-gray-700">Confirm Password</label> <input v-model="form.confirmPassword" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /> </div> </div> <div v-if="error" class="text-red-600 text-sm">{{ error }}</div> <div> <button type="submit" :disabled="loading" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50" > {{ loading ? 'Creating account...' : 'Create account' }} </button> </div> <div class="text-center"> <NuxtLink to="/login" class="text-indigo-600 hover:text-indigo-500"> Already have an account? Sign in </NuxtLink> </div> </form> </div> </div> </template> <script setup lang="ts"> definePageMeta({ layout: false }) const { register } = useAuth() const form = reactive({ email: '', password: '', confirmPassword: '' }) const loading = ref(false) const error = ref('') const handleRegister = async () => { if (form.password !== form.confirmPassword) { error.value = 'Passwords do not match' return } loading.value = true error.value = '' const { user, error: authError } = await register(form.email, form.password) if (user) { await navigateTo('/dashboard') } else { error.value = authError || 'Failed to create account' } loading.value = false } </script>
Create Protected Routes
Now let's create a dashboard and set up route protection! ๐ก๏ธ
Step 1: Auth Middleware
Create middleware/auth.ts
:
export default defineNuxtRouteMiddleware((to, from) => { const { user, loading } = useAuth() // Wait for auth to initialize if (loading.value) { return } // Redirect to login if not authenticated if (!user.value) { return navigateTo('/login') } })
Step 2: Dashboard Page
Create pages/dashboard.vue
:
<template> <div class="min-h-screen bg-gray-50"> <nav class="bg-white shadow"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex justify-between h-16"> <div class="flex items-center"> <h1 class="text-xl font-semibold">Dashboard</h1> </div> <div class="flex items-center space-x-4"> <span class="text-gray-700">{{ user?.email }}</span> <button @click="handleLogout" class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700" > Logout </button> </div> </div> </div> </nav> <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> <div class="px-4 py-6 sm:px-0"> <div class="bg-white overflow-hidden shadow rounded-lg"> <div class="px-4 py-5 sm:p-6"> <h2 class="text-lg font-medium text-gray-900 mb-4">Welcome to your dashboard!</h2> <p class="text-gray-600">You are successfully logged in.</p> <div class="mt-6 bg-gray-50 p-4 rounded-lg"> <h3 class="text-sm font-medium text-gray-900 mb-2">User Information:</h3> <p><strong>Email:</strong> {{ user?.email }}</p> <p><strong>UID:</strong> {{ user?.uid }}</p> <p><strong>Email Verified:</strong> {{ user?.emailVerified ? 'Yes' : 'No' }}</p> </div> </div> </div> </div> </main> </div> </template> <script setup lang="ts"> definePageMeta({ middleware: 'auth' }) const { user, logout } = useAuth() const handleLogout = async () => { await logout() } </script>
Initialize Authentication State
We need to initialize our auth state when the app starts.
Step 1: Create Auth Plugin
Create plugins/auth.client.ts
:
export default defineNuxtPlugin(() => { const { initAuth } = useAuth() // Initialize auth state listener initAuth() })
Step 2: Update App.vue
Update your app.vue
to handle loading state:
<template> <div> <div v-if="loading" class="min-h-screen flex items-center justify-center"> <div class="text-center"> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div> <p class="mt-4 text-gray-600">Loading...</p> </div> </div> <NuxtPage v-else /> </div> </template> <script setup lang="ts"> const { loading } = useAuth() </script>
Handle Authentication Flow
Let's add some finishing touches to make our auth flow smooth! โจ
Step 1: Guest Middleware
Create middleware/guest.ts
for login/register pages:
export default defineNuxtRouteMiddleware((to, from) => { const { user } = useAuth() // Redirect to dashboard if already authenticated if (user.value) { return navigateTo('/dashboard') } })
Step 2: Update Login/Register Pages
Add the guest middleware to your login and register pages:
definePageMeta({ layout: false, middleware: 'guest' })
Step 3: Create Home Page
Update pages/index.vue
:
<template> <div class="min-h-screen bg-gray-50 flex items-center justify-center"> <div class="max-w-md w-full text-center"> <h1 class="text-3xl font-bold text-gray-900 mb-8"> Welcome to Our App! </h1> <div class="space-y-4"> <NuxtLink to="/login" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 block" > Sign In </NuxtLink> <NuxtLink to="/register" class="w-full bg-gray-200 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-300 block" > Sign Up </NuxtLink> </div> </div> </div> </template> <script setup lang="ts"> definePageMeta({ layout: false }) </script>
Add Error Handling and Loading States
Let's make our app more robust with better error handling.
Step 1: Enhanced Error Handling
Update your useAuth
composable with better error messages:
const getErrorMessage = (error: any) => { switch (error.code) { case 'auth/user-not-found': return 'No account found with this email' case 'auth/wrong-password': return 'Incorrect password' case 'auth/email-already-in-use': return 'An account with this email already exists' case 'auth/weak-password': return 'Password should be at least 6 characters' case 'auth/invalid-email': return 'Invalid email address' default: return error.message || 'An error occurred' } }
Step 2: Add Loading States
Create a loading component components/LoadingSpinner.vue
:
<template> <div class="flex items-center justify-center"> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div> <span v-if="text" class="ml-2 text-gray-600">{{ text }}</span> </div> </template> <script setup lang="ts"> defineProps<{ text?: string }>() </script>
Testing Your Authentication ๐งช
Now let's test our authentication system:
- Start your dev server:
npm run dev
-
Test Registration:
- Go to
/register
- Create a new account
- Check if you're redirected to dashboard
- Go to
-
Test Login:
- Logout and go to
/login
- Sign in with your credentials
- Try Google sign-in
- Logout and go to
-
Test Protection:
- Try accessing
/dashboard
without being logged in - Should redirect to login
- Try accessing
-
Test State Persistence:
- Refresh the page while logged in
- Should stay logged in
Best Practices and Security Tips ๐
Here are some important security considerations:
1. Environment Variables
Never commit your Firebase config to version control. Always use environment variables!
2. Firebase Security Rules
Set up proper Firestore security rules if you're using Firestore:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
3. Email Verification
Consider implementing email verification:
import { sendEmailVerification } from 'firebase/auth' const sendVerification = async () => { if (user.value) { await sendEmailVerification(user.value) } }
4. Password Reset
Add password reset functionality:
import { sendPasswordResetEmail } from 'firebase/auth' const resetPassword = async (email: string) => { try { await sendPasswordResetEmail($auth, email) return { success: true, error: null } } catch (error: any) { return { success: false, error: error.message } } }
Deployment Considerations
When deploying your Nuxt.js app with Firebase Auth:
Environment Variables: Make sure to set your Firebase config variables in your deployment environment
Domain Configuration: Add your production domain to Firebase Auth authorized domains
HTTPS: Always use HTTPS in production (Firebase Auth requires it)
OAuth Redirect URIs: Configure proper redirect URIs for Google sign-in
Conclusion
And there you have it! ๐ You now have a fully functional authentication system using Firebase Auth and Nuxt.js. We've covered:
- Setting up Firebase Authentication
- Creating login and registration forms
- Implementing Google sign-in
- Protecting routes with middleware
- Handling auth state management
- Error handling and loading states
Firebase Auth takes care of all the heavy lifting - password hashing, session management, security, and scalability. Combined with Nuxt.js, you get a powerful, secure, and user-friendly authentication system.
The best part? This scales from a simple personal project to a full-blown enterprise application without breaking a sweat! ๐ช
Happy coding!โจ
Hi๐๐ป
My name is Domenico, software developer passionate of Open Source, I write article about it for share my knowledge and experience.
Don't forget to visit my Linktree to discover my projects ๐ซฐ๐ป
Linktree: https://linktr.ee/domenicotenace
Follow me on dev.to for other articles ๐๐ป
If you like my content or want to support my work on GitHub, you can support me with a very small donation.
I would be grateful ๐ฅน
Top comments (0)