Categories:
layout development Angular 2 code & tech
icon-search
sylwia-bartyzel-442-unsplash

Angular 2 Route Guards used in authorization: real life example

Piotr Wojnarowski 20.12.2016

During the later stages of Angular 2 development, the Beta and RC phases, one of the most dynamically changing module was the router. Since it seems that one very interesting feature – the Route Guards – has reached its final version, we’d like to write about it. In this post you’ll see the changes it went through and a real life example of Angular 2 Route Guards used in authorization.

Disclaimer: This article was written based on Angular 2.2 and Router 3.2. Because of that the examples shown here should work with later versions but will definitely fail with older router versions.

What are the Angular 2 Route Guards and what to do with them?

As the name suggests, you can configure guards on routes in your application to control how the user navigates between them. Those are functions called when router tries to activate or deactivate certain routes. We will focus on the CanActivate guard, but we will discuss the other guards later in the post. You can read about all of them in the  Angular Guard documentation.

The general rule is that the guards are functions that are called in certain points of the router lifecycle. They return a boolean or an asynchronous response: Promise or Observable. In the case of CanActivate, the guard function is called when user tries to navigate into the route. The component behind it will only be activated after the function returns true or the Observable / Promise will eventually return true. When the function hangs or returns false, the router will not display the route content.

As you can imagine, it is basically a bad design when the application just hangs with an empty part of the page when user does not have access to something. Usually it’s best to redirect the user to some other route. This means that we have a side effect in our function. Furthermore the function is called “CanActivate” so we would expect that it will just return true or false. Unfortunately Angular doesn’t give us access to handling CanActivate rejection so this is the only way we have.

Defining Route Guards

There are two ways of defining a guard. A simpler one is just through creating a function, like below:

Route Guard as a function

// file app.routing.ts

const appRoutes: Routes = [
    {path: "", redirectTo: "board", pathMatch: "full"},
    {path: "board", component: BoardComponent, canActivate: ["boardGuard"]}
];

export const routingProviders = [
    {provide: "boardGuard", useValue: boardGuard}
];

export function boardGuard(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return true;
}
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
// file app.module.ts

import {routing, routingProviders} from "./app.routing";

@NgModule({
   import: [routing],
   providers: [routingProviders]
})
export class AppComponent {}

Here you can see part of a routing file and a part of a module file. First, we define a route, “board”, that will display user’s dashboard. To that goal, we define the guard, by setting the canActivate attribute on the route: [“boardGuard”]. As you can see this attribute is an array, so you can define multiple guards, eg. to better split the responsibility. Router will iterate over all of them, unless one will deny the access.
In the following lines we define an Angular provider for the function, so that Angular dependency injection could recognize it. This array with single provider will be later used in app.module file in NgModule definition.
The function just returns true, nothing fancy so far, so user will always be able to activate the route.

You can also use the dependency injection in the guard function, like this:

export const tpRoutingProviders = [
    {provide: "authenticatedGuard", useFactory: (authService: AuthService) => {
            return () => authService.isUserAuthenticated();
        },
        deps: [AuthService]
    }
];

By using useFactory provider, we can define some dependencies of our function, allowing us to inject the AuthService. Then we just define a function that calls isUserAuthenticated() from that service. This function could return a boolean eg. by checking cookies or maybe an observable, getting data from a backend. Only after it resolves, the route will be activated.

Route Guard as a class

The second option is to define a class that implements the Can Activate interface. Let’s follow our example:

// file app.routing.ts

const appRoutes: Routes = [
   {path: "worksheet", component: WorksheetComponent, canActivate: [WorksheetAccessGuard]}      
];

// file ActivationGuards.ts

@Injectable()
export class WorksheetAccessGuard implements CanActivate {
   private static USER_PARAM = "userId";

   constructor(private router: Router, private userService: CurrentUserService) {}

   public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
      const currentUser = this.userService.currentUser;
      const paramUser = route.params[WorksheetAccessGuard.USER_PARAM];
      if (paramUser && paramUser !== currentUser.id && !currentUser.admin) {
          this.router.navigate(["worksheet"]);
          return false;
      }
      return true;
   }
}

// file app.module.ts

@NgModule({
   providers: [WorksheetAccessGuard]
})
export class AppComponent {}

As we can see, the WorksheetAccessGuard class has method canActivate, which is used by the router in the same manner the function from previous example – it is called when the “worksheet” route is being activated and decides if it can happen. But this time we have also the constructor in which we can inject some services.

How does it work?

The worksheet component by default displays the work data of the current user. When accessed with user param set it can display data for another user. However this feature is only available for admin. As we can see, the canActivate function gets the current user from the injected CurrentUserService, then user param from ActivatedRouteSnapshot. If there is no param or the param user is the same as the current user or if the user is admin, then we let the component be activated. Otherwise we use the injected router to navigate into the user’s own worksheet, navigating without params.

We are using the method’s first argument of type ActivatedRouteSnapshot to get the this route params. This argument represents the state of the route that the users wants to activate. We can also get the url from it or the whole tree of routes in the current router.

Old router’s annotations

In the old router, the guards where setup by annotations. Just like RouteConfig, they were defined in decentralized way, in every navigable component, not in the NgModule:

@Component()
@RouteConfig([
   {path: "/userAccount", as: "UserAccount", component: UserAccountComponent},
   {path: "/accounts", as: "Accounts", component: OtherAccountComponent}
])
@CanActivate(() => AppConfigurationService.instance.waitForConfiguration())
export class WorkflowComponent {}

There was one big issue with that – only static functions and no dependency injection. This sometimes pushed developers for very creative solutions. In our case, we created a static field “instance” in the  AppConfigurationService. This field is set in the constructor of this service. We also inject the service into the main component of our application. This way it will be constructed before it will be needed for guarding. As you can see there is a lot of complications that we got rid off in the new router.

More complex example

Now let’s dive into the full example of how to guard activation on multiple routes.

Routing

const appRoutes: Routes = [
    {path: "login", component: LoginComponent},
    {path: "", redirectTo: "auth", pathMatch: "full"},
    {path: "expired", component: TrialExpiredComponent, canActivate: [AuthenticatedGuard]},
    {path: "auth/invite", component: InviteComponent, canActivate: [AuthenticatedGuard, AdminGuard]},
    {path: "company", component: CompanyPickerComponent, canActivate: [AuthenticatedGuard, CompanyPickerGuard]},
    {path: "auth", component: AuthComponent, canActivate: [AuthenticatedGuard, AuthComponentGuard],
        children: [
            {path: "", redirectTo: "ts", pathMatch: "full"},
            {path: "ts", component: TimesheetComponent, canActivate: [TimesheetAccessGuard]},
            {path: "projects", component: ProjectsComponent, canActivate: [AdminGuard]}
        ]
    }
];

There are multiple routes in our application. We can divide them into four groups:

  1. Login with no activation guard – everybody can access it
  2. TrialExpired that just requires user to be authenticated
  3. Invite and CompanyPicker components – both of them requires user to be authenticated, which is ensured by AuthenticatedGuard. Then they have some other guard that checks various requirements for every one of them.
  4. AuthComponent and its children. Whenever user opens one of them, first the router checks if AuthComponent can be activated and then it runs guards of the child component.

Warning

We haven’t found documentation for it but it seems that activation guards are called in the given order but don’t have to be resolved in that order. If the first guard returns false the second one won’t be called. But when the first one returns a Promise that will resolve to false in 5 seconds, then the second one will be called and processed and then router will wait for all for the guards to resolve. The same goes for guards defined in parent and child components – they are called in order but not necessarily resolved in that order.

Authenticated Guard

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

    public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (!this.authService.isAuthenticated()) {
            this.router.navigate(["/login"]);
            return false;
        } else {
            return true;
        }
    }
}

This is the most basic one – it just checks if the user is authenticated and then lets the component to activate or redirects to login.

AdminGuard

@Injectable()
export class AdminGuard implements CanActivate {
    constructor(private companyService: CurrentCompanyUserViewService) {}

    public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.companyService.companyUser.admin;
    }
}

Admin guard is also very easy – it just checks whether the user is an admin or not.

AuthComponentGuard

@Injectable()
export class AuthComponentGuard implements CanActivate {
    constructor(private router: Router, private companyUserView: CurrentCompanyUserViewService) {}

    public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (!this.companyUserView.companyUser) {
            this.router.navigate(["/company"]);
            return false;
        } else if (this.companyUserView.isTrialExpired) {
            this.router.navigate(["/expired"]);
            return false;
        } else {
            return true;
        }
    }
}

AuthComponent surrounds the main content of the application. User can be assigned to one or more companies. User can switch between them while browsing the application. To access the content of Auth Component, user needs to choose a company first. If not, the companyUserView.companyUser field will be undefined and router will navigate to “company” route.
Also user has to have an active subscription. If the trial is expired user will be redirected to “expired” route.
Remember that this route is used together with AuthenticatedGuard so inside AuthComponent we know that the user is authenticated.

CompanyPickerGuard

@Injectable()
export class CompanyPickerGuard implements CanActivate {

  constructor(private router: Router, private companyViewService: CurrentCompanyUserViewService) {}

   public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
      if (this.companyViewService.isTrialExpired) {
          this.router.navigate(["expired"]);
          return false; 
      } else if (!this.companyViewService.companyUser) {
          return true;
      } else {
          this.router.navigate(["auth", "ts", {userId: this.companyViewService.companyUser.id}]);
          return false;
      }
   }
}

This component forces user to choose a company. It cannot be accessed if the company is already set – there is another component for that. In that case user is redirected to the timesheet. User also cannot have an expired trial.

TimesheetAccessGuard

@Injectable()
export class TimesheetAccessGuard implements CanActivate {
   private static USER_PARAM = "userId";

   constructor(private router: Router, private companyService: CurrentCompanyUserViewService) {}

   public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
      const currentUser = this.userService.currentUser;
      const paramUser = route.params[TimesheetAccessGuard.USER_PARAM];
      if (paramUser && paramUser !== currentUser.id && !currentUser.admin) {
         this.router.navigate(["worksheet"]);
         return false;
      }
      return true;
   }
}

This is basically the same guard as shown in “Route Guard as a class” section – checking “userId” param from the route and comparing it to the current user privileges.

What’s out there besides CanActivate?

There are other Route Guards defined in Anngular. Lets describe them shortly:

  • CanActivateChildren – it works just like CanActivate but it guards all the child routes of the one it’s defined for. Often used for component-less routes.
  • CanDeactivate – can user leave the current route? Useful for reminding user to save the unsaved work.
  • Resolve – it allows to delay the activation of a route until we get some data.
  • CanLoad – guards access to the asynchronous module.

Summary

Overall, Route Guards in Angular are really a powerful and useful tool when you want to control your users’ navigation. It is important to know, how it works and how it can be combined with other Angular mechanisms such as dependency injection. In addition it is interesting to see how this concept has changed between Angular Router versions. Furthermore, we can see that the topic does not end on CanActivate – you can expect more posts on our blog when we tackle the topic of loading asynchronous modules or component-less routes.

comments: 0


Notice: Theme without comments.php is deprecated since version 3.0.0 with no alternative available. Please include a comments.php template in your theme. in /var/www/html/www_en/wp-includes/functions.php on line 3879

One response to “Angular 2 Route Guards used in authorization: real life example”

  1. I think your analysis about guards which are promises is almost right. I think guards are processed by something very similar to Promise.all, so that, as long as guards are true or resolved, the others continue running, but as soon as one is false or rejected then the wrapping Promise is immediately stopped. Thus this implementation closely matches what we expect from a short-circuit-able AND of all the guards.

    I think many developers don’t get this right and use resolve(false) when they really mean reject(“cannot activate route”).

Leave a Reply

Your email address will not be published. Required fields are marked *