Firebase Authentication + Angular: Complete Integration Guide
Development

Firebase Authentication + Angular: Complete Integration Guide

Ricardo Gil
January 4, 2026
35 min read
#Angular #Firebase #Authentication #TypeScript #Web Development #Security #Tutorial

Introduction

Authentication is one of those features every application needs, but implementing it securely can be surprisingly complex. You need to handle password hashing, session management, email verification, password resets, social logins, and a dozen other edge cases.

That's where Firebase Authentication shines. It handles all the heavy lifting—secure password storage, OAuth providers, email verification, and more—while you focus on building your actual application.

In this guide, I'll walk you through implementing complete Firebase Authentication in Angular 18, from initial setup to production deployment. This isn't a basic "hello world" tutorial—we're building a production-ready authentication system with:

  • ✅ Email/password registration and login
  • ✅ Google Sign-In (OAuth)
  • ✅ Password reset flow
  • ✅ Email verification
  • ✅ Route guards (public, authenticated, admin-only)
  • ✅ User profile management
  • ✅ Role-based access control
  • ✅ Persistent sessions
  • ✅ Proper error handling
  • ✅ Loading states
  • By the end, you'll have authentication that actually works in production, complete with all the features users expect from modern web applications.

    Tech Stack:

  • Angular 18 (with standalone components)
  • Firebase 10
  • Angular Fire 18
  • TypeScript
  • Tailwind CSS (optional, for styling)
  • Let's build this.

    Why Firebase Authentication?

    Before we dive into code, let's talk about why Firebase Auth is worth using:

    Security Out of the Box:

  • Password hashing with bcrypt
  • Protection against brute force attacks
  • Built-in rate limiting
  • OAuth 2.0 compliance for social logins
  • Secure token-based sessions
  • Time Savings:

  • No need to build user tables
  • No password reset email infrastructure
  • No OAuth integration headaches
  • No security audits for auth code
  • Cost:

  • Free tier: 10,000 verifications/month
  • After that: $0.01 per verification
  • For most apps, this means essentially free
  • Downsides:

  • Vendor lock-in (migrating users is possible but annoying)
  • Less control over the auth flow
  • Requires internet connection (no offline-first auth)
  • For most applications, these trade-offs are absolutely worth it. Now let's build it.

    Part 1: Firebase Project Setup

    Step 1: Create Firebase Project

    1. Go to Firebase Console: https://console.firebase.google.com 2. Click "Add Project" 3. Enter project name: (e.g., "my-angular-app") 4. Disable Google Analytics (unless you want it) 5. Click "Create Project"

    Step 2: Enable Authentication Methods

    1. In Firebase Console, go to "Build" → "Authentication" 2. Click "Get Started" 3. Go to "Sign-in method" tab 4. Enable "Email/Password" - Toggle it on - Click "Save" 5. Enable "Google" - Toggle it on - Enter project support email - Click "Save"

    Step 3: Create Firestore Database

    We'll store additional user data in Firestore (name, role, profile info):

    1. Go to "Build" → "Firestore Database" 2. Click "Create Database" 3. Choose "Start in production mode" 4. Select a location (choose closest to your users) 5. Click "Enable"

    Step 4: Configure Security Rules

    In Firestore, go to "Rules" tab and add:

    javascript
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        // Helper function to check if user is authenticated
        function isSignedIn() {
          return request.auth != null;
        }
        
        // Helper function to check if user is admin
        function isAdmin() {
          return isSignedIn() && 
                 get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'Admin';
        }
        
        // Users collection
        match /users/{userId} {
          // Users can read their own document
          allow read: if isSignedIn() && request.auth.uid == userId;
          
          // Users can create their own document during registration
          allow create: if isSignedIn() && request.auth.uid == userId;
          
          // Users can update their own document (but not role)
          allow update: if isSignedIn() && 
                           request.auth.uid == userId && 
                           request.resource.data.role == resource.data.role;
          
          // Only the user themselves can delete their account
          allow delete: if isSignedIn() && request.auth.uid == userId;
        }
        
        // Blog posts collection (example)
        match /blog-posts/{postId} {
          allow read: if true; // Public
          allow create, update, delete: if isAdmin(); // Admin only
        }
      }
    }

    These rules ensure:

  • Users can only access their own data
  • Users can't promote themselves to admin
  • Blog posts are public but only admins can create/edit
  • Step 5: Get Firebase Config

    1. Go to Project Settings (gear icon) 2. Scroll to "Your apps" 3. Click web icon () 4. Register your app 5. Copy the Firebase configuration object

    It will look like:

    typescript
    const firebaseConfig = {
      apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      authDomain: "your-app.firebaseapp.com",
      projectId: "your-app",
      storageBucket: "your-app.appspot.com",
      messagingSenderId: "123456789",
      appId: "1:123456789:web:abcdefghijk"
    };

    Keep this safe—we'll use it in a moment.

    Part 2: Angular Project Setup

    Step 1: Create Angular Project

    bash
    # Create new Angular project
    ng new my-angular-app --standalone

    Navigate to project

    cd my-angular-app

    Install Firebase and AngularFire

    npm install firebase @angular/fire

    Step 2: Configure Firebase in Angular

    Create environment files:

    bash
    # Create environment files
    ng generate environments

    src/environments/environment.ts:

    typescript
    export const environment = {
      production: false,
      firebase: {
        apiKey: "YOUR_API_KEY",
        authDomain: "your-app.firebaseapp.com",
        projectId: "your-app",
        storageBucket: "your-app.appspot.com",
        messagingSenderId: "123456789",
        appId: "1:123456789:web:abcdefghijk"
      }
    };

    src/environments/environment.production.ts:

    typescript
    export const environment = {
      production: true,
      firebase: {
        // Same config as above
      }
    };

    IMPORTANT: Add environment.ts to .gitignore:

    bash
    # In .gitignore
    /src/environments/environment.ts
    /src/environments/environment.*.ts
    !src/environments/environment.example.ts

    Create environment.example.ts with placeholder values to commit to Git.

    Step 3: Configure App

    src/app/app.config.ts:

    typescript
    import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
    import { provideRouter } from '@angular/router';
    import { provideHttpClient } from '@angular/common/http';
    import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
    import { provideAuth, getAuth } from '@angular/fire/auth';
    import { provideFirestore, getFirestore } from '@angular/fire/firestore';

    import { routes } from './app.routes'; import { environment } from '../environments/environment';

    export const appConfig: ApplicationConfig = { providers: [ provideZonelessChangeDetection(), provideRouter(routes), provideHttpClient(), provideFirebaseApp(() => initializeApp(environment.firebase)), provideAuth(() => getAuth()), provideFirestore(() => getFirestore()) ] };

    This sets up:

  • Firebase app initialization
  • Auth module
  • Firestore module
  • HTTP client for API calls
  • Step 4: Create User Model

    src/app/models/user.model.ts:

    typescript
    export interface User {
      uid: string;
      email: string;
      displayName: string;
      firstName: string;
      lastName: string;
      photoURL?: string;
      emailVerified: boolean;
      role?: 'User' | 'Admin';
      createdAt?: Date;
      updatedAt?: Date;
    }

    export interface UserRegistration { email: string; password: string; firstName: string; lastName: string; }

    export interface UserProfile { firstName?: string; lastName?: string; displayName?: string; photoURL?: string; }

    Part 3: Authentication Service

    This is the heart of our auth system. It handles all Firebase interactions.

    src/app/services/auth.service.ts:

    typescript
    import { Injectable, inject, signal } from '@angular/core';
    import {
      Auth,
      createUserWithEmailAndPassword,
      signInWithEmailAndPassword,
      signInWithPopup,
      GoogleAuthProvider,
      signOut,
      sendPasswordResetEmail,
      updateProfile,
      deleteUser,
      sendEmailVerification,
      user
    } from '@angular/fire/auth';
    import {
      Firestore,
      doc,
      setDoc,
      getDoc,
      updateDoc,
      deleteDoc,
      serverTimestamp
    } from '@angular/fire/firestore';
    import { Router } from '@angular/router';
    import { User, UserRegistration, UserProfile } from '../models/user.model';

    @Injectable({ providedIn: 'root' }) export class AuthService { private auth = inject(Auth); private firestore = inject(Firestore); private router = inject(Router);

    // Reactive state using signals currentUser = signal<User | null>(null); loading = signal<boolean>(true); private user$ = user(this.auth);

    constructor() { // Listen to auth state changes this.user$.subscribe(async (firebaseUser) => { try { if (firebaseUser) { // User is signed in const userData = await this.getUserData(firebaseUser.uid); this.currentUser.set(userData); } else { // User is signed out this.currentUser.set(null); } } catch (error) { console.error('Error in auth state change:', error); this.currentUser.set(null); } finally { this.loading.set(false); } }); }

    // Register with email and password async register(registration: UserRegistration): Promise<void> { try { // Create Firebase Auth user const userCredential = await createUserWithEmailAndPassword( this.auth, registration.email, registration.password );

    const displayName = ${registration.firstName} ${registration.lastName};

    // Update Firebase Auth profile await updateProfile(userCredential.user, { displayName });

    // Create Firestore user document const userData: User = { uid: userCredential.user.uid, email: registration.email, displayName, firstName: registration.firstName, lastName: registration.lastName, photoURL: '', emailVerified: false, role: 'User' };

    await setDoc(doc(this.firestore, 'users', userCredential.user.uid), { ...userData, createdAt: serverTimestamp(), updatedAt: serverTimestamp() });

    // Navigate to dashboard this.router.navigate(['/dashboard']); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Sign in with email and password async signIn(email: string, password: string): Promise<void> { try { await signInWithEmailAndPassword(this.auth, email, password); // Wait for currentUser signal to update let attempts = 0; while (!this.currentUser() && attempts < 20) { await new Promise(resolve => setTimeout(resolve, 100)); attempts++; }

    this.router.navigate(['/dashboard']); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Sign in with Google async signInWithGoogle(): Promise<void> { try { const provider = new GoogleAuthProvider(); const result = await signInWithPopup(this.auth, provider);

    // Ensure user document exists in Firestore await this.ensureUserDocument(result.user);

    this.router.navigate(['/dashboard']); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Sign out async logout(): Promise<void> { await signOut(this.auth); this.router.navigate(['/']); }

    // Send password reset email async resetPassword(email: string): Promise<void> { try { await sendPasswordResetEmail(this.auth, email); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Send verification email async sendVerificationEmail(): Promise<void> { const user = this.auth.currentUser; if (!user) throw new Error('No user logged in');

    try { await sendEmailVerification(user); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Update user profile async updateUserProfile(profile: UserProfile): Promise<void> { const user = this.auth.currentUser; if (!user) throw new Error('No user logged in');

    try { // Update Firebase Auth profile if displayName changed if (profile.firstName || profile.lastName) { const displayName = ${profile.firstName || ''} ${profile.lastName || ''}.trim(); await updateProfile(user, { displayName }); }

    // Update Firestore document await updateDoc(doc(this.firestore, 'users', user.uid), { ...profile, updatedAt: serverTimestamp() });

    // Refresh current user data const userData = await this.getUserData(user.uid); this.currentUser.set(userData); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Delete user account async deleteAccount(): Promise<void> { const user = this.auth.currentUser; if (!user) throw new Error('No user logged in');

    try { // Delete Firestore document await deleteDoc(doc(this.firestore, 'users', user.uid));

    // Delete Firebase Auth user await deleteUser(user);

    this.router.navigate(['/']); } catch (error: any) { throw new Error(this.getErrorMessage(error.code)); } }

    // Ensure user document exists in Firestore (for OAuth logins) private async ensureUserDocument(firebaseUser: any): Promise<void> { const userDocRef = doc(this.firestore, 'users', firebaseUser.uid); const userDoc = await getDoc(userDocRef);

    if (!userDoc.exists()) { const names = firebaseUser.displayName?.split(' ') || ['', '']; const userData: User = { uid: firebaseUser.uid, email: firebaseUser.email!, displayName: firebaseUser.displayName || '', firstName: names[0], lastName: names.slice(1).join(' '), photoURL: firebaseUser.photoURL || '', emailVerified: firebaseUser.emailVerified, role: 'User' };

    await setDoc(userDocRef, { ...userData, createdAt: serverTimestamp(), updatedAt: serverTimestamp() }); } }

    // Get user data from Firestore private async getUserData(uid: string): Promise<User> { const userDoc = await getDoc(doc(this.firestore, 'users', uid));

    if (userDoc.exists()) { const data = userDoc.data(); return { ...data, uid, role: data['role'] || 'User', createdAt: data['createdAt']?.toDate(), updatedAt: data['updatedAt']?.toDate() } as User; }

    // Fallback if no Firestore doc exists const firebaseUser = this.auth.currentUser!; const names = firebaseUser.displayName?.split(' ') || ['', '']; return { uid: firebaseUser.uid, email: firebaseUser.email!, displayName: firebaseUser.displayName || '', firstName: names[0], lastName: names.slice(1).join(' '), photoURL: firebaseUser.photoURL || '', emailVerified: firebaseUser.emailVerified, role: 'User' } as User; }

    // User-friendly error messages private getErrorMessage(code: string): string { switch (code) { case 'auth/email-already-in-use': return 'Email already in use'; case 'auth/invalid-email': return 'Invalid email address'; case 'auth/weak-password': return 'Password is too weak (minimum 6 characters)'; case 'auth/user-disabled': return 'User account has been disabled'; case 'auth/user-not-found': return 'User not found'; case 'auth/wrong-password': return 'Invalid email or password'; case 'auth/popup-closed-by-user': return 'Sign in cancelled'; case 'auth/requires-recent-login': return 'Please log in again to perform this action'; default: return 'An error occurred. Please try again'; } } }

    Key Features:

  • ✅ Signals for reactive state
  • ✅ Automatic sync between Firebase Auth and Firestore
  • ✅ Google OAuth support
  • ✅ Proper error handling
  • ✅ User-friendly error messages
  • Part 4: Route Guards

    Guards protect routes from unauthorized access.

    src/app/guards/auth.guard.ts:

    typescript
    import { inject } from '@angular/core';
    import { Router, CanActivateFn } from '@angular/router';
    import { AuthService } from '../services/auth.service';

    // Protects routes that require authentication export const authGuard: CanActivateFn = async (route, state) => { const authService = inject(AuthService); const router = inject(Router);

    // Wait for auth to finish loading let attempts = 0; while (authService.loading() && attempts < 50) { await new Promise(resolve => setTimeout(resolve, 50)); attempts++; }

    if (authService.currentUser()) { return true; }

    // Redirect to login with return URL router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return false; };

    // Prevents authenticated users from accessing auth pages export const publicGuard: CanActivateFn = async (route, state) => { const authService = inject(AuthService); const router = inject(Router);

    // Wait for auth to finish loading let attempts = 0; while (authService.loading() && attempts < 50) { await new Promise(resolve => setTimeout(resolve, 50)); attempts++; }

    if (!authService.currentUser()) { return true; }

    // Already logged in, redirect to dashboard router.navigate(['/dashboard']); return false; };

    src/app/guards/admin.guard.ts:

    typescript
    import { inject } from '@angular/core';
    import { Router, CanActivateFn } from '@angular/router';
    import { AuthService } from '../services/auth.service';

    // Protects routes that require admin role export const adminGuard: CanActivateFn = async (route, state) => { const authService = inject(AuthService); const router = inject(Router);

    // Wait for auth to finish loading let attempts = 0; while (authService.loading() && attempts < 50) { await new Promise(resolve => setTimeout(resolve, 50)); attempts++; }

    // Check if user is authenticated if (!authService.currentUser()) { router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return false; }

    // Check if user has admin role const user = authService.currentUser(); if (user && user.role === 'Admin') { return true; }

    // User is authenticated but not an admin console.warn('Access denied: User does not have admin role'); router.navigate(['/dashboard']); return false; };

    Part 5: Login Component

    src/app/pages/login/login.ts:

    typescript
    import { Component, signal } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    import { RouterLink, Router, ActivatedRoute } from '@angular/router';
    import { AuthService } from '../../services/auth.service';

    @Component({ selector: 'app-login', standalone: true, imports: [CommonModule, FormsModule, RouterLink], templateUrl: './login.html' }) export class LoginComponent { email = ''; password = ''; error = signal<string>(''); loading = signal<boolean>(false); private returnUrl: string = '/dashboard';

    constructor( private authService: AuthService, private route: ActivatedRoute ) { // Get return URL from query params this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'; }

    async onSubmit() { if (!this.email || !this.password) { this.error.set('Please fill in all fields'); return; }

    this.loading.set(true); this.error.set('');

    try { await this.authService.signIn(this.email, this.password); // AuthService handles navigation } catch (err: any) { this.error.set(err.message); } finally { this.loading.set(false); } }

    async signInWithGoogle() { this.loading.set(true); this.error.set('');

    try { await this.authService.signInWithGoogle(); // AuthService handles navigation } catch (err: any) { this.error.set(err.message); } finally { this.loading.set(false); } } }

    src/app/pages/login/login.html:

    html
    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
      <div class="max-w-md w-full space-y-8">
        <div>
          <h2 class="text-center text-3xl font-bold text-gray-900">
            Sign in to your account
          </h2>
        </div>

    <!-- Error Message --> @if (error()) { <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded"> {{ error() }} </div> }

    <!-- Email/Password Form --> <form (ngSubmit)="onSubmit()" class="space-y-6"> <div> <label for="email" class="block text-sm font-medium text-gray-700"> Email address </label> <input id="email" name="email" type="email" [(ngModel)]="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="you@example.com" /> </div>

    <div> <label for="password" class="block text-sm font-medium text-gray-700"> Password </label> <input id="password" name="password" type="password" [(ngModel)]="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="••••••••" /> </div>

    <div class="flex items-center justify-between"> <div class="text-sm"> <a routerLink="/forgot-password" class="text-blue-600 hover:text-blue-500"> Forgot your password? </a> </div> </div>

    <button type="submit" [disabled]="loading()" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50" > @if (loading()) { <span>Signing in...</span> } @else { <span>Sign in</span> } </button> </form>

    <!-- Divider --> <div class="relative"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-300"></div> </div> <div class="relative flex justify-center text-sm"> <span class="px-2 bg-gray-50 text-gray-500">Or continue with</span> </div> </div>

    <!-- Google Sign In --> <button (click)="signInWithGoogle()" [disabled]="loading()" class="w-full flex items-center justify-center gap-3 py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50" > <svg class="w-5 h-5" viewBox="0 0 24 24"> <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/> <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/> <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/> <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/> </svg> Sign in with Google </button>

    <!-- Register Link --> <div class="text-center"> <span class="text-sm text-gray-600">Don't have an account? </span> <a routerLink="/register" class="text-sm text-blue-600 hover:text-blue-500 font-medium"> Sign up </a> </div> </div> </div>

    Part 6: Register Component

    src/app/pages/register/register.ts:

    typescript
    import { Component, signal } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    import { RouterLink } from '@angular/router';
    import { AuthService } from '../../services/auth.service';
    import { UserRegistration } from '../../models/user.model';

    @Component({ selector: 'app-register', standalone: true, imports: [CommonModule, FormsModule, RouterLink], templateUrl: './register.html' }) export class RegisterComponent { firstName = ''; lastName = ''; email = ''; password = ''; confirmPassword = ''; error = signal<string>(''); loading = signal<boolean>(false);

    constructor(private authService: AuthService) {}

    async onSubmit() { // Validation if (!this.firstName || !this.lastName || !this.email || !this.password) { this.error.set('Please fill in all fields'); return; }

    if (this.password !== this.confirmPassword) { this.error.set('Passwords do not match'); return; }

    if (this.password.length < 6) { this.error.set('Password must be at least 6 characters'); return; }

    this.loading.set(true); this.error.set('');

    try { const registration: UserRegistration = { email: this.email, password: this.password, firstName: this.firstName, lastName: this.lastName };

    await this.authService.register(registration); // AuthService handles navigation } catch (err: any) { this.error.set(err.message); } finally { this.loading.set(false); } } }

    src/app/pages/register/register.html:

    html
    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
      <div class="max-w-md w-full space-y-8">
        <div>
          <h2 class="text-center text-3xl font-bold text-gray-900">
            Create your account
          </h2>
        </div>

    @if (error()) { <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded"> {{ error() }} </div> }

    <form (ngSubmit)="onSubmit()" class="space-y-6"> <div class="grid grid-cols-2 gap-4"> <div> <label for="firstName" class="block text-sm font-medium text-gray-700"> First name </label> <input id="firstName" name="firstName" type="text" [(ngModel)]="firstName" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" /> </div>

    <div> <label for="lastName" class="block text-sm font-medium text-gray-700"> Last name </label> <input id="lastName" name="lastName" type="text" [(ngModel)]="lastName" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" /> </div> </div>

    <div> <label for="email" class="block text-sm font-medium text-gray-700"> Email address </label> <input id="email" name="email" type="email" [(ngModel)]="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" /> </div>

    <div> <label for="password" class="block text-sm font-medium text-gray-700"> Password </label> <input id="password" name="password" type="password" [(ngModel)]="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" /> </div>

    <div> <label for="confirmPassword" class="block text-sm font-medium text-gray-700"> Confirm password </label> <input id="confirmPassword" name="confirmPassword" type="password" [(ngModel)]="confirmPassword" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" /> </div>

    <button type="submit" [disabled]="loading()" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50" > @if (loading()) { <span>Creating account...</span> } @else { <span>Sign up</span> } </button> </form>

    <div class="text-center"> <span class="text-sm text-gray-600">Already have an account? </span> <a routerLink="/login" class="text-sm text-blue-600 hover:text-blue-500 font-medium"> Sign in </a> </div> </div> </div>

    Part 7: Forgot Password Component

    src/app/pages/forgot-password/forgot-password.ts:

    typescript
    import { Component, signal } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    import { RouterLink } from '@angular/router';
    import { AuthService } from '../../services/auth.service';

    @Component({ selector: 'app-forgot-password', standalone: true, imports: [CommonModule, FormsModule, RouterLink], templateUrl: './forgot-password.html' }) export class ForgotPasswordComponent { email = ''; error = signal<string>(''); success = signal<boolean>(false); loading = signal<boolean>(false);

    constructor(private authService: AuthService) {}

    async onSubmit() { if (!this.email) { this.error.set('Please enter your email'); return; }

    this.loading.set(true); this.error.set(''); this.success.set(false);

    try { await this.authService.resetPassword(this.email); this.success.set(true); this.email = ''; } catch (err: any) { this.error.set(err.message); } finally { this.loading.set(false); } } }

    src/app/pages/forgot-password/forgot-password.html:

    html
    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
      <div class="max-w-md w-full space-y-8">
        <div>
          <h2 class="text-center text-3xl font-bold text-gray-900">
            Reset your password
          </h2>
          <p class="mt-2 text-center text-sm text-gray-600">
            Enter your email and we'll send you a password reset link
          </p>
        </div>

    @if (success()) { <div class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded"> Password reset email sent! Check your inbox. </div> }

    @if (error()) { <div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded"> {{ error() }} </div> }

    <form (ngSubmit)="onSubmit()" class="space-y-6"> <div> <label for="email" class="block text-sm font-medium text-gray-700"> Email address </label> <input id="email" name="email" type="email" [(ngModel)]="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="you@example.com" /> </div>

    <button type="submit" [disabled]="loading()" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50" > @if (loading()) { <span>Sending...</span> } @else { <span>Send reset link</span> } </button> </form>

    <div class="text-center"> <a routerLink="/login" class="text-sm text-blue-600 hover:text-blue-500 font-medium"> ← Back to sign in </a> </div> </div> </div>

    Part 8: Configure Routes

    src/app/app.routes.ts:

    typescript
    import { Routes } from '@angular/router';
    import { LoginComponent } from './pages/login/login';
    import { RegisterComponent } from './pages/register/register';
    import { ForgotPasswordComponent } from './pages/forgot-password/forgot-password';
    import { DashboardComponent } from './pages/dashboard/dashboard';
    import { AdminPanelComponent } from './pages/admin-panel/admin-panel';
    import { authGuard, publicGuard } from './guards/auth.guard';
    import { adminGuard } from './guards/admin.guard';

    export const routes: Routes = [ // Public routes (redirect to dashboard if already logged in) { path: 'login', component: LoginComponent, canActivate: [publicGuard] }, { path: 'register', component: RegisterComponent, canActivate: [publicGuard] }, { path: 'forgot-password', component: ForgotPasswordComponent, canActivate: [publicGuard] },

    // Protected routes (require authentication) { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },

    // Admin routes (require admin role) { path: 'admin', component: AdminPanelComponent, canActivate: [adminGuard] },

    // Default redirect { path: '', redirectTo: '/dashboard', pathMatch: 'full' } ];

    Part 9: Common Pitfalls & Solutions

    Pitfall #1: Auth State Not Loading Before Guards Execute

    Problem: Guards check currentUser() before Firebase finishes loading auth state.

    Solution: Add loading check in guards:

    typescript
    // Wait for auth to finish loading
    let attempts = 0;
    while (authService.loading() && attempts < 50) {
      await new Promise(resolve => setTimeout(resolve, 50));
      attempts++;
    }

    Pitfall #2: Firestore Permissions Denied

    Error: "Missing or insufficient permissions"

    Solution: Check Firestore security rules match your user structure:

    javascript
    match /users/{userId} {
      allow read: if request.auth.uid == userId;
      allow create: if request.auth.uid == userId;
      allow update: if request.auth.uid == userId;
    }

    Pitfall #3: Google Sign-In Popup Blocked

    Problem: Browser blocks the OAuth popup.

    Solution: Call signInWithPopup from a user interaction (button click), not from ngOnInit or automatic code.

    Pitfall #4: Session Doesn't Persist After Page Reload

    Problem: User gets logged out on refresh.

    Solution: Firebase Auth automatically persists sessions using IndexedDB. If this isn't working, check:

    1. Browser isn't in incognito mode 2. IndexedDB isn't disabled 3. You're using the same Firebase project

    Pitfall #5: Environment Variables Exposed in Production

    Problem: Firebase config visible in production JavaScript.

    Solution: This is actually okay! The config is not sensitive. Firestore security rules protect your data, not the config. However, do add:

    typescript
    // In Firebase Console → App Check
    // Enable reCAPTCHA v3 or App Attest

    This prevents abuse of your Firebase quota.

    Pitfall #6: User Role Not Updating Immediately

    Problem: User promoted to admin but guards still deny access.

    Solution: Firestore updates don't trigger auth state change. Force a refresh:

    typescript
    // After updating role in Firestore
    await this.authService.refreshUser();

    Or have the user log out and back in.

    Part 10: Production Deployment Tips

    1. Enable App Check (Prevent API Abuse)

    In Firebase Console: 1. Go to "Build" → "App Check" 2. Click "Register" for your web app 3. Choose reCAPTCHA v3 4. Add your production domain 5. Enable enforcement

    This prevents bots from hammering your Firebase quota.

    2. Set Up Custom Email Templates

    Firebase's default emails are ugly. Customize them:

    1. Go to "Authentication" → "Templates" 2. Customize: - Email verification - Password reset - Email address change

    Add your logo, brand colors, and custom domain.

    3. Monitor Auth Activity

    Set up alerts for suspicious activity:

    typescript
    // In Firebase Console → "Authentication" → "Settings"
    
  • Enable "Email enumeration protection"
  • Set up "Sign-in method providers" carefully
  • Review "Authorized domains"
  • 4. Add Rate Limiting

    Prevent brute force attacks:

    typescript
    // Firebase automatically rate limits, but you can add client-side checks
    private loginAttempts = 0;
    private readonly MAX_ATTEMPTS = 5;

    async signIn(email: string, password: string) { if (this.loginAttempts >= this.MAX_ATTEMPTS) { throw new Error('Too many login attempts. Please try again later.'); }

    try { await signInWithEmailAndPassword(this.auth, email, password); this.loginAttempts = 0; // Reset on success } catch (error) { this.loginAttempts++; throw error; } }

    5. Build for Production

    bash
    # Build with production optimizations
    ng build --configuration production

    Output will be in dist/your-app-name/browser/

    6. Deploy to Firebase Hosting (Optional)

    bash
    # Install Firebase CLI
    npm install -g firebase-tools

    Login to Firebase

    firebase login

    Initialize hosting

    firebase init hosting

    Select options:

    - Public directory: dist/your-app-name/browser

    - Single-page app: Yes

    - Set up automatic builds with GitHub: Optional

    Deploy

    firebase deploy --only hosting

    7. Set Up HTTPS (Required for Auth)

    Firebase Hosting automatically provides HTTPS. If using another host (S3, Netlify, etc.), ensure HTTPS is configured—Firebase Auth requires it.

    8. Configure CORS for API Calls

    If your app makes API calls to your own backend:

    typescript
    // In your backend (Node.js/Express example)
    app.use(cors({
      origin: [
        'http://localhost:4200',
        'https://your-app.web.app',
        'https://your-custom-domain.com'
      ],
      credentials: true
    }));

    Part 11: Testing Your Implementation

    Manual Testing Checklist

    Registration Flow:

  • [ ] Can register with email/password
  • [ ] First/last name saved correctly
  • [ ] Firestore user document created
  • [ ] Redirect to dashboard after registration
  • [ ] Error shown for weak password
  • [ ] Error shown for existing email
  • Login Flow:

  • [ ] Can login with correct credentials
  • [ ] Error shown for wrong password
  • [ ] Error shown for non-existent user
  • [ ] Google Sign-In creates Firestore document
  • [ ] Return URL works after login
  • Password Reset:

  • [ ] Email sent successfully
  • [ ] Link redirects to Firebase hosted page
  • [ ] Can set new password
  • [ ] Can login with new password
  • Guards:

  • [ ] /login redirects to /dashboard if logged in
  • [ ] /dashboard redirects to /login if not logged in
  • [ ] /admin redirects to /dashboard if not admin
  • [ ] Return URL preserved through redirects
  • Session Persistence:

  • [ ] Refresh page doesn't log out user
  • [ ] Opening new tab keeps user logged in
  • [ ] Logout clears session
  • [ ] Browser close/reopen restores session
  • Part 12: Next Steps & Enhancements

    Once you have basic auth working, consider adding:

    1. Email Verification Enforcement

    Require users to verify email before accessing the app:

    typescript
    async signIn(email: string, password: string) {
      const result = await signInWithEmailAndPassword(this.auth, email, password);
      
      if (!result.user.emailVerified) {
        await signOut(this.auth);
        throw new Error('Please verify your email before signing in');
      }
    }

    2. Multi-Factor Authentication (MFA)

    Firebase supports SMS-based 2FA:

    typescript
    import { multiFactor, PhoneAuthProvider } from '@angular/fire/auth';

    // Enable in Firebase Console first // Then implement 2FA flow

    3. Social Logins (Facebook, Twitter, GitHub)

    Similar to Google Sign-In:

    typescript
    import { FacebookAuthProvider, GithubAuthProvider } from '@angular/fire/auth';

    async signInWithFacebook() { const provider = new FacebookAuthProvider(); await signInWithPopup(this.auth, provider); }

    4. Anonymous Authentication

    Allow users to try your app before registering:

    typescript
    import { signInAnonymously } from '@angular/fire/auth';

    async signInAnonymously() { await signInAnonymously(this.auth); }

    5. User Profile Pictures

    Upload photos to Firebase Storage:

    typescript
    import { Storage, ref, uploadBytes, getDownloadURL } from '@angular/fire/storage';

    async uploadProfilePicture(file: File) { const storage = inject(Storage); const user = this.auth.currentUser!; const storageRef = ref(storage, profile-pics/${user.uid}); await uploadBytes(storageRef, file); const photoURL = await getDownloadURL(storageRef); await updateProfile(user, { photoURL }); }

    6. Activity Logging

    Track user actions in Firestore:

    typescript
    async logActivity(action: string) {
      const user = this.currentUser();
      if (!user) return;
      
      await addDoc(collection(this.firestore, 'activity-logs'), {
        userId: user.uid,
        action,
        timestamp: serverTimestamp()
      });
    }

    Conclusion

    You now have a complete, production-ready Firebase Authentication system integrated with Angular 18. This implementation includes:

    ✅ Email/password registration and login ✅ Google OAuth ✅ Password reset flow ✅ Email verification ✅ Route guards (public, auth, admin) ✅ Role-based access control ✅ Persistent sessions ✅ Proper error handling ✅ Loading states ✅ Firestore integration

    What makes this implementation production-ready:

    1. Security: Firestore rules prevent unauthorized access 2. User Experience: Proper loading states and error messages 3. Code Quality: Clean separation of concerns with service/guards 4. Scalability: Signals for reactive state management 5. Maintainability: TypeScript types and clear code structure

    Common use cases this covers:

  • SaaS applications with user accounts
  • Content management systems
  • E-commerce platforms
  • Internal tools and admin panels
  • Mobile apps (using Capacitor with this same code)
  • What you learned:

  • Firebase Auth setup and configuration
  • Angular Fire integration
  • Route guards implementation
  • Firestore security rules
  • OAuth provider integration
  • Session management
  • Error handling patterns
  • Production deployment practices
  • The beauty of this implementation is that it's ready to extend. Need to add Facebook login? Just add another provider. Need user roles beyond admin? Add fields to Firestore. Need to track user activity? Add logging to the service.

    Firebase Authentication removes the complexity of building auth from scratch, letting you focus on the unique features of your application. And with this complete implementation, you have a solid foundation to build on.

    Now go build something awesome! 🚀

    ---

    About This Implementation:

    This guide is based on my production blog system running at https://gilricardo.com. The auth system handles:

  • User registration and login
  • Admin access for blog post management
  • Protected dashboard routes
  • Google OAuth for convenience
  • Profile management
  • Tech Stack:

  • Angular 18 (standalone components)
  • Firebase 10
  • AngularFire 18
  • TypeScript 5
  • Tailwind CSS 3
  • Live Demo: https://gilricardo.com

    Questions? Feel free to reach out at contact@gilricardo.com

    ---

    If you found this helpful, please share it with other Angular developers. Authentication doesn't have to be complicated!

    Related Posts

    Building with Angular + Firebase? Check out my Photography E-Commerce Platform that combines Firebase auth with Stripe payments and AWS S3 storage.

    Self-hosting your projects? Learn how I host production apps on a Raspberry Pi with PM2, Nginx, and Tailscale.

    About the Author: Ricardo Gil is a full-stack software engineer specializing in .NET/C#, Angular, and cloud platforms. Read more or subscribe for updates.

    📬Weekly Newsletter

    Get the best home lab & AI content

    No spam. One email per week. Unsubscribe anytime.

    Share this article