July 21, 2020

How to Auth Lazily-Loaded Routes in Angular

by Chidume Nnamdi

How to Auth Lazily-Loaded Routes in Angular

Angular has built-in authentication mechanisms for protecting routes in our app. These are called Route Guards. You might recall we did a tutorial recently about creating a secure role-based app using Angular route guards — so feel free to check that tutorial as well.

In this article, we will cover a specific security problem that will arise when you’re trying to add lazily-loaded routes in Angular.

Let’s start with a little bit of background.

Route Guards is a feature of the @angular/router module that provides interfaces and methods to reject or allow navigation to routes.

There are five 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 route’s 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 is lazy loaded.

As we’ll see in a minute, only the CanLoad guard protects lazy-loaded routes.

Let's say we have our routes configuration like this:

const routes: Routes =
[
    {
        path: 'admin',
        component: AdminComponent
    },
    {
        path:'dashboard',
        component: DashboardComponent,
        loadChildren: ()=> import("./dashboard/dashboard.module").then(m => m.DashboardModule)
    }
]

We have two routes or paths here: 'admin' and 'dashboard'. The 'dashboard' route is lazy-loaded, which means that the route will not be loaded with the bundle on the initial load of the Angular app. It will be loaded only when the user navigates to that route.

The dashboard is a lazy-loaded route because of the loadChildren property there. It is a function value that runs when the route is navigated to. This function uses the import function to load its component module (DashboardModule), which contains components and other Modules, Components, Directives, Services attached to this module. The function returns a Promise that resolves to the NgModule instance of the module.

We can protect the admin route by using the CanActivate guard:

@Injectable()
export class AdminAuthGuard implements CanActivate, CanActivateChild {...}

const routes: Routes =
[
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [AdminAuthGuard]
    },
    {
        path:'dashboard',
        component: DashboardComponent,
        loadChildren: ()=> import("./dashboard/dashboard.module").then(m => m.DashboardModule)
    }
]

But we can't use it to protect lazy-loaded routes like the dashboard route.

@Injectable()
export class AdminAuthGuard implements CanActivate, CanActivateChild {...}

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {...}

const routes: Routes =
[
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [AdminAuthGuard]
    },
    {
        path:'dashboard',
        canActivate: [AuthGuard]
        component: DashboardComponent,
        loadChildren: ()=> import("./dashboard/dashboard.module").then(m => m.DashboardModule)
    }
]

It just won't work. Navigating to the dashboard will load the DashboardModule without any security checks.

To auth lazily-loaded routes, we will use the CanLoad guard, as recommended in the Angular docs.

We will create a guard class that will implement the CanLoad interface and define a canLoad method:

@Injectable()
export class AuthGuard implements CanLoad {

  constructor(...) {}

  checkTokenExpiration() {
      // checking whether your token has expired
      ... implementation here
      return false || true
  }

  canLoad() {
    return this.checkTokenExpiration()
  }
}

Our AuthGuard implemented the CanLoad interface and now has a canLoad method. The canLoad checks for token expiration via the checktokenExpiration method. Just like canActivate and canActivateChild, canLoad returns true if a route is to be loaded or false if it is not to be loaded.

Now, we will remove canActivate from the dashboard route and add canLoad:

...

const routes: Routes =
[
    ...,
    {
        path:'dashboard',
        canLoad:[AuthGuard],
        component: DashboardComponent,
        loadChildren: ()=> import("./dashboard/dashboard.module").then(m => m.DashboardModule)
    }
]

Navigating to dashboard would auth in AuthGuard#canLoad. If it returns true, the Module is loaded; if not, the navigation is denied — which is precisely what we were looking for.

So, we see that the CanLoad guard is used to auth/protect or authorize navigation to lazily-loaded routes in Angular.

Problem

Still, we are faced with a problem. While CanLoad auths lazily-loaded routes, once the Module is loaded, then CanLoad would no longer auth that route again.

Think of this example: a user is logged and authenticated in your Angular app and navigates to the dashboard route for the first time — it will pass, the Module will load. Now, the user logs out of the app — he is not authenticated anymore. But, when he navigates to the dashboard route during his logged-out state, the Module will load! Remember, he is already logged-out, not auth anymore — yet, the dashboard route loaded. This is a security breach.

This happened because the DashboardModule route was already loaded at the initial navigation to the route when the user was authenticated. The CanLoad guard doesn't work anymore once the Module of the route it protects is loaded. The route will have to be authenticated by a canActivate guard.

To patch this, we need to include a canActivate guard on the lazy route.

...

const routes: Routes =
[
    ...,
    {
        path: 'dashboard',
        canLoad: [ AuthGuard ],
        canActivate: [ AuthGuard ],
        component: DashboardComponent,
        loadChildren: ()=> import("./dashboard/dashboard.module").then(m => m.DashboardModule)
    }
]

Thanks to this patch, CanLoad will work on the initial navigation and CanActivate will kick in on further navigations after the initial navigation. Security breach avoided!

Conclusion

Authentication in Angular is broad.

Thanks to an amazing effort by the Angular team, we have built-in strong features for route and HTTP request validation. Still, nothing is 100% secured, we will still have to take a lot of security design decisions ourselves.

If you have comments, suggestions, or corrections, feel free to DM me.

Thanks!


To improve the security of your Angular apps even further and prevent code theft and reverse-engineering, be sure to check our tutorial.