Firebase Authentication + Angular: Complete Integration Guide
Development

Firebase Authentication + Angular: Complete Integration Guide

Ricardo Gil
January 4, 2026
25 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://ricardogil.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://ricardogil.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.

Need this built for your business?

I build bilingual (EN/ES) web apps and sites with .NET, Angular and AWS: production-ready, documented and handed off. See Full-Stack Development →

Book a free discovery call.

📬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article