Router infinite loop with second canActivate guard on lazy-loaded modules

msanford picture msanford · Sep 5, 2017 · Viewed 8.3k times · Source

I have an angular 4.3.6 application with lazy-loaded modules. Here is a partial root router:

const routes: Routes = [
  { path: '', redirectTo: 'fleet', pathMatch: 'full' },
  {
    path: '',
    component: AppComponent,
    canActivate: [AuthenticationGuard],
    children: [
      {
        path: 'fleet',
        loadChildren: "./modules/fleet.module",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'password/set',
        loadChildren: "./modules/chooseNewPassword.module",
        canActivate: [ChoosePasswordGuard]
      }
    ]
  }
]
// Exports RouterModule.forRoot(routes, { enableTracing: true });

My child routers within these two example modules:

Fleet:

RouterModule.forChild([
  {
    path: '',
    component: FleetComponent,
    canActivate: [AuthenticationGuard]
  }
]);

Choose New Password:

RouterModule.forChild([
  {
    path: '',
    component: ChooseNewPasswordComponent,
    canActivate: [ChoosePasswordGuard]
  }
]);

The AuthenticationGuard calls a method that looks like this:

return this.getUserSession().map((userSession: UserSession) => {
  if (userSession && userSession.ok) {
    return true;
  }
  else if (userSession && userSession.expired) {
    this.router.navigate(['password/set'])
      .catch((e: Error) => console.error(e));
    return true;
  }
  else {
    window.location.replace('/');
    return false;
  }
}

So, if the user's session is ok, it activates the route. If the user's password is expired, it redirects the user to the choose new password module. If no session, redirects to login.

The ChoosePasswordGuard does a similar thing, but only protects the choose new password component (a different facility is used for setting passwords generically):

return this.getUserSession().map((userSession: UserSession) => {
  if (userSession) {
    return userSession.expired;
  }
  else {
    return false;
  }
});

This worked before module splitting.

Now, I'm stuck in a redirection loop. With router tracing on, I observe the following sequence. The user logs in and the AuthenticationGuard corrects redirects to the /password/set module, and is handed off to ChooseNewPasswordGuard:

  1. NavigationStart(id: 4, url: '/password/set')
  2. RoutesRecognized {id: 4, url: "/password/set", urlAfterRedirects: "/password/set", state: RouterStateSnapshot}
  3. GuardsCheckStart {id: 4, url: "/password/set", urlAfterRedirects: UrlTree, state: RouterStateSnapshot}
  4. GuardsCheckEnd {id: 4, url: "/password/set", urlAfterRedirects: UrlTree, state: RouterStateSnapshot, shouldActivate: true}
  5. NavigationCancel {id: 4, url: "/password/set", reason: ""}

And the this loop repeats.

(It also repeats if I replace the whole ChooseNewPasswordGuard with return Observable.of(true);)

EDIT: I am redirected to the root page (/) even when I provide /#/password/set in the URL bar...

Questions:

  1. What have I done wrong in my router(s) or guards to force this loop now that modules are lazy-loaded? I'm particularly confused by shouldActivate: true followed by NavigationCancel reason: "".

  2. Does it have something to do with the fact that I'm redirecting directly in the AuthenticationGuard, and now that this guard is applied to my main empty root route ({ path: '', redirectTo: 'fleet', pathMatch: 'full' }) it's always called and redirects, even once I've set the path?

  3. Do I actually need to repeat the canActivate guard in my child route and my root route?

  4. As usual, any other comments are welcome.

Answer

msanford picture msanford · Sep 5, 2017

The problem was that I was over-applying the AuthenticationGuard: it should not have been applied to the top-level AppComponent because it will always redirect to the Choose New Password module, even when it is loading that module.

My root routes should have looked like this:

const routes: Routes = [
  { path: '', redirectTo: 'fleet', pathMatch: 'full' },
  {
    path: '',
    component: AppComponent,
    // canActivate: [AuthenticationGuard], // <-- Remove this guard
    children: [
      {
        path: 'fleet',
        loadChildren: "./modules/fleet.module",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'password/set',
        loadChildren: "./modules/chooseNewPassword.module",
        canActivate: [ChoosePasswordGuard]
      }
    ]
  }
]

(I welcome and will happily Accept better explanations or better AuthenticationGuard patterns.)