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

1

10

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.

Deering answered 5/9, 2017 at 15:30 Comment(0)
D
12

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.)

Deering answered 5/9, 2017 at 15:42 Comment(5)
So 'password/set' did not have to be secured by AuthenticationGuard?Juanajuanita
@AnjilDhamala Correct, it has its own guard, as you can see, which reimplements some of the logic of AuthenticationGuard, but allows you only to access one route.Deering
Thanks for getting back. I actually found that I had issues with my ngrx setup. I was stuck in a state change hell where the login page was subscribed to state changes and redirected to a certain page when state returned a redirect url. Unfortunately, the navigation ticked off guard checks and the page I was trying to deeplink to made another ngrx action call that kept changing the state and my Login page kept trying to react to the change. Thus, crashing my browser.Juanajuanita
@AnjilDhamala Ah, perhaps of note to your case is that our login page is actually outside the angular application, and served by JBoss, which then redirects to Angular. The entirety of our Angular application requires authentication. Good on you for reading up though! I'm sure you can post a question about it at this point.Deering
Looks like it also works with AuthGuard at the parent, but you had to work with returning urltree in AuthGurad. Got it working like that. Check this out: juristr.com/blog/2018/11/better-route-guard-redirects/…Ending

© 2022 - 2024 — McMap. All rights reserved.