{"slug": "securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1", "title": "Securing an Angular Firebase AI App with Email/Password Authentication (Part 1: Emulator, AuthService, and Route Guard) [GDE]", "summary": "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.", "body_md": "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.\n\nFirebase 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`.\n\nPart 1 focuses on running the Firebase Auth Emulator alongside the Angular dev server and configuring authentication state management.\n\nThe architecture uses Firebase's browser session persistence (`browserSessionPersistence`) to manage the user's authentication state, which provides two key benefits:\n\n`sessionStorage`. When the user refreshes the browser page, the browser retains their login state.\nWithout session persistence, the application would require extra custom logic to check token expiration, explicitly purge the user state from storage, and trigger route redirection.\n\nSimilarly, 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.\n\n```\nflowchart LR\n    subgraph Dev[\"Local Development (npm run launch)\"]\n        NG[\"Angular Dev Server (:4200)\"]\n        EMU[\"Firebase Auth Emulator (:9099 & UI :4000)\"]\n    end\n\n    subgraph AuthCore[\"AuthService (src/app/core/auth)\"]\n        ENSURE[\"ensureAuth() (Single-Flight Promise)\"]\n        SDK[\"Dynamic import('firebase/auth')\"]\n        STATE[\"onAuthStateChanged -> #user Signal\"]\n        COMP[\"isAuthenticated Computed Signal\"]\n        ENSURE --> SDK --> STATE --> COMP\n    end\n\n    subgraph Routing[\"Angular Router (app.routes.ts)\"]\n        GUARD[\"canActivateDashboard (CanActivateFn)\"]\n        HOME[\"/home (Sign-In Screen)\"]\n        DASH[\"/dashboard (Image Analysis & TTS)\"]\n    end\n\n    NG --> ENSURE\n    SDK -. \"isLocalhost()\" .-> EMU\n    GUARD --> ENSURE\n    COMP -- \"true\" --> DASH\n    COMP -- \"false (Redirect)\" --> HOME\n```\n\n`firebase.json`\nThe `{ \"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`).\n\n```\n// firebase/firebase.json\n{\n  // ... rest of configuration omitted for brevity\n  \"emulators\": {\n    \"auth\": {\n      \"port\": 9099\n    },\n    \"ui\": {\n      \"enabled\": true\n    }\n  }\n}\n```\n\n`concurrently` as a Dev Dependency\nThis 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.\n\n```\nnpm install --save-dev concurrently\n```\n\n`package.json`\nDuring 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.\n\n```\n// package.json (scripts excerpt)\n{\n  \"scripts\": {\n    \"start\": \"ng serve\",\n    \"firebase:emulate:auth\": \"firebase --config firebase/firebase.json emulators:start --only auth\",\n    \"launch\": \"concurrently \\\"npm run firebase:emulate:auth\\\" \\\"npm run start\\\"\"\n  }\n}\n```\n\nHere is what each script does:\n\n`start`` ng serve`) on `http://localhost:4200`.` firebase:emulate:auth``firebase/firebase.json` configuration to start the Authentication Emulator only.`launch`\nSeeding 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.\n\n*Caption: Creating a new test user with an email and password in the Firebase Emulator UI at `http://localhost:4000`.*\n\nSaving 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.\n\n*Caption: The newly created email/password user account displayed in the Firebase Authentication Emulator.*\n\n`AuthService`\n`AuthCredentials` Interface\nThe `AuthCredentials` interface defines the `email` and `password` properties for user login credentials.\n\n```\n// src/app/core/auth/auth-credentials.interface.ts\nexport interface AuthCredentials {\n  email: string;\n  password: string;\n}\n```\n\nFirst, `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:\n\n``` js\n// src/app/core/auth/auth.service.ts\nimport { WINDOW } from '@/core/constants/navigator.const';\nimport { ConfigService, isLocalhost } from '@/core/firebase';\nimport { computed, DestroyRef, inject, Service, signal } from '@angular/core';\nimport type { Auth, User } from 'firebase/auth';\nimport type { AuthCredentials } from './auth-credentials.interface';\n\nconst AUTH_EMULATOR_URL = 'http://127.0.0.1:9099';\n\n@Service()\nexport class AuthService {\n  readonly #window = inject(WINDOW);\n  readonly #configService = inject(ConfigService);\n  readonly #destroyRef$ = inject(DestroyRef);\n\n  readonly #user = signal<User | null>(null);\n  user = this.#user.asReadonly();\n  isAuthenticated = computed(() => !!this.#user());\n\n  #auth: Auth | null = null;\n  #authSdk: typeof import('firebase/auth') | null = null;\n  #authReady: Promise<void> | null = null;\n```\n\nNext, `AuthService` manages the Firebase Auth lifecycle through four core methods:\n\n`loadAuth()`` firebase/auth` for the first time and retrieves the current Firebase application instance.`localhost`.` browserSessionPersistence`), registers `onAuthStateChanged` to monitor authentication state changes, and awaits `auth.authStateReady()` for the initial auth state to resolve.`auth` instance in `#auth` and the Auth SDK module in `#authSdk`.\n\n```\n  private async loadAuth(): Promise<void> {\n    const [firebaseApp, authSdk] = await Promise.all([this.#configService.getApp(), import('firebase/auth')]);\n\n    const auth = authSdk.getAuth(firebaseApp);\n    if (isLocalhost(this.#window)) {\n      authSdk.connectAuthEmulator(auth, AUTH_EMULATOR_URL, { disableWarnings: true });\n    }\n\n    await authSdk.setPersistence(auth, authSdk.browserSessionPersistence);\n    const unsubscribe = authSdk.onAuthStateChanged(auth, (user) => this.#user.set(user));\n\n    this.#destroyRef$.onDestroy(unsubscribe);\n\n    await auth.authStateReady();\n\n    this.#auth = auth;\n    this.#authSdk = authSdk;\n  }\n```\n\n`ensureAuth()`` signIn()` and `signOut()` to reuse them when logging a user in or out.\n\n```\n  async ensureAuth(): Promise<{ auth: Auth; sdk: typeof import('firebase/auth') }> {\n    if (!this.#authReady) {\n      this.#authReady = this.loadAuth();\n    }\n\n    await this.#authReady;\n\n    if (!this.#auth) {\n      throw new Error('Auth is not initialized');\n    }\n\n    if (!this.#authSdk) {\n      throw new Error('Auth SDK does not exist');\n    }\n\n    return { auth: this.#auth, sdk: this.#authSdk };\n  }\n```\n\n`email` and `password`, validates the arguments, and calls `sdk.signInWithEmailAndPassword()` to authenticate the user. When authentication succeeds, the `authSdk.onAuthStateChanged(auth, (user) => this.#user.set(user))` listener observes the state change, updates the `#user` signal, and recalculates the `isAuthenticated` computed signal.\n\n```\n  async signIn({ email, password }: AuthCredentials): Promise<void> {\n    if (!email || !password) {\n      this.#user.set(null);\n      return;\n    }\n\n    const { auth, sdk } = await this.ensureAuth();\n    await sdk.signInWithEmailAndPassword(auth, email, password);\n  }\n```\n\n`sdk.signOut(auth)` to sign out the currently logged-in user. Similarly, the `null` and recalculates the \n\n```\n  async signOut(): Promise<void> {\n    const { auth, sdk } = await this.ensureAuth();\n    await sdk.signOut(auth);\n  }\n}\n```\n\n`canActivateDashboard`\n`CanActivateFn` Route Guard\n`canActivateDashboard` is a functional route guard of type `CanActivateFn`. It injects `AuthService` and `Router` to verify the user's authentication status and handle route redirection. Calling `await authService.ensureAuth()` initializes Firebase Auth (connecting to the Auth Emulator when running on `localhost`) and updates the `isAuthenticated` computed signal with the current authentication state. When the user is authenticated, the guard returns `true` to allow navigation to the route; otherwise, it redirects the user back to `/home` via `router.createUrlTree([APP_LINKS.HOME])`.\n\n``` js\n// src/app/core/auth/auth.guard.ts\nimport { AuthService } from './auth.service';\nimport { APP_LINKS } from '@/core/constants/routes.const';\nimport { inject } from '@angular/core';\nimport type { CanActivateFn } from '@angular/router';\nimport { Router } from '@angular/router';\n\nexport const canActivateDashboard: CanActivateFn = async () => {\n  const authService = inject(AuthService);\n  const router = inject(Router);\n\n  await authService.ensureAuth();\n\n  if (authService.isAuthenticated()) {\n    return true;\n  }\n\n  return router.createUrlTree([APP_LINKS.HOME]);\n};\n```\n\nFinally, we re-export the `AuthCredentials` interface, the `canActivateDashboard` route guard, and `AuthService` from a single barrel file (`src/app/core/auth/index.ts`). This gives the `/dashboard` route configuration—and the Sign-In component we will build in Part 2—a clean, unified import path.\n\n```\n// src/app/core/auth/index.ts\nexport * from './auth-credentials.interface';\nexport * from './auth.guard';\nexport * from './auth.service';\n```\n\n`app.routes.ts`\nThe `canActivate` property of the dashboard route accepts the `canActivateDashboard` function in an array. When a user types `/dashboard` in the address bar or navigates to the path programmatically, the router executes this route guard function. Authenticated users proceed to the dashboard, while the guard blocks unauthenticated visitors and redirects them back to `/home`.\n\n``` js\n// src/app/core/constants/routes.const.ts\nexport const ROUTE_PATHS = {\n  HOME: 'home',\n  DASHBOARD: 'dashboard',\n} as const;\n// src/app/app.routes.ts (dashboard route excerpt)\n  {\n    path: ROUTE_PATHS.DASHBOARD,\n    title: 'Firebase TTS',\n    providers: [provideEnvironmentInitializer(() => inject(ConfigService).initialize())],\n    loadComponent: () => import('./features/dashboard/dashboard.component'),\n    canActivate: [canActivateDashboard],\n  },\n```\n\nIn Part 1, we configured Firebase Auth to protect our image analysis and TTS endpoints:\n\nIn **Part 2**, we will build the interactive sign-in experience on the frontend. We will use **Angular 22's Signal Forms** to create the form where users input their email and password, use **Angular CDK Dialog** to open the sign-in modal when a user clicks the **Sign In** button on the home screen, and connect the Sign-In component with `AuthService` to call the `signIn()` method.", "url": "https://wpnews.pro/news/securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1", "canonical_source": "https://dev.to/gde/securing-an-angular-firebase-ai-app-with-emailpassword-authentication-part-1-emulator-4jm", "published_at": "2026-11-02 09:45:08+00:00", "updated_at": "2026-11-02 09:52:26.903149+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["Angular", "Firebase", "Google Developer Experts", "Firebase Auth Emulator"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1", "markdown": "https://wpnews.pro/news/securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1.md", "text": "https://wpnews.pro/news/securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1.txt", "jsonld": "https://wpnews.pro/news/securing-an-angular-firebase-ai-app-with-email-password-authentication-part-1.jsonld"}}