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.