DEV Community

Cover image for Firebase Authentication: From Zero to Hero ๐Ÿ”
Domenico Tenace for This is Learning

Posted on

Firebase Authentication: From Zero to Hero ๐Ÿ”

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

  1. In your Firebase project, go to "Authentication"
  2. Click "Get started"
  3. Go to "Sign-in method" tab
  4. Enable "Email/Password"
  5. 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 
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Firebase

npm install firebase 
Enter fullscreen mode Exit fullscreen mode

Step 3: Install Additional Dependencies

For better UX, let's install some helpful packages:

npm install @nuxtjs/tailwindcss @headlessui/vue @heroicons/vue 
Enter fullscreen mode Exit fullscreen mode

Add Tailwind to your nuxt.config.ts:

export default defineNuxtConfig({ devtools: { enabled: true }, modules: ['@nuxtjs/tailwindcss'] }) 
Enter fullscreen mode Exit fullscreen mode

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 } } }) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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') } }) 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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() }) 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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') } }) 
Enter fullscreen mode Exit fullscreen mode

Step 2: Update Login/Register Pages

Add the guest middleware to your login and register pages:

definePageMeta({ layout: false, middleware: 'guest' }) 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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' } } 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

Testing Your Authentication ๐Ÿงช

Now let's test our authentication system:

  1. Start your dev server:
 npm run dev 
Enter fullscreen mode Exit fullscreen mode
  1. Test Registration:

    • Go to /register
    • Create a new account
    • Check if you're redirected to dashboard
  2. Test Login:

    • Logout and go to /login
    • Sign in with your credentials
    • Try Google sign-in
  3. Test Protection:

    • Try accessing /dashboard without being logged in
    • Should redirect to login
  4. 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; } } } 
Enter fullscreen mode Exit fullscreen mode

3. Email Verification

Consider implementing email verification:

import { sendEmailVerification } from 'firebase/auth' const sendVerification = async () => { if (user.value) { await sendEmailVerification(user.value) } } 
Enter fullscreen mode Exit fullscreen mode

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 } } } 
Enter fullscreen mode Exit fullscreen mode

Deployment Considerations

When deploying your Nuxt.js app with Firebase Auth:

  1. Environment Variables: Make sure to set your Firebase config variables in your deployment environment

  2. Domain Configuration: Add your production domain to Firebase Auth authorized domains

  3. HTTPS: Always use HTTPS in production (Firebase Auth requires it)

  4. 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 ๐Ÿฅน

Buy Me A Coffee

Top comments (0)