Securing an Angular Firebase AI App with Email/Password Authentication (Part 1: Emulator, AuthService, and Route Guard) [GDE] A Google Developer Expert detailed a first-part guide for protecting an Angular app's Firebase AI features—image analysis and text-to-speech—behind Email/Password authentication. The approach runs the Firebase Auth Emulator on port 9099 alongside the Angular dev server via a single `concurrently` launch script, lazy-loads the heavy `firebase/auth` SDK through a dynamic import into a reactive `isAuthenticated` signal, and guards the `/dashboard` route with a functional `CanActivateFn` using `browserSessionPersistence` to retain login state across refreshes. Anyone with the public URL can use my tokens to perform image analysis and text-to-speech, which can quickly drive up my billing costs. To prevent unauthorized usage, I want to convert /dashboard into a protected route by adding Email/Password authentication with Firebase Auth. Firebase Auth supports many authentication providers out of the box, and I chose Email/Password because it is the simplest method to configure. In Part 1, we will configure the Firebase Auth Local Emulator, build a lazy-loaded AuthService and a functional route guard, and protect the /dashboard route using CanActivateFn . Part 1 focuses on running the Firebase Auth Emulator alongside the Angular dev server and configuring authentication state management. The architecture uses Firebase's browser session persistence browserSessionPersistence to manage the user's authentication state, which provides two key benefits: sessionStorage . When the user refreshes the browser page, the browser retains their login state. Without session persistence, the application would require extra custom logic to check token expiration, explicitly purge the user state from storage, and trigger route redirection. Similarly, because firebase/auth is a heavy third-party SDK, loading it eagerly would increase the initial bundle size. Instead, AuthService lazy-loads it via a dynamic import to update the reactive isAuthenticated signal. flowchart LR subgraph Dev "Local Development npm run launch " NG "Angular Dev Server :4200 " EMU "Firebase Auth Emulator :9099 & UI :4000 " end subgraph AuthCore "AuthService src/app/core/auth " ENSURE "ensureAuth Single-Flight Promise " SDK "Dynamic import 'firebase/auth' " STATE "onAuthStateChanged - user Signal" COMP "isAuthenticated Computed Signal" ENSURE -- SDK -- STATE -- COMP end subgraph Routing "Angular Router app.routes.ts " GUARD "canActivateDashboard CanActivateFn " HOME "/home Sign-In Screen " DASH "/dashboard Image Analysis & TTS " end NG -- ENSURE SDK -. "isLocalhost " .- EMU GUARD -- ENSURE COMP -- "true" -- DASH COMP -- "false Redirect " -- HOME firebase.json The { "emulators": { "auth": ... } } block in firebase/firebase.json configures the Firebase Auth Emulator to run on host port 9099 127.0.0.1:9099 , while "ui": { "enabled": true } enables the Emulator UI on port 4000 http://127.0.0.1:4000 . // firebase/firebase.json { // ... rest of configuration omitted for brevity "emulators": { "auth": { "port": 9099 }, "ui": { "enabled": true } } } concurrently as a Dev Dependency This step is optional, but I prefer not to start the Firebase Auth Emulator and the Angular dev server in two separate steps—opening one terminal tab to run the emulator and a second terminal tab to run the Angular dev server. Installing concurrently allows us to run both processes with a single command so we can start development immediately. npm install --save-dev concurrently package.json During local development, I connect the Angular application to the Firebase Authentication Emulator to seed a test user for login. AuthService connects to the emulator programmatically, and the new scripts in package.json launch it. // package.json scripts excerpt { "scripts": { "start": "ng serve", "firebase:emulate:auth": "firebase --config firebase/firebase.json emulators:start --only auth", "launch": "concurrently \"npm run firebase:emulate:auth\" \"npm run start\"" } } Here is what each script does: start ng serve on http://localhost:4200 . firebase:emulate:auth firebase/firebase.json configuration to start the Authentication Emulator only. launch Seeding a user is a manual process, so I open the Emulator UI at http://localhost:4000 to create a dummy user with the credentials aaa@example.com and 123456 . Note that 123456 is a weak password created strictly to facilitate local authentication testing. Caption: Creating a new test user with an email and password in the Firebase Emulator UI at http://localhost:4000 . Saving the form creates the user account. Because the users table displays the email address without the password, I recommend using a password that is easy to remember or saving it in a password manager. Caption: The newly created email/password user account displayed in the Firebase Authentication Emulator. AuthService AuthCredentials Interface The AuthCredentials interface defines the email and password properties for user login credentials. // src/app/core/auth/auth-credentials.interface.ts export interface AuthCredentials { email: string; password: string; } First, AuthService injects WINDOW , ConfigService , and DestroyRef , and declares the user signal, the isAuthenticated computed signal, and private fields to cache the Firebase Auth instance and SDK module: js // src/app/core/auth/auth.service.ts import { WINDOW } from '@/core/constants/navigator.const'; import { ConfigService, isLocalhost } from '@/core/firebase'; import { computed, DestroyRef, inject, Service, signal } from '@angular/core'; import type { Auth, User } from 'firebase/auth'; import type { AuthCredentials } from './auth-credentials.interface'; const AUTH EMULATOR URL = 'http://127.0.0.1:9099'; @Service export class AuthService { readonly window = inject WINDOW ; readonly configService = inject ConfigService ; readonly destroyRef$ = inject DestroyRef ; readonly user = signal