Search code examples
angulartypescriptangular-routerangular-router-guards

Angular router activates wrong route guard


I'm having issues with Angular routing that don't seem to make much sense. With the following set-up, this outcome occurs...

  • App loads at path /
  • Auth guard runs
  • Auth guard returns false as there is not yet a JWT in storage
  • Redirection to /login works as expected.

However...

  • Navigate directly to /activate (Route in the account module)
  • Console logs that the auth guard is being run (Which shouldn't happen)
  • Redirected to /login

I'm struggling to understand why auth guard is running for the /activate route when it's not a child of the dashboard layout.

App routes

 {
    path: '',
    component: DashboardLayoutComponent,
    canActivate: [AuthGuard],
    canActivateChild: [DashboardGuard],
    children: [
      {
        path: 'schedule',
        loadChildren: () =>
          import('@libs/schedule').then(
            i => i.ScheduleModule
          ),
        data: {
          breadcrumb: 'Schedule'
        }
      },
      // Other feature modules omitted
      {
        path: '',
        redirectTo: '/schedule',
        pathMatch: 'full'
      }
    ]
  }

Account Routes

  { path: 'login', component: LoginComponent },
  { path: 'activate', component: ActivateComponent }

Auth Guard

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private router: Router,
    private accountService: accountService
  ) { }

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

    console.log('Auth Guard Start'); // <-- This appears in console

    return this.jwtService.getToken().pipe(
      map(token => {
        if (!token) {
          this.router.navigate(['/login']);
          return false;
        }
        // Attempt to populate the user using the token.
        this.accountService.populate();
        return true;
      }),
      take(1)
    );
  }
}

App Module

@NgModule({
  declarations: [AppComponent, DashboardLayoutComponent],
  imports: [
    // .. other modules omitted
    AccountModule,
    AppRoutingModule
  ],
  providers: [AuthGuard, DashboardGuard],
  bootstrap: [AppComponent]
})
export class AppModule { }

Additional information

This happens when running in production mode only.

Augury reports that /login and /activate are both siblings of the / route. https://i.sstatic.net/2LDY4.jpg

@angular/core: 8.2.6

@angular/router: 8.2.6


Solution

  • This issue was actually caused by my ngrx-router implementation where I was defining its initial state.

    Originally initial state was set in the following way as suggested here - https://github.com/ngrx/platform/issues/835#issuecomment-369592809

    export const routerInitialState: fromRouter.RouterReducerState<RouterStateUrl> = {
      state: {
        url: window.location.pathname,
        queryParams: getQueryParams(window.location.search.substring(1)),
        params: {}
      },
      navigationId: 0
    }
    

    This will not work in production builds. Navigating directly to a route like /activate, for example, causes a discrepancy between router state and angular router state, cancelling the navigation.

    The correct way top set the initial router state is as follows...

    export const routerInitialState: fromRouter.RouterReducerState<RouterStateUrl> = {
      state: {
        url: '/',
        queryParams: {},
        params: {}
      },
      navigationId: 0
    }
    

    With initialNavigation: 'enabled' in app-routing.module, the router state is updated at the earliest possible point and is synced with the angular router.