Lesson Monday

While authentication is the process of signing a user in, authorization is the process of determining whether a user should be able to access a resource. If we have content on our site that’s only for registered users, we need to protect that content from users that aren’t logged in.

Adding a Router

Start by adding two new components to the authentication example from the previous lesson:

$ ng g component public
$ ng g component private

HTML content from the public component will be available to everyone, but content from the private component should only be available to to logged-in users.

Go ahead and set up routing on your own so that both the public and private component are accessible to everyone. This is all review. Refer back to these lessons for the specifics: Implementing a Router and Managing and Navigating Multiple Routes. Make sure to add the <router-outlet> to app.component.html once your routes are set up.

Adding Redirects

One of the easiest ways to protect our routes is to add a redirect. If a user that isn’t logged in tries to access the private route (or any other route that’s not the public route), they should be redirected to the public route. We can implement this very quickly:

src/app/app.component.ts
...
  constructor(public authService: AuthenticationService, private router: Router) {
    this.authService.user.subscribe(user => {
      if (user == null) {
        this.isLoggedIn = false;
        this.router.navigate(['public']);
      } else {
        this.isLoggedIn = true;
        this.userName = user.displayName;
        this.router.navigate([]);
      }
    });
  }

We’ve only made a few small changes here. If the user isn’t logged in, we’ve instructed the router to navigate to the public route. Users that are logged in can navigate where they wish because we’ve left an empty array in this.router.navigate([]);. (You could also leave this.router.navigate([]); out completely, since it’s not imposing any restrictions on users.)

Go ahead and serve the application.

This approach works well if there is only one route that you want users to see (such as a ‘public’ route). But what if your application is more nuanced?

Using *ngIf to Protect Content

Let’s look at another approach for protecting content. Let’s say that all visitors to the public route should see some content but there’s also some private content on the page as well. Here’s an example of how the HTML would look:

src/app/public/public.component.html
<p>
  This content is public for everyone!
</p>

<p *ngIf="isLoggedIn">
  Here's some bonus content for signed-in users.
</p>

To get this working, we’ll need to inject our authentication service. If we wanted to, we could add code similar to what we’ve already written:

public.component.ts
import { Component } from '@angular/core';
import { AuthenticationService } from '../authentication/authentication.service';

@Component({
  selector: 'app-public',
  templateUrl: './public.component.html',
  styleUrls: ['./public.component.css'],
  providers: [AuthenticationService]
})
export class PublicComponent {

  private isLoggedIn: Boolean = null;

  constructor(public authService: AuthenticationService) {
    this.authService.user.subscribe(user => {
      if (user == null) {
        this.isLoggedIn = false;
      } else {
        this.isLoggedIn = true;
      }
    });
  }
}

This works fine, but we can refactor our code further. To do so, we’ll use a provided Firebase method and another Angular lifecycle hook called ngDoCheck.

Here’s our new code:

src/app/public/public.component.ts
import { Component } from '@angular/core';
import * as firebase from "firebase";

@Component({
  selector: 'app-public',
  templateUrl: './public.component.html',
  styleUrls: ['./public.component.css']
})
export class PublicComponent {
  private user;

  constructor() {}

  ngDoCheck() {
    this.user = firebase.auth().currentUser;
  }
}

Let’s go over the changes in the code. First, we’re no longer injecting our authentication service. Instead, we add the following to our imports: import * as firebase from "firebase”;. This will give us access to a Firebase helper method: firebase.auth().currentUser;. For more on this method, check out the documentation on Firebase

The purpose of this helper method should be clear from the name: it sets the local variable user to the currentUser. If there’s no currentUser, the value of user will be null. However, if there is a currentUser, we’ll store it in the user variable. One nice thing about this method is that we can access the object’s properties without needing to subscribe.

In order for this code to work, we’ll need to make one change in our HTML. We no longer have an isLoggedIn boolean; instead our *ngIf directive should look like this: <p *ngIf=“user”>.

To see why this works, let’s take a deeper look at the Angular lifecycle hook in this code: ngDoCheck().

ngDoCheck Hook

Let’s start by using console.log() to demonstrate why we need to use ngDoCheck() instead of ngOnInit().

src/app/public/public.component.ts
  ngDoCheck() {
    this.user = firebase.auth().currentUser;
    console.log(this.user);
  }

Now let’s try logging in to Firebase. The console will look something like this (your nulls may vary):

The console spits out six null values and then a currentUser object. (You can disregard the GET and POST requests.) In other words, console.log was called seven times, and user didn’t have a value until the seventh time. (Again, your the number of nulls will vary, and you may also see the currentUser object logged multiple times as well.)

If we were to put this same code inside a ngOnInit() hook, the code would run only once, on initialization, and the value of currentUser will be null. If we knew that currentUser would have a value by the time ngOnInit() was called, then that would be the right hook to use. However, logging into Firebase is an asynchronous operation that takes time, and it’s very unlikely it will ever be completed before the page loads. By that point, the ngOnInit() hook has already been called.

Using ngDoCheck() solves this problem. If we check the Angular documentation on lifecycle hooks, ngDoCheck() will “detect and act upon changes that Angular can't or won't detect on its own.” It’s called immediately after ngOnInit() and also “during every change detection run.” In other words, each time there’s a change in the component, ngDoCheck() is called. This is very helpful when we’re waiting on async code.

You now have another valuable tool in your Angular toolbox. If you’re calling async code in ngOnInit(), it may be a good idea to call that code in ngDoCheck() instead. Even if the code seems to be working perfectly, you run the risk of dealing with a race condition.

A race condition is a computer science term which we’ll quickly cover here because it’s so relevant to JavaScript and async code. Imagine two async functions (A and B) in a race to cross an imaginary finish line. Function B is dependent on function A, and returns a fatal error if A isn’t called first. You can guess what happens.

If function A finishes first, all is well. But if function B finishes first, your application will break. This is called a race condition, and it’s always a bug, even if function A usually crosses the finish line first.

While ngDoCheck() is useful, resist the temptation to put too much code in there. If the code is synchronous (it will run immediately instead of waiting), it should still go into ngOnInit(). We should avoid unnecessary method calls and use the best tool for the job at hand.

In the next lesson, we’ll cover another valuable tool for protecting routes: route guards.