Categories
Angular Full Stack Development Web

Passing the intended URL to CanLoad

A solution to use CanLoad in Authentication Guards to achieve the best of two worlds: performance and security

Angular Guards perform some activity before a request reaches a route. Two of the three possible interfaces they can implement are: CanActivate and CanLoad.

These two interfaces are very similar but with an important difference: while CanActivate applies to a generic Route, CanLoad applies to lazily loaded components.

If you have worked with Angular for a while, you’ll know that loading modules lazily improves the overall performance of your application and that we should use the CanLoad interface for it.

Here’s the rub: In the CanLoad interface, there is no injection of the RouterStateSnapshot class, while this is present in the CanActivate interface.

How to use the RouterSateSnapshot class in CanActivate

The RouterStateSnapshot class contains the URL users intended to visit. The typical flow of a Guard therefore looks something like the following:

  1. Check if the user logged in, then
  2. If they are authenticated, allow them to proceed to the intended URL, or
  3. If they aren’t authenticated, redirect them to the login Route

With CanActivate, this flow looks like the following:

canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean | UrlTree> | boolean {
    return this.authService.isAuthenticated$.pipe(
      tap((loggedIn) => {
        if (!loggedIn) {
          this.authService.login(state.url);
        }
      })
    );
  }

In the above example, I’m using AuthO (Auth Zero) as an Authentication service. This code uses RxJs to check (tap) the user’s authentication status: if the user is not authenticated, the flow is redirected to the login function, passing as argument the state.url which contains the URL the user originally requested.

For lazily loaded components, while we want to achieve the same, there’s a problem: the CanLoad interface does not provide the RouterStateSnapshot object as an argument. What to do then?

Router States to the rescue

The solution is describes, albeit at high-level, in this issue. It consists of the following steps:

  • Subscribe to the router.events Observable in the main app component.
  • Extract the intended URL from the RouterEvent object
  • Store the intended URL is an app-wide service
  • Access the intended URL from the Guard.

Below it’s an implementation which shows how to do this.

app.component.ts

import { Component, OnInit } from '@angular/core';
import { NavigationStart, Router, RouterEvent } from '@angular/router';
import { filter } from 'rxjs/operators';
import { AuthService } from './auth/services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  constructor(private router: Router, private authService: AuthService) {}

  ngOnInit(): void {
    this.router.events
      .pipe(
        filter((e: RouterEvent): e is RouterEvent => e instanceof RouterEvent)
      )
      .subscribe((e: RouterEvent) => {
        this.authService.attemptedUrl = e.url;
      });
  }
}

Here, again the solution uses RxJs to select only events of type RouterEvent with the filter operator. Once such event is available, the code sets the attemptedUrl value to the value of the URL originally requested by the user. This service is globally available.

auth.guard.ts (canLoad function)

canLoad(
    route: Route,
    segments: UrlSegment[]
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated$.pipe(
      tap((loggedIn) => {
        if (!loggedIn) {
          this.authService.login(this.authService.attemptedUrl);
        }
      })
    );
  }

Now, CanLoad can pass to the Authentication login service the URL that the user originally attempted to visit but which requires authentication.

Full auth.guard.ts

import { Injectable } from '@angular/core';
import {
  CanLoad,
  Route,
  UrlSegment,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  CanActivate,
} from '@angular/router';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AuthService } from './services/auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanLoad, CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean | UrlTree> | boolean {
    return this.authService.isAuthenticated$.pipe(
      tap((loggedIn) => {
        if (!loggedIn) {
          this.authService.login(state.url);
        }
      })
    );
  }

  canLoad(
    route: Route,
    segments: UrlSegment[]
  ): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated$.pipe(
      tap((loggedIn) => {
        if (!loggedIn) {
          this.authService.login(this.authService.attemptedUrl);
        }
      })
    );
  }
}

Of course we need to define the guards in our routing logic. Below is how we can do this.

const routes: Routes = [
  {
    path: 'feedback',
    canLoad: [AuthGuard],
    loadChildren: () =>
      import('./smiley/smiley.module').then((m) => m.SmileyModule),
  },
  {
    path: 'profile',
    canActivate: [AuthGuard],
    component: ProfileComponent,
  },
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: '**',
    component: NotFoundComponent,
  },
];

Here, I’ve activated the canLoad guard for the lazily loaded module and the canActivate guard for the eagerly loaded one.

The best of two worlds

With this solution, we can get the best the two worlds: we can implement an Authentication Guard which works for both eagerly (with CanActivate) and lazily (with CanLoad) loaded modules, thus allowing for performance and security at the same time.

If you know of a better way of achieving this, I’ll be happy if you got in touch.

By Marco Tedone

I lead the API, Integration and Microservices (AIM) Practice for a world’s leading international bank. My team is in charge of defining and maintaining standards, best practices and accelerators for everything that relates to APIs, Integration and Microservices. We also act as an internal consultancy helping other teams with AIM adoption, architecture reviews, pair programming and technology choices. We are chief problem solvers. I also specialise in building high performing technology teams. I have experience and a passion for Lean Enterprise transformations with the goal of helping organisations to deliver business value faster by looking at product delivery as business value delivery and as a system flow, where BDD, Agile, DevOps, Testing Automation, Portfolio and Budget Management, Regulatory and Compliance, Security and NFRs are all parts of a single journey. My favourite execution tool for Lean Enterprise transformations is the Improvement/Coaching Kata, by Mike Rother. I’m well versed in Followership, Leadership and Coaching.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.