2

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();
        }
      }),
    );
  }
Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
bordeltabernacle
  • 1,603
  • 5
  • 24
  • 46

1 Answers1

2

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)
Ludevik
  • 6,954
  • 1
  • 41
  • 59
  • Thanks @Ludevik but I still have the same behaviour. The problem is that it's not routing at all. Clicking on the navigation just checks the state of the authentication, and doesn't act on the routerLink if it's not valid. – bordeltabernacle Oct 10 '18 at 13:06
  • So you have some custom check when clicking on navigation? And when user is no longer authenticated it does nothing? – Ludevik Oct 10 '18 at 13:16
  • the nav buttons hav `*ngIf="authService.isAuthenticated()"` on them.They disappear when no longer authenticated, but the page doesn't route to `login` until the token is refreshed, which could be a minute later, or I explicitly navigate somewhere else. – bordeltabernacle Oct 10 '18 at 13:26
  • @bordeltabernacle. What you are experiencing is expected behavior. Angular router doesn't know when you remove the keys. The reason why your nav disapper is angular assigns bindings for the expressions in template – Gangadhar Jannu Oct 10 '18 at 14:04
  • Thanks @GangadharJannu Yeah, I kind of figured it was expected behaviour, I'm interested to know how I'd go about getting the router to route to `login` in this circumstance. – bordeltabernacle Oct 10 '18 at 14:10
  • Ah yes @Ludevik this makes a lot of sense, I figured it might have to be some kind of continual check on something, just wan't really sure of what or where. This is my first Angular auth set up. I'll look into your suggestions here. Thanks a lot! – bordeltabernacle Oct 10 '18 at 14:33
  • @bordeltabernacle found out that you can observe changes on storage with browser api, edited answer – Ludevik Oct 10 '18 at 14:57