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:
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:
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:
Time Savings:
Cost:
Downsides:
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:
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:
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:
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
# Create new Angular project
ng new my-angular-app --standaloneNavigate to project
cd my-angular-appInstall Firebase and AngularFire
npm install firebase @angular/fire
Step 2: Configure Firebase in Angular
Create environment files:
# Create environment files
ng generate environments
src/environments/environment.ts:
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:
export const environment = {
production: true,
firebase: {
// Same config as above
}
};
IMPORTANT: Add environment.ts to .gitignore:
# 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:
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:
Step 4: Create User Model
src/app/models/user.model.ts:
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:
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:
Part 4: Route Guards
Guards protect routes from unauthorized access.
src/app/guards/auth.guard.ts:
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:
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:
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:
<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:
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:
<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:
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:
<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:
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:
// 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:
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:
// 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:
// 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:
// 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:
// 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
# Build with production optimizations ng build --configuration production
Output will be in dist/your-app-name/browser/
6. Deploy to Firebase Hosting (Optional)
# Install Firebase CLI
npm install -g firebase-toolsLogin to Firebase
firebase loginInitialize hosting
firebase init hostingSelect 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:
// 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:
Login Flow:
Password Reset:
Guards:
/login redirects to /dashboard if logged in/dashboard redirects to /login if not logged in/admin redirects to /dashboard if not adminSession Persistence:
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:
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:
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:
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:
import { signInAnonymously } from '@angular/fire/auth';
async signInAnonymously() { await signInAnonymously(this.auth); }
5. User Profile Pictures
Upload photos to Firebase Storage:
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:
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:
What you learned:
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:
Tech Stack:
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.
