Router Guards

In traditional server side applications the application would check permissions on the server and return a 403 error page if the user didn’t have permissions, or perhaps redirect them to a login/register page if they were not signed up.

Note

403 is a HTTP error code specifically this one means Permission Denied

We want to have the same functionality in our client side SPA, and with Router Guards we can.

With Router Guards we can prevent users from accessing areas that they’re not allowed to access, or, we can ask them for confirmation when leaving a certain area.

Learning Objectives

In this lecture you will learn:

  • The 4 different types of Router Guards

  • Implementing Router Guards as classes

  • How to implement a login guard for our iTunes app

Guard Types

In this chapter, we take a look at a few of the different types of guards and how to implement them for specific use cases.

  • Maybe the user must login (authenticate) first.

  • Perhaps the user has logged in but is not authorized to navigate to the target component.

  • We might ask the user if it’s OK to discard pending changes rather than save them.

There are four different types of Guards:

CanActivate

Checks to see if a user can visit a route.

CanActivateChild

Checks to see if a user can visit a routes children.

CanDeactivate

Checks to see if a user can exit a route.

Resolve

Performs route data retrieval before route activation.

CanLoad

Checks to see if a user can route to a module that lazy loaded.

For a given route we can implement zero or any number of Guards.

We’ll go through the first three as the last two are very advanced use cases and need lazy loading modules which we we haven’t covered.

CanActivate

Guards are implemented as services that need to be provided so we typically create them as @Injectable classes.

Guards return either true if the user can access a route or false if they can’t.

They can also return an Observable or Promise that later on resolves to a boolean in case the guard can’t answer the question straight away, for example it might need to call an API. Angular will keep the user waiting until the guard returns true or false.

Lets create a simple CanActivate guard.

First we need to import the CanActivate interface, like so:

import {CanActivate} from "@angular/router";

Then lets create an Injectable class called AlwaysAuthGuard which implements the canActivate function, like so:

class AlwaysAuthGuard implements CanActivate {
  canActivate() {
    console.log("AlwaysAuthGuard");
    return true;
  }
}

This guard returns true all the time, so doesn’t really guard anything. It lets all users through but at the same time our guard logs "AlwaysAuthGuard" to the console so we can at least see when it’s being used.

We need to provide this guard, for this example lets configure it via our NgModule, like so:

@NgModule({
  .
  .
  providers: [
    .
    .
    AlwaysAuthGuard
  ]
})

Finally we need to add this guard to one or more of our routes, lets add it to our ArtistComponent route like so:

const routes: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'full'},
  {path: 'find', redirectTo: 'search'},
  {path: 'home', component: HomeComponent},
  {path: 'search', component: SearchComponent},
  {
    path: 'artist/:artistId',
    component: ArtistComponent,
    canActivate: [AlwaysAuthGuard], (1)
    children: [
      {path: '', redirectTo: 'tracks'},
      {path: 'tracks', component: ArtistTrackListComponent},
      {path: 'albums', component: ArtistAlbumListComponent},
    ]
  },
  {path: '**', component: HomeComponent}
];
1 We added our AlwaysAuthGuard to the list of canActivate guards for this route.

Note

Since it holds an array we could have multiple guards for a single route.

Note

If this was a canActivateChild guard we would be adding it to the canActivateChild property and so on for the other guard types.

Now every-time we navigate to the ArtistComponent route we get "AlwaysAuthGuard" printed to the console so we know that the AlwaysAuthGuard is working.

OnlyLoggedInUsersGuard

The most typical use case for the CanActivate guard is some form of checking to see if the user has permissions to view a page.

Normally in an Angular application we would have a service which held whether or not the current user is logged in or what permissions they have.

We will simulate this via a mock UserService like so:

class UserService {
  isLoggedIn(): boolean {
    return false;
  }
}

This service has one function isLoggedIn() which always returns false.

Lets create another guard called OnlyLoggedInUsersGuard which only allows logged in users to view a route.

@Injectable()
class OnlyLoggedInUsersGuard implements CanActivate { (1)
  constructor(private userService: UserService) {}; (2)

  canActivate() {
    console.log("OnlyLoggedInUsers");
    if (this.userService.isLoggedIn()) { (3)
      return true;
    } else {
      window.alert("You don't have permission to view this page"); (4)
      return false;
    }
  }
}
1 We create a new CanActivate guard called OnlyLoggedInUsersGuard
2 We inject and store UserService into the constructor for our class.
3 If the user is logged in the guard passes and lets the user through.
4 If the user is not logged in the guard fails, we show the user an alert and the page doesn’t navigate to the new URL.

Finally we need to add this guard to the list of guards for our search route, like so:

{
  path: 'artist/:artistId',
  component: ArtistComponent,
  canActivate: [OnlyLoggedInUsersGuard, AlwaysAuthGuard], (1)
  children: [
    {path: '', redirectTo: 'tracks'},
    {path: 'tracks', component: ArtistTrackListComponent},
    {path: 'albums', component: ArtistAlbumListComponent},
  ]
}
1 We add OnlyLoggedInUsersGuard to the list of guards for our route.

Now when we try to navigate to the search view we are blocked from doing so and shown a window alert, like so:

only loggedin users guard

Tip

If we want to redirect users to a login page we may inject Router into the constructor and then use the navigate function to redirect them to the appropriate login page.

Note

So the rest of the samples in this chapter work we will change the isLoggedIn function on our UserService to return true instead. If you want to play around with this functionality in the plunker remember to switch that back to returning false.

CanActivateChild

As well as CanActivate we also have CanActivateChild which we implement in similar way.

Lets do the same as the CanActivate example and create a guard called AlwaysAuthChildrenGuard.

import {CanActivateChild} from "@angular/router";

class AlwaysAuthChildrenGuard implements CanActivateChild {
  canActivateChild() {
    console.log("AlwaysAuthChildrenGuard");
    return true;
  }
}

Note

Remember to provide it on our NgModule

We add the guard to the canActivateChild child property on our ArtistComponent route

{
  path: 'artist/:artistId',
  component: ArtistComponent,
  canActivate: [OnlyLoggedInUsersGuard, AlwaysAuthGuard],
  canActivateChild: [AlwaysAuthChildrenGuard],
  children: [
    {path: '', redirectTo: 'tracks'},
    {path: 'tracks', component: ArtistTrackListComponent},
    {path: 'albums', component: ArtistAlbumListComponent},
  ]
}

Now every-time we try to activate either the ArtistTrackListComponent or ArtistAlbumListComponent child routes it checks the AlwaysAuthChildrenGuard to see if the user has permission.

Guard function parameters

To help in determining whether or not a guard should accept or deny access the guard function can be passed certain arguments:

  1. component: Component this is the component itself.

  2. route: ActivatedRouteSnapshot — this is the future route that will be activated if the guard passes, we can use it’s params property to extract the route params.

  3. state: RouterStateSnapshot — this is the future RouterState if the guard passes, we can find the URL we are trying to navigate to from the url property.

CanDeactivate

A third type of guard we can add to our application is a CanDeactivate guard which is usually used to warn people if they are navigating away from a page where they have some unsaved changes.

Lets create a simple CanDeactivate guard which checks to see if the user navigates away from the search page without actually performing a search.

Firstly lets create a function called canDeactivate on our SearchComponent, it should be the component that decides whether or not it has unsaved changes.

canDeactivate() {
  return this.itunes.results.length > 0;
}

As a proxy for unsaved changes we are just seeing if the user has performed a search, if so then the results array should be > 0.

Next lets create a CanDeactivate guard.

class UnsearchedTermGuard implements CanDeactivate<SearchComponent> { (1)
  canDeactivate(component: SearchComponent, (1)
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot): boolean {
    console.log("UnsearchedTermGuard");
    console.log(route.params);
    console.log(state.url);
    return component.canDeactivate() || window.confirm("Are you sure?");
  }
}
1 We implement from the CanDeactivate interface but this is a generic interface so needs for us to additionally provide it the type of our the component in-between some angle brackets CanDeactivate<SearchComponent>. This is so it knows the type of the component to pass into the canDeactivate function itself.

Now if we navigate to the search page, we don’t perform a search and then we try to navigate away we get a popup confirmation box. If we say yes we navigate away if we say no we stay on the page, like so:

can deactivate guard

Important

As of 18/10/2016 there is a bug in Angular version 2.1.0. The canDeactivate function is not passed the future ActivatedRouteSnapshot or the future RouterStateSnapshot.

This is why our example use case is so woefully inadequate.

If you want to encourage the Angular team to fix this issue then please give a thumbs up to this issue on github https://github.com/angular/angular/issues/9853

Summary

With guards we can add checks to restrict access to a user to certain pages on our site.

Depending on the type of guard the guard function also has some arguments passed to it which we can take advantage of if we want, namely the future ActivatedRoute and the future RouterState and for CanDeactivate guards we also have the ability to get the component itself.

Guards themselves are just classes and as such can have any other dependencies injected into their constructor so can work in conjunction with other services to figure out if the guard passes or fails.

Guard functions can return either a boolean or an Observable<boolean> or Promise<boolean> which resolves to a boolean at some point in the future.

A route can be configured with multiple guards and the guards are checked in the order they were added to the route.

We are almost complete with this section on Routing, in the next lecture we will cover the concept of Path Strategies.

Listing

Listing 1. script.ts
import {NgModule, Component, Injectable} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {JsonpModule, Jsonp, Response} from '@angular/http';
import {ReactiveFormsModule, FormControl, FormsModule} from '@angular/forms';
import {Routes, RouterModule} from "@angular/router";
import {Observable} from 'rxjs';
import 'rxjs/add/operator/toPromise';
import {
    Routes,
    RouterModule,
    Router,
    ActivatedRoute,
    CanActivate,
    CanActivateChild,
    CanDeactivate,
    ActivatedRouteSnapshot,
    RouterStateSnapshot
} from "@angular/router";


// Domain models

class SearchItem {
  constructor(public name: string,
              public artist: string,
              public link: string,
              public thumbnail: string,
              public artistId: string) {
  }
}

// Services

@Injectable()
class SearchService {
  apiRoot: string = 'https://itunes.apple.com/search';
  results: SearchItem[];

  constructor(private jsonp: Jsonp) {
    this.results = [];
  }

  search(term: string) {
    return new Promise((resolve, reject) => {
      this.results = [];
      let apiURL = `${this.apiRoot}?term=${term}&media=music&limit=20&callback=JSONP_CALLBACK`;
      this.jsonp.request(apiURL)
          .toPromise()
          .then(
              res => { // Success
                this.results = res.json().results.map(item => {
                  return new SearchItem(
                      item.trackName,
                      item.artistName,
                      item.trackViewUrl,
                      item.artworkUrl30,
                      item.artistId
                  );
                });
                resolve();
              },
              msg => { // Error
                reject(msg);
              }
          );
    });
  }
}

@Injectable()
class UserService {
  isLoggedIn(): boolean {
    return true; // Switch to `false` to make OnlyLoggedInUsersGuard work
  }
}

// Components

@Component({
  selector: 'app-search',
  template: `<form class="form-inline">
  <div class="form-group">
    <input type="search"
           class="form-control"
           placeholder="Enter search string"
           #search>
  </div>
  <button type="button"
          class="btn btn-primary"
          (click)="onSearch(search.value)">
    Search
  </button>
</form>

<hr />

<div class="text-center">
  <p class="lead"
     *ngIf="loading">Loading...</p>
</div>

<div class="list-group">
  <a [routerLink]="['/artist', track.artistId]"
     class="list-group-item list-group-item-action"
     *ngFor="let track of itunes.results">
    <img src="{{track.thumbnail}}">
    {{ track.name }} <span class="text-muted">by</span> {{ track.artist }}
  </a>
</div>
 `
})
class SearchComponent {
  private loading: boolean = false;

  constructor(private itunes: SearchService,
              private route: ActivatedRoute,
              private router: Router) {
    this.route.params.subscribe(params => {
      console.log(params);
      if (params['term']) {
        this.doSearch(params['term'])
      }
    });
  }

  doSearch(term: string) {
    this.loading = true;
    this.itunes.search(term).then(_ => this.loading = false)
  }

  onSearch(term: string) {
    this.router.navigate(['search', {term: term}]);
  }

  canDeactivate() {
    return this.itunes.results.length > 0;
  }
}

@Component({
  selector: 'app-home',
  template: `
<div class="jumbotron">
  <h1 class="display-3">iTunes Search App</h1>
</div>
 `
})
class HomeComponent {
}

@Component({
  selector: 'app-header',
  template: `<nav class="navbar navbar-light bg-faded">
  <a class="navbar-brand"
     [routerLink]="['home']">iTunes Search App
  </a>
  <ul class="nav navbar-nav">
    <li class="nav-item"
        [routerLinkActive]="['active']">
      <a class="nav-link"
         [routerLink]="['home']">Home
      </a>
    </li>
    <li class="nav-item"
        [routerLinkActive]="['active']">
      <a class="nav-link"
         [routerLink]="['search']">Search
      </a>
    </li>
  </ul>
</nav>
 `
})
class HeaderComponent {
  constructor(private router: Router) {
  }

  goHome() {
    this.router.navigate(['']);
  }

  goSearch() {
    this.router.navigate(['search']);
  }
}

@Component({
  selector: 'app-artist-track-list',
  template: `
<ul class="list-group">
  <li class="list-group-item"
      *ngFor="let track of tracks">
    <img src="{{track.artworkUrl30}}">
    <a target="_blank"
       href="{{track.trackViewUrl}}">{{ track.trackName }}
    </a>
  </li>
</ul>
 `
})
class ArtistTrackListComponent {
  private tracks: any[];

  constructor(private jsonp: Jsonp,
              private route: ActivatedRoute) {
    this.route.parent.params.subscribe(params => {
      this.jsonp.request(`https://itunes.apple.com/lookup?id=${params['artistId']}&entity=song&callback=JSONP_CALLBACK`)
          .toPromise()
          .then(res => {
            console.log(res.json());
            this.tracks = res.json().results.slice(1);
          });
    });
  }
}

@Component({
  selector: 'app-artist-album-list',
  template: `<ul class="list-group">
  <li class="list-group-item"
      *ngFor="let album of albums">
    <img src="{{album.artworkUrl60}}">
    <a target="_blank"
       href="{{album.collectionViewUrl}}">{{ album.collectionName }}
    </a>
  </li>
</ul>
 `
})
class ArtistAlbumListComponent {
  private albums: any[];

  constructor(private jsonp: Jsonp,
              private route: ActivatedRoute) {
    this.route.parent.params.subscribe(params => {
      this.jsonp.request(`https://itunes.apple.com/lookup?id=${params['artistId']}&entity=album&callback=JSONP_CALLBACK`)
          .toPromise()
          .then(res => {
            console.log(res.json());
            this.albums = res.json().results.slice(1);
          });
    });
  }
}

@Component({
  selector: 'app-artist',
  template: `<div class="card">
  <div class="card-block">
    <h4>{{artist?.artistName}} <span class="tag tag-default">{{artist?.primaryGenreName}}</span></h4>
    <hr />
    <footer>
      <ul class="nav nav-pills">
        <li class="nav-item">
          <a class="nav-link"
             [routerLinkActive]="['active']"
             [routerLink]="['./tracks']">Tracks
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link"
             [routerLinkActive]="['active']"
             [routerLink]="['./albums']">Albums
          </a>
        </li>
      </ul>
    </footer>
  </div>
</div>

<div class="m-t-1">
  <router-outlet></router-outlet>
</div>
 `
})
class ArtistComponent {
  private artist: any;

  constructor(private jsonp: Jsonp,
              private route: ActivatedRoute) {
    this.route.params.subscribe(params => {
      this.jsonp.request(`https://itunes.apple.com/lookup?id=${params['artistId']}&callback=JSONP_CALLBACK`)
          .toPromise()
          .then(res => {
            console.log(res.json());
            this.artist = res.json().results[0];
            console.log(this.artist);
          });
    });
  }
}

@Component({
  selector: 'app',
  template: `
  <app-header></app-header>
  <div class="m-t-1">
    <router-outlet></router-outlet>
  </div>
 `
})
class AppComponent {
}

// Guards

class AlwaysAuthGuard implements CanActivate {
  canActivate() {
    console.log("AlwaysAuthGuard");
    return true;
  }
}

@Injectable()
class OnlyLoggedInUsersGuard implements CanActivate {
  constructor(private userService: UserService) {
  };

  canActivate() {
    console.log("OnlyLoggedInUsers");
    if (this.userService.isLoggedIn()) {
      return true;
    } else {
      window.alert("You don't have permission to view this page");
      return false;
    }
  }
}


class AlwaysAuthChildrenGuard implements CanActivateChild {
  canActivateChild() {
    console.log("AlwaysAuthChildrenGuard");
    return true;
  }
}

class UnsearchedTermGuard implements CanDeactivate<SearchComponent> {
  canDeactivate(component: SearchComponent,
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot): boolean {
    console.log("UnsearchedTermGuard");
    console.log(state.url);
    return component.canDeactivate() || window.confirm("Are you sure?");
  }
}

// Routes

const routes: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'full'},
  {path: 'find', redirectTo: 'search'},
  {path: 'home', component: HomeComponent},
  {path: 'search', component: SearchComponent, canDeactivate: [UnsearchedTermGuard]},
  {
    path: 'artist/:artistId',
    component: ArtistComponent,
    canActivate: [OnlyLoggedInUsersGuard, AlwaysAuthGuard],
    canActivateChild: [AlwaysAuthChildrenGuard],
    children: [
      {path: '', redirectTo: 'tracks', pathMatch: 'full'},
      {path: 'tracks', component: ArtistTrackListComponent},
      {path: 'albums', component: ArtistAlbumListComponent},
    ]
  },
  {path: '**', component: HomeComponent}
];

// Bootstrap

@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    FormsModule,
    JsonpModule,
    RouterModule.forRoot(routes, {useHash: true})
  ],
  declarations: [
    AppComponent,
    SearchComponent,
    HomeComponent,
    HeaderComponent,
    ArtistAlbumListComponent,
    ArtistTrackListComponent,
    ArtistComponent
  ],
  bootstrap: [AppComponent],
  providers: [
    SearchService,
    UserService,
    OnlyLoggedInUsersGuard,
    AlwaysAuthGuard,
    AlwaysAuthChildrenGuard,
    UnsearchedTermGuard
  ]
})
class AppModule {
}

platformBrowserDynamic().bootstrapModule(AppModule);

Learn Angular 5 For FREE

I've released my 700 page Kick Starter funded Angular 5 book for FREE