We're live-coding on Twitch! Join us!
Protecting Angular v2+ Routes with CanActivate/CanActivateChild Guards

Protecting Angular v2+ Routes with CanActivate/CanActivateChild Guards

Code

Protecting routes is something that has to happen in any app with authentication. There will be sections of your applications that you don't want certain users to get to.

We'll be looking at a simple example of how to use Angular router guards to protect an area of our site. While this will be a simple example that you can reference back to, we'll look at more advanced setups in future articles.

In this tutorial, we'll look at how we can use Angular guards to:

  • Protect an entire section (route)
  • Protect just a part of the app (child routes)
Related Video Course: Routing Angular Applications

TLDR: How to Create and Use An Angular Guard

The overall process for protecting Angular routes:

  • Create a guard service: ng g guard auth
  • Create canActivate() or canActivateChild() methods
  • Use the guard when defining routes

To create a guard using the Angular CLI, use:

ng g guard auth

This will create an auth.guard.ts

Here is the generated file:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class AuthGuard implements CanActivate {

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
      return true;
  }

}

Now we can add in an AuthService that we may have:

Essential Reading: Learn React from Scratch! (2020 Edition)
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';

// import the auth service here
// import { AuthService } from '../core/auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  // add the service we need
  constructor(
    private auth: AuthService,
    private router: Router
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

      // handle any redirects if a user isn't authenticated
      if (!this.auth.isLoggedIn) {
        // redirect the user
        this.router.navigate(['/login']);
        return false;
      }

      return true;
  }

}

In the canActivate() method, we can do any checks we need to check if a user has access. Return a boolean true/false. Also, this is where you can handle routing users away from a route if they don't have access.

Import this new class into a module so your app knows how to grab it. Then you can use this when defining routes:

// import the newly created AuthGuard

const routes: Routes = [
  {
    path: 'account',
    canActivate: [AuthGuard]
  }
];

This will protect the /account route! That's the quick overview on how to use the guards. Let's go a bit more in depth.

The Angular Router Guards

The Angular router ships with a feature called guards. These provide us with ways to control the flow of our application. We can stop a user from visitng certain routes, stop a user from leaving routes, and more.

We'll only be talking about CanActivate/CanActivateChild in this tutorial, but be aware that these are the available guards:

  • CanActivate: Check if a user has access
  • CanActivateChild: Check if a user has access to any of the child routes
  • CanDeactivate: Can a user leave a page? For example, they haven't finished editing a post
  • Resolve: Grab data before the route is instantiated
  • Lazy loading feature modules.
  • CanLoad: Check to see if we can load the routes assets

Creating a Sample App

Our sample app will be a very simple one. We'll use the Angular CLI to create a new Angular app. We'll also create a brand new account and dashboard sections. The requirements:

  • Only logged in users can visit the /account section
  • Only admins can access the /dashboard section
  • Only super admins can visit the /dashboard/super-duper section

Let's get started!

Using the CLI to Create an App

Once you have the CLI installed, create a new app with routing and a home page component using:

ng new angular-guard-app --routing

This will generate a new app for us with routing configured. Check out our src/ folder:

We can open up our app now using:

ng serve --open

Creating a Main Route

We'll need a home page route so that we can have a default path configured. Create a home component with:

ng g component home

The CLI will automatically add this new component to our app.module.ts. All we have to do is set it in our app-routing.module.ts:

// app-routing.module.ts 

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Now when you visit your app, you should see home works in tiny text at the very bottom. This is where the <router-outlet> is located.

Next we'll move onto our app sections.

Creating Our App Sections

We'll need an account section and also a dashboard section. While we could create these as components, we'll be creating them as modules.

Here's the modules and components we'll want.

|- dashboard.module.ts
    |- dashboard-home.component.ts
    |- super-duper.component.ts
|- account.module.ts
    |- account-home.component.ts

Since these are sections of our app that a user might not always visit, we may want to lazy-load these sections. By lazy loading, the assets for these sections will only be loaded when a user visits that section.

# create the account module and main component
ng g module account --routing
ng g component account/account-home

# create the dashboard module and main component
ng g module dashboard --routing
ng g component dashboard/dashboard-home

# create the dashboard child route for super admins
ng g component dashboard/super-duper

These commands will get us two new modules and a new component.

Now we can setup the routing for each of these modules.

Routing the Child Modules

Each module created will have its own routing file (dashboard-routing.module.ts and account-routing.module.ts).

We'll set up the routing in each of these child routing files. The auth guards will be used when we define the routes in these modules. The reason we attach guards here and not in the main app-router.module.ts is because we'll be lazy-loading them and we'll have more control in the child routing files.

Let's setup the routes now:

// account-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AccountHomeComponent } from './account-home/account-home.component';

const routes: Routes = [
  {
    path: '',
    component: AccountHomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AccountRoutingModule { }
// dashboard-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component';
import { SuperDuperComponent } from './super-duper/super-duper.component';

const routes: Routes = [
  {
    path: '',
    component: DashboardHomeComponent,
    children: [
      {
        path: 'super-duper',
        component: SuperDuperComponent
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class DashboardRoutingModule { }

Now we can add these to the main app-routing.module.ts. We'll use the syntax for lazy-loading by using the loadChildren parameter.

// app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'account',
    loadChildren: 'app/account/account.module#AccountModule'
  },
  {
    path: 'dashboard',
    loadChildren: 'app/dashboard/dashboard.module#DashboardModule'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Notice that we don't import the AccountModule or DashboardModule. We also don't import these into the main AppModule; if we did import it there, then it would no longer be lazy-loaded.

Since we defined the path as dashboard and account in the main app-routing.module.ts, we'll define the top level route as '' in the child routes.

Now that we have our routes ready, we can work on the auth service and guards!

Authentication Service

We'll create a generic authentication service that won't hook into anything. We'll return a boolean for true to show a user is logged in. In your application, you would hook this into a service like a backend API to check auth status.

We'll use the CLI to create this service. I'm going to drop this right into the main app/ folder but it would be good practice to create services and guards inside a CoreModule or its own AuthModule.

ng g service auth/auth

We'll add a method to check for isLoggedIn and isSuperAdmin. We'll just fill these in with a boolean but you'll need to make sure to add the logic for your own apps. It wouldn't be great security if your AuthService always returned true!

import { Injectable } from '@angular/core';

@Injectable()
export class AuthService {

  constructor() { }

  get isLoggedIn() {
    return true;
  }

  get isSuperAdmin() {
    return true;
  }

}

Note We're using the get keyword here to treat these as properties. That way we can access these functions as:

// with the get keyword we can do this
const isLoggedIn = this.auth.isLoggedIn;

// without it, we have to treat it was a method
const isLoggedIn = this.auth.isLoggedIn();

Our service is ready to be used in our guards!

Authentication Guard

We'll need a guard to check if a user is logged in or not. We'll create it in the auth/guards folder that we'll create now with the CLI:

ng g guard auth/guards/auth

Here's the new guard in all its glory:

// app/auth/guards/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return true;
  }
}

We'll inject the AuthService and bring in the Router so that we can:

  • check if a user is logged in
  • redirect them if not logged in
// app/auth/guards/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from '../auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(
    private auth: AuthService,
    private router: Router
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    // redirect and return false
    if (!this.auth.isLoggedIn) {
      this.router.navigate(['']);
      return false;
    }

    return true;
  }
}

Easy cheese! Let's make an admin guard now for our dashboard super-duper section.

Super Admin Guard

ng g guard auth/guards/admin
// app/auth/guards/admin.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from '../auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(
    private auth: AuthService,
    private router: Router
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    // redirect and return false
    if (!this.auth.isSuperAdmin) {
      this.router.navigate(['']);
      return false;
    }

    return true;
  }
}

With our service and guards ready to use, we'll need to register them with our application's AppModules. While the CLI automatically registers components, it doesn't do the same for services.

// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { AuthService } from './auth/auth.service';
import { AuthGuard } from './auth/guards/auth.guard';
import { AdminGuard } from './auth/guards/admin.guard';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [
    AuthService,
    AuthGuard,
    AdminGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

All good! Now we can finally apply these to our routes. Open up the child routes and let's use our new guards.

Applying to Our Routes

In the account routing, we'll use the main AuthGuard. Here are the relevant lines:

// account-routing.module.ts
...

import { AuthGuard } from '../auth/guards/auth.guard';

const routes: Routes = [
  {
    path: '',
    component: AccountHomeComponent,
    canActivate: [AuthGuard]
  }
];

...

Let's test this out! Visit http://localhost:4200/account in your browser. You should be able to see at the very bottom account-home works!

Testing the Auth

We need to make sure that our user actually gets redirected to the home page if they aren't authenticated. Go back into the AuthService and change the isLoggedIn() method to return false.

get isLoggedIn() {
  return false;
}

Now when we visit http://localhost:4200/account, we'll be redirected to the home page!

How does this work?

When we define the route and its guard, Angular will look inside the guard and look for the corresponding method. For instance, we defined this line:

{
  path: '',
  component: AccountHomeComponent,
  canActivate: [AuthGuard]
}

Angular will look inside the AuthGuard file and match the canActivate method.

CanActivateChild

We could do the same exact thing for our dashboard section, but let's switch it up. We'll use canActivateChild instead of the main canActivate. Remember we defined child routes inside the dashboard-routing.module.ts.

Let's update our admin guard to also implement the canActivateChild method.

// dashboard-routing.module.ts

import { Injectable } from '@angular/core';
import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from '../auth.service';

@Injectable()
export class AdminGuard implements CanActivate, CanActivateChild {

  constructor(
    private auth: AuthService,
    private router: Router
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    // redirect and return false
    if (!this.auth.isLoggedIn) {
      this.router.navigate(['']);
      return false;
    }

    return true;
  }

  canActivateChild(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    // redirect and return false
    if (!this.auth.isLoggedIn) {
      this.router.navigate(['']);
      return false;
    }

    return true;
  }
}

Then we can apply this to our dashboard routes:

// dashboard-routing.module.ts
...

import { AdminGuard } from '../auth/guards/admin.guard';

const routes: Routes = [
  {
    path: '',
    component: DashboardHomeComponent,
    canActivateChild: [AdminGuard],
    children: [
      {
        path: 'super-duper',
        component: SuperDuperComponent
      }
    ]
  }
];

...

One thing to note is that since these are child routes, we'll need to add a new <router-outlet></router-outlet> to the dashboard-home.component.html:

<p>
  dashboard-home works!
</p>


<router-outlet></router-outlet>

Now our guard works! We can go into AuthService and change the isAdmin() method to check.

The reason you would use CanActivateChild over CanActivate is that you can limit the nested router-outlet. Notice we can still see the dashboard-home.component but not anything in its own nested routes.

Conclusion

Angular CanActivate and CanActivateChild guards provide a clean and defined way to "guard" certain routes. We've only taken a look at two of the guards, but all of them are worth looking into! They are a solid way to manage the flow of users throughout your app.

Like this article? Follow @chrisoncode on Twitter