cd /news/ai-tools/securing-an-angular-firebase-ai-app-… · home › topics › ai-tools › article
[ARTICLE · art-162397] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

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.

by read7 min views1 publishedNov 2, 2026

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, 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:

// 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<User | null>(null);
  user = this.#user.asReadonly();
  isAuthenticated = computed(() => !!this.#user());

  #auth: Auth | null = null;
  #authSdk: typeof import('firebase/auth') | null = null;
  #authReady: Promise<void> | null = null;

Next, AuthService manages the Firebase Auth lifecycle through four core methods:

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.

  private async loadAuth(): Promise<void> {
    const [firebaseApp, authSdk] = await Promise.all([this.#configService.getApp(), import('firebase/auth')]);

    const auth = authSdk.getAuth(firebaseApp);
    if (isLocalhost(this.#window)) {
      authSdk.connectAuthEmulator(auth, AUTH_EMULATOR_URL, { disableWarnings: true });
    }

    await authSdk.setPersistence(auth, authSdk.browserSessionPersistence);
    const unsubscribe = authSdk.onAuthStateChanged(auth, (user) => this.#user.set(user));

    this.#destroyRef$.onDestroy(unsubscribe);

    await auth.authStateReady();

    this.#auth = auth;
    this.#authSdk = authSdk;
  }

ensureAuth()`` signIn() and signOut() to reuse them when logging a user in or out.

  async ensureAuth(): Promise<{ auth: Auth; sdk: typeof import('firebase/auth') }> {
    if (!this.#authReady) {
      this.#authReady = this.loadAuth();
    }

    await this.#authReady;

    if (!this.#auth) {
      throw new Error('Auth is not initialized');
    }

    if (!this.#authSdk) {
      throw new Error('Auth SDK does not exist');
    }

    return { auth: this.#auth, sdk: this.#authSdk };
  }

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.

  async signIn({ email, password }: AuthCredentials): Promise<void> {
    if (!email || !password) {
      this.#user.set(null);
      return;
    }

    const { auth, sdk } = await this.ensureAuth();
    await sdk.signInWithEmailAndPassword(auth, email, password);
  }

sdk.signOut(auth) to sign out the currently logged-in user. Similarly, the null and recalculates the

  async signOut(): Promise<void> {
    const { auth, sdk } = await this.ensureAuth();
    await sdk.signOut(auth);
  }
}

canActivateDashboard CanActivateFn Route Guard 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]).

// src/app/core/auth/auth.guard.ts
import { AuthService } from './auth.service';
import { APP_LINKS } from '@/core/constants/routes.const';
import { inject } from '@angular/core';
import type { CanActivateFn } from '@angular/router';
import { Router } from '@angular/router';

export const canActivateDashboard: CanActivateFn = async () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  await authService.ensureAuth();

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

  return router.createUrlTree([APP_LINKS.HOME]);
};

Finally, 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.

// src/app/core/auth/index.ts
export * from './auth-credentials.interface';
export * from './auth.guard';
export * from './auth.service';

app.routes.ts The 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.

// src/app/core/constants/routes.const.ts
export const ROUTE_PATHS = {
  HOME: 'home',
  DASHBOARD: 'dashboard',
} as const;
// src/app/app.routes.ts (dashboard route excerpt)
  {
    path: ROUTE_PATHS.DASHBOARD,
    title: 'Firebase TTS',
    providers: [provideEnvironmentInitializer(() => inject(ConfigService).initialize())],
    loadComponent: () => import('./features/dashboard/dashboard.component'),
    canActivate: [canActivateDashboard],
  },

In Part 1, we configured Firebase Auth to protect our image analysis and TTS endpoints:

In 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.

── more in #ai-tools 4 stories · sorted by recency
── more on @angular 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/securing-an-angular-…] indexed:0 read:7min 2026-11-02 · —