2
votes

I'm securing my Angular 6 app with JWT tokens. I have an auth service to secure UI elements:

export class AuthService {

  constructor(
    private http: HttpClient,
    private router: Router,
    public jwtHelper: JwtHelperService,
  ) {}

  isAuthenticated(): boolean {
    return !this.isTokenExpired;
  }

which I can then call in my components:

<span class="page-nav" *ngIf="!authService.isAuthenticated()">
    <a mat-button routerLink="/login">
        Log In
    </a>
</span>

I also have an auth guard for my routes:

export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(): boolean {
    if (!this.authService.isAuthenticated()) {
      this.router.navigate(['/login']);
      return false;
    }
    return true;
  }
}

This all works fine, except when the token becomes invalid, then the current page does not route to login.

For instance, if I login, get a valid JWT token, I can navigate to a protected page, and protected UI elements appear (navigation etc.). If I then remove the JWT token from local storage via chrome dev tools, and click on the navigation, the protected UI elements disappear as expected, but the protected page stays, the route auth guard is not called, and only routes to login when navigating to a different page.

I've tried routing in the isAuthenticated() authService method, but this causes an infinite loop. How can I automatically route to login outside of calling the AuthGuard?

EDIT

routing:

const routes: Routes = [
  { path: '', redirectTo: '/products', pathMatch: 'full' },
  {
    path: 'products',
    component: ProductsComponent,
    canActivate: [AuthGuard],
    children: [
      { path: ':id', component: ProductComponent, canActivate: [AuthGuard] },
    ],
  },
  {
    path: 'search',
    component: SearchComponent,
    canActivate: [AuthGuard],
  },
  { path: 'login', component: LoginComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

EDIT: I realise that this is the expected behaviour, as @GangadharJannu mentions, the router is not aware that the UI elements have called the isAuthenticated() method and are now hidden as it is returning false, not until the AuthService refresh method is called and carries out the same check. I'm interested to know the best way to let the router know to navigate to login when the UI elements call the isAuthenticated() method?

EDIT: My working solution:

 import { map } from 'rxjs/operators';
 import { interval } from 'rxjs';
 import { JwtHelperService } from '@auth0/angular-jwt';

 ...

 public validateToken(): void {
    interval(1000).pipe(
      map(() => {
        const isExpired: boolean = this.jwtHelper.isTokenExpired(
          localStorage.getItem('jwt_token'),
        );
        if (isExpired) {
          this.logout();
        }
      }),
    );
  }
1
can we see the routing setup? - Nima Hakimi
Could be because this.isTokenExpired is not set properly. - Ploppy
@NimaHakimi I've added the routing. - bordeltabernacle
@Ploppy No, that's not it, the app has a refresh timer that renews the JWT token every minute, it also checks the current token and navigates to login if invalid. Plus it's working for the UI elements, just not being accessed by anything that is then routing to /login. - bordeltabernacle
Which one is your protected component? - Gangadhar JANNU

1 Answers

2
votes

We had parent empty state just to guard the protected part of our application, you might want to try that. Try routing config similar to this:

const routes: Routes = [{ 
    path: '', 
    canActivateChild: [AuthGuard], 
    runGuardsAndResolvers: 'always',
    children: [{
        path: 'products',
        component: ProductsComponent,
        children: [
          { path: ':id', component: ProductComponent },
        ],
      }, {
        path: 'search',
        component: SearchComponent
    }]
  },
  { path: '', redirectTo: '/products', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
];

Notice runGuardsAndResolvers: 'always' - that tells angular to always run guards and resolvers even when navigating to same state.

Edit

So basically you want to observe local storage and react on changes, so when the token is removed from the storage or expires then you want to immediately navigate to login component. Some ideas:

  • you can increase the frequency of the expiration check to 1s, this would not hurt much as currently you are checking whether the token expired each render cycle
  • you can setup a timed observable each time the token is refreshed, so it fires one second after expiration and navigates to login
  • to observe changes on local storage itself you could search for some library that has this feature (like this one) or just use native storage change event directly
  • if you don't care about use case when user deletes the token manually, then you could navigate to login when your app removes the token from local storage (question/answers)