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 as well. If we run the application now:

npm start

We should see an error like this in the console:

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

and when we try to add a new message:

ERROR Error: Uncaught (in promise): FirebaseError: [code=permission-denied]: PERMISSION_DENIED: 
Null value error. for 'create' @ L7, false for 'create' @ L12
FirebaseError: PERMISSION_DENIED: 

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
Null value error. for 'create' @ L7
false for 'create' @ L12

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

allow read: if isAuthenticated();
allow read, write: if false
allow create: if isValidMessage();
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 log in 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 } from '@angular/core';
import {
  Auth,
  authState,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut
} from '@angular/fire/auth';
import { from } from 'rxjs';
import { Credentials } from '../interfaces/credentials';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  user$ = authState(this.auth);

  constructor(private auth: Auth) {}

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

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

  createAccount(credentials: Credentials) {
    return 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$ - we will use this to get the active user
  • 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

Something curious to note here is that for login and logout we are converting the Promise returned from @angular/fire into an observable stream by using from:

    return from(
      signInWithEmailAndPassword(
        this.auth,
        credentials.email,
        credentials.password
      )
    );

But, we don’t do this for createAccount. Although I generally like working with observable streams more, I won’t pointlessly convert a Promise to an observable if it serves no purpose. In this case, we will be using login and logout as part of an effect in our stores with Component Store which is why it is easier to have these particular methods as streams.

We won’t be using createAccount in an effect so there is no need to convert it to an observable (in fact, this would make it harder because if we did we would manually need to subscribe to it). For our CreateModalComponent we are very intentionally not using Component Store just to demonstrate the difference between using it and not using it. Our LoginComponent and our CreateModalComponent will be very similar, and one uses Component Store and one doesn’t, so it will make for a good comparison. It will also be a good exercise for you to convert CreateModalComponent to use Component Store as well.

The Create Account Form

Our CreateModalComponent is going to consist of one smart component and one dumb UI component: the form. Let’s create the form first, and then we will move on to the actual modal component itself.

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

import { CommonModule } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  NgModule,
  Output,
} from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { Credentials } from '../../shared/interfaces/credentials';

@Component({
  selector: 'app-create-form',
  template: `
    <form [formGroup]="createForm" (ngSubmit)="onSubmit()" #form="ngForm">
      <ion-item lines="none">
        <ion-icon color="light" slot="start" name="mail-outline"></ion-icon>
        <ion-input
          formControlName="email"
          type="email"
          placeholder="email"
        ></ion-input>
      </ion-item>
      <ion-item lines="none">
        <ion-icon
          color="light"
          slot="start"
          name="lock-closed-outline"
        ></ion-icon>
        <ion-input
          formControlName="password"
          type="password"
          placeholder="password"
        ></ion-input>
      </ion-item>
      <ion-item lines="none">
        <ion-icon
          color="light"
          slot="start"
          name="lock-closed-outline"
        ></ion-icon>
        <ion-input
          formControlName="confirmPassword"
          type="password"
          placeholder="confirm password"
        ></ion-input>
      </ion-item>
      <ion-button type="submit" expand="full"> Submit </ion-button>
    </form>
  `,
  styles: [
    `
      :host {
        height: 100%;
      }

      form {
        text-align: right;
      }

      ion-note {
        margin: 0 1rem 1rem 1rem;
        display: block;
      }
    `,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateFormComponent {
  @Output() create = new EventEmitter<Credentials>();

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

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    if (this.createForm.valid) {
      const { confirmPassword, ...credentials } = this.createForm.getRawValue();
      this.create.emit(credentials);
    }
  }
}

@NgModule({
  declarations: [CreateFormComponent],
  exports: [CreateFormComponent],
  imports: [IonicModule, ReactiveFormsModule, CommonModule],
})
export class CreateFormComponentModule {}

This is reasonably similar to the sorts of dumb form components we have already created - we build a form and emit an event with the value when the user submits it. An important missing piece here is that we have not yet implemented some important details with regard to form validations and showing the user errors - we are going to tackle that in the next lesson. Our goal for now is just to get it working, and we will provide a better user experience in the next lesson.

However, one interesting thing here is this:

    if (this.createForm.valid) {
      const { confirmPassword, ...credentials } = this.createForm.getRawValue();
      this.create.emit(credentials);
    }

We have discussed this approach of using getRawValue to take the values from the form… but we haven’t seen this:

const { confirmPassword, ...credentials } = this.createForm.getRawValue();

What this does is remove the confirmPassword property from the getRawValue() object. We don’t need to submit the confirmPassword when creating an account - this is just a local check to make sure the user typed in the password correctly.

We are using some fancy destructing syntax here to achieve what we want. If we were to do this:

const { confirmPassword } = this.createForm.getRawValue();

This means we want to get the confirmPassword property from getRawValue and have it be available on the variable confirmPassword. By doing this:

const { confirmPassword, ...credentials } = this.createForm.getRawValue();

We are first assigning confirmPassword separately, and then we are saying to assign “everything else” to a variable named credentials. This means that the credentials variable will have all of the values except confirmPassword. If this syntax confuses you, you can also just do this:

const values = this.createForm.getRawValue();

const credentials: Credentials = {
    email: values.email,
    password: values.password
}

The Create Modal

Now let’s create our smart component that will make use of this form.

Create a file at src/app/create/create-modal.component.ts and add the following:

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
import { IonicModule, ModalController, NavController } from '@ionic/angular';
import { BehaviorSubject } from 'rxjs';
import { AuthService } from '../shared/data-access/auth.service';
import { Credentials } from '../shared/interfaces/credentials';
import { CreateFormComponentModule } from './ui/create-form.component';

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

@Component({
  selector: 'app-create-modal',
  template: `
    <ion-header>
      <ion-toolbar color="primary">
        <ion-title>Create Account</ion-title>
        <ion-buttons slot="end">
          <ion-button (click)="modalCtrl.dismiss()">
            <ion-icon name="close" slot="icon-only"></ion-icon>
          </ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <app-create-form
        *ngIf="createStatus$ | async as status"
        (create)="createAccount($event)"
        [createStatus]="status"
      ></app-create-form>
    </ion-content>
  `,
  styles: [
    `
      :host {
        height: 100%;
      }

      ion-content {
        --background: linear-gradient(
          62deg,
          var(--ion-color-primary) 0%,
          var(--ion-color-secondary) 100%
        );
      }
    `,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateModalComponent {
  createStatus$ = new BehaviorSubject<CreateStatus>('pending');

  constructor(
    protected authService: AuthService,
    protected modalCtrl: ModalController,
    private navCtrl: NavController
  ) {}

  async createAccount(credentials: Credentials) {
    this.createStatus$.next('creating');

    try {
      await this.authService.createAccount(credentials);
      this.createStatus$.next('success');
      this.modalCtrl.dismiss();
      this.navCtrl.navigateRoot('/home');
    } catch (err) {
      this.createStatus$.next('error');
    }
  }
}

@NgModule({
  declarations: [CreateModalComponent],
  exports: [CreateModalComponent],
  imports: [IonicModule, CommonModule, CreateFormComponentModule],
})
export class CreateModalComponentModule {}
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).