3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 9

Adding User Authentication with Firebase

Allow users to create accounts and log in

EXTENDED

Adding User Authentication

If we have done everything correctly, our security rules should now be active when using the emulators and they should also be deployed for our production version. If we run the application now:

npm start

We should see an error like this in the browser console:

FirebaseError:
false for 'list' @ L6, false for 'list' @ L12

Locked out of our own application, how rude! These errors are interesting to inspect a little closer. Notice that they give you the specific lines where the error occurred:

false for 'list' @ L6
false for 'list' @ L12

These lines from our rules file, in order, are:

allow read: if isAuthenticated();
allow read, write: if false

In all cases, these if conditions are failing, which makes sense because we haven’t implemented any kind of authentication system yet. To regain the use of our own application, we are going to have to:

  • Create a mechanism to allow creating an account with email/password
  • Create a mechanism that allows users to log in

Creating Accounts with Email/Password in Firebase

We are going to need to allow the user to create an account before we can create a login mechanism so we will start with that. But, our create account page is going to be accessed from our login page, so we are going to need to implement at least some of the login page before being able to complete our create account functionality.

Creating the Auth Service

Let’s start by creating our AuthService. It might sound like this is going to be a scary/complex part of the application, but this service is actually going to be quite simple — all of the hard stuff is handled for us by Firebase.

First, we are going to need to create another interface for the credentials the user will use to log in.

Create a new file at src/app/shared/interfaces/credentials.ts and add the following:

export interface Credentials {
  email: string;
  password: string;
}

Create a new file at src/app/shared/data-access/auth.service.ts and add the following:

import { Injectable, computed, inject, signal } from '@angular/core';
import { from, defer, merge } from 'rxjs';
import { map } from 'rxjs/operators';
import {
  User,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
} from 'firebase/auth';
import { authState } from 'rxfire/auth';
import { Credentials } from '../interfaces/credentials';
import { connect } from 'ngxtension/connect';
import { AUTH } from '../../app.config';

export type AuthUser = User | null | undefined;

interface AuthState {
  user: AuthUser;
}

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private auth = inject(AUTH);

  // sources
  private user$ = authState(this.auth);

  // state
  private state = signal<AuthState>({
    user: undefined,
  });

  // selectors
  user = computed(() => this.state().user);

  constructor() {
    const nextState$ = merge(this.user$.pipe(map((user) => ({ user }))));

    connect(this.state).with(nextState$);
  }

  login(credentials: Credentials) {
    return from(
      defer(() =>
        signInWithEmailAndPassword(
          this.auth,
          credentials.email,
          credentials.password
        )
      )
    );
  }

  logout() {
    signOut(this.auth);
  }

  createAccount(credentials: Credentials) {
    return from(
      defer(() =>
        createUserWithEmailAndPassword(
          this.auth,
          credentials.email,
          credentials.password
        )
      )
    );
  }
}

This is the entire service — we won’t need to make any more changes to this file for the rest of the build. There are three important aspects to this file:

  • user$ — this will emit the active user from Firebase or null
  • login — we will use this to authenticate a user using their credentials
  • logout — we will use this to sign them out with Firebase
  • createAccount — we will use this to create a new account using credentials

Mostly this is just our normal state management set up, and again we are converting promises from Firebase into observables with from and defer.

We are using the authState for our user$ source — this is provided by Firebase and will emit either the User if the user is logged in, or null if they are not. We can react to this to determine if the user is authenticated or not.

When we create an account or login a user, we do not need to worry about handling anything — we just call the methods and that is it. Our authState will automatically update when the auth state changes, and we can react to that.

The Register Service

Our register feature is going to have its own service for managing state, let’s create that now.

This is going to be a reasonably standard service, but it can still be hard to think through the design. I am going to give you a basic outline of the service, and you can see how much of it you can create on your own.

  • It should store a single state property called status
  • The type of status can be pending, creating, success, or error
  • There should be a createUser$ source that can be nexted with Credentials
  • There should be a userCreated$ source that takes those credentials and calls the createAccount method from the AuthService
  • The status state should be updated appropriately in response to the sources

Create a new file at src/app/auth/register/data-access/register.service.ts and add the following:

NOTE: Notice that we have created our register featured inside of a folder called auth. This is the first time we are creating a feature that will have child routesauth will hold both our register and login routes. We will set up the routing to handle this soon.

import { Injectable, computed, inject, signal } from '@angular/core';
import { connect } from 'ngxtension/connect';
import { EMPTY, Subject, catchError, map, merge, switchMap } from 'rxjs';
import { AuthService } from '../../../shared/data-access/auth.service';
import { Credentials } from '../../../shared/interfaces/credentials';

export type RegisterStatus = 'pending' | 'creating' | 'success' | 'error';

interface RegisterState {
  status: RegisterStatus;
}

@Injectable()
export class RegisterService {
  private authService = inject(AuthService);

  // sources
  error$ = new Subject<any>();
  createUser$ = new Subject<Credentials>();

  userCreated$ = this.createUser$.pipe(
    switchMap((credentials) =>
      this.authService.createAccount(credentials).pipe(
        catchError((err) => {
          this.error$.next(err);
          return EMPTY;
        })
      )
    )
  );

  // state
  private state = signal<RegisterState>({
    status: 'pending',
  });

  // selectors
  status = computed(() => this.state().status);

  constructor() {
    // reducers
    const nextState$ = merge(
      this.userCreated$.pipe(map(() => ({ status: 'success' as const }))),
      this.createUser$.pipe(map(() => ({ status: 'creating' as const }))),
      this.error$.pipe(map(() => ({ status: 'error' as const })))
    );

    connect(this.state).with(nextState$);
  }
}

I suspect that most people will still be struggling at this point to do it entirely on their own. If you are struggling, and if you feel like practicing some more, it will be helpful to just delete some or all of this service and try again without looking at the solution. This should generally work well with any example.

The Register Form

Let’s focus on creating the form for registering our users now — this will be a dumb component.

Create a new file at src/app/auth/register/ui/register-form.component.ts and add the following:

import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RegisterStatus } from '../data-access/register.service';
import { Credentials } from '../../../shared/interfaces/credentials';

@Component({
  standalone: true,
  selector: 'app-register-form',
  template: `
    <form [formGroup]="registerForm" (ngSubmit)="onSubmit()" #form="ngForm">
      <mat-form-field appearance="fill">
        <mat-label>email</mat-label>
        <input
          matNativeControl
          formControlName="email"
          type="email"
          placeholder="email"
        />
        <mat-icon matPrefix>email</mat-icon>
      </mat-form-field>
      <mat-form-field>
        <mat-label>password</mat-label>
        <input
          matNativeControl
          formControlName="password"
          data-test="create-password-field"
          type="password"
          placeholder="password"
        />
        <mat-icon matPrefix>lock</mat-icon>
      </mat-form-field>
      <mat-form-field>
        <mat-label>confirm password</mat-label>
        <input
          matNativeControl
          formControlName="confirmPassword"
          type="password"
          placeholder="confirm password"
        />
        <mat-icon matPrefix>lock</mat-icon>
      </mat-form-field>

      <button
        mat-raised-button
        color="accent"
        type="submit"
        [disabled]="status === 'creating'"
      >
        Submit
      </button>
    </form>
  `,
  imports: [
    ReactiveFormsModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    MatIconModule,
    MatProgressSpinnerModule,
  ],
  styles: [
    `
      form {
        display: flex;
        flex-direction: column;
        align-items: center;
      }

      button {
        width: 100%;
      }

      mat-error {
        margin: 5px 0;
      }

      mat-spinner {
        margin: 1rem 0;
      }
    `,
  ],
})
export class RegisterFormComponent {
  @Input({ required: true }) status!: RegisterStatus;
  @Output() register = new EventEmitter<Credentials>();

  private fb = inject(FormBuilder);

  registerForm = this.fb.nonNullable.group({
    email: ['', [Validators.email, Validators.required]],
    password: ['', [Validators.minLength(8), Validators.required]],
    confirmPassword: ['', [Validators.required]],
  });

  onSubmit() {
    if (this.registerForm.valid) {
      const { confirmPassword, ...credentials } =
        this.registerForm.getRawValue();
      this.register.emit(credentials);
    }
  }
}
EXTENDED
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).