Running the extra mile on access management

Apparently, we have everything that it takes to move on with our application. However, as our application grows and more areas need to be protected, we will find ourselves facing the burden of toggling visibility on more and more links and having to enable access and activation of components one by one by implementing the @CanActivate decorator on each.

Obviously, this scales up just badly, so it would be great to rely on a one-size-fits-all solution instead. Unfortunately, at the time of writing, the Angular 2 framework still does not provide a feasible solution to tackle with this concern. On the other hand, and according to modern UX, most of the time the expected behavior is to provide the user with as many browsing alternatives as possible and redirect the user to the login page only where required.

In this last section, we will introduce a generic workaround for this, based on the following criterion: protecting areas of content as a whole by wrapping them inside child routes that will redirect the user to the login page whenever unauthorized access is detected, where parameters such as the location of the login path are fully configurable from our solution.

Building our own secure RouterOutlet directive

Our workaround is based on developing our very own RouterOutlet directive by extending the RouterOutlet class baked in Angular 2, which will be slightly rewritten to override the base directive's default behavior when it comes to proceeding to activate (or not) the requested component. All of this based on the current user login status.

To do so, we will create a new directive in our shared context, which will be used across the application. So we need to expose it in the shared facade as well.

app/shared/directives/router-outlet.directive.ts

import { 
  Directive, 
  ViewContainerRef, 
  DynamicComponentLoader, 
  Attribute, 
  Input } from '@angular/core';
import { 
  Router, 
  RouterOutlet, 
  ComponentInstruction } from '@angular/router-deprecated';
import { AuthenticationService } from '../shared';

@Directive({
  selector: 'pomodoro-router-outlet'
})
export default class RouterOutletDirective extends RouterOutlet {
  parentRouter: Router;
  @Input() protectedPath: string;
  @Input() loginUrl: string;

  constructor(
    _viewContainerRef: ViewContainerRef,
    _loader: DynamicComponentLoader,
    _parentRouter: Router,
    @Attribute('name') nameAttr: string) {

    super(_viewContainerRef, _loader, _parentRouter, nameAttr);
    this.parentRouter = _parentRouter;
  }

  activate(nextInstruction: ComponentInstruction): Promise<any> {
    let requiresAuthentication = 
        this.protectedPath === nextInstruction.urlPath;

    if (requiresAuthentication && 
        !AuthenticationService.isAuthorized()) {
      this.parentRouter.navigateByUrl(this.loginUrl);
    }

    return super.activate(nextInstruction);
  }
}

Here, we are creating a new directive that extends from the RouterOutlet directive we have been using all this time in our application. This directive needs to be made available for use from the other feature contexts of our application:

app/shared/shared.ts

import Queueable from './interfaces/queueable';
import Task from './interfaces/task';

import FormattedTimePipe from './pipes/formatted-time.pipe';
import QueuedOnlyPipe from './pipes/queued-only.pipe';

import AuthenticationService from './services/authentication.service';
import SettingsService from './services/settings.service';
import TaskService from './services/task.service';

import RouterOutletDirective from './directives/router-outlet.directive';

const SHARED_PIPES: any[] = [
  FormattedTimePipe,
  QueuedOnlyPipe
];

const SHARED_PROVIDERS: any[] = [
  AuthenticationService,
  SettingsService,
  TaskService
];

const SHARED_DIRECTIVES: any[] = [
  RouterOutletDirective
];

export {
  Queueable,
  Task,

  FormattedTimePipe,
  QueuedOnlyPipe,
  SHARED_PIPES,

  AuthenticationService,
  SettingsService,
  TaskService,
  SHARED_PROVIDERS,

  RouterOutletDirective,
  SHARED_DIRECTIVES
};

Back to the directive file, we can see it inherits the same constructor of the inherited RouterOutlet constructor. Thus, we will import the same required tokens so that we can properly declare the constructor dependencies and call the superclass constructor with super(). The code is as follows:

app/shared/directives/router-outlet.directive.ts

constructor(
  _elementRef: ElementRef,
  _loader: DynamicComponentLoader,
  _parentRouter: Router,
  @Attribute('name') nameAttr: string) {

  super(_elementRef, _loader, _parentRouter, nameAttr);
  this.parentRouter = _parentRouter;
}

In the constructor body, we do not only inject the dependencies, we also assign the Router instance in a class member for future use. The core of the solution relies on overriding the implementation of the native activate() method, where we basically introduce an authentication check (that is why we also import the AuthenticationService at the top of the script) to see if the recently required component lives in the domain of the protected path. In that case, we will redirect the user to the login page location should the authentication token is not available thanks to the static method isAuthorized() exposed by the AuthenticationService. In any event, the method will finally return the result of the superclass' activate method, represented by a Promise object. The code is as follows:

activate(nextInstruction: ComponentInstruction): Promise<any> {
  let requiresAuthentication =this.protectedPath === nextInstruction.urlPath;

  if (requiresAuthentication &&
      !AuthenticationService.isAuthorized()) {
    this.parentRouter.navigateByUrl(this.loginUrl);
  }

  return super.activate(nextInstruction);
}

Where do we fetch these protectedPath and loginUrl parameters? As you saw already at the beginning of this section, we are exposing two input parameters on this directive, which will make its instances look like this:

<pomodoro-router-outlet protectedPath="edit" loginUrl="/login">
</pomodoro-router-outlet>

So, open up the top router component file and replace the current RouterOutlet directive instance in the template with a few lines of code, right after importing the SHARED_DIRECTIVES symbol in the directives property of the component decorator:

app/app.component.ts

import { Component } from 'angular2/core';
import { 
  SHARED_PROVIDERS,
  AuthenticationService,
  SHARED_DIRECTIVES } from './shared/shared';
...

@Component({
  selector: 'pomodoro-app',
  directives: [ROUTER_DIRECTIVES, SHARED_DIRECTIVES],
  ...
})

app/app.component.html

<nav class="navbar navbar-default navbar-static-top">
  <div class="container">
    <div class="navbar-header">
      <strong class="navbar-brand">My Pomodoro App</strong>
    </div>
    <ul class="nav navbar-nav navbar-right">
      <li><a [routerLink]="['TasksComponent']">Tasks</a></li>
      <li><a [routerLink]="['TimerComponent']">Timer</a></li>
      <li *ngIf="userIsLoggedIn">
        <a [routerLink]="['TaskEditorComponent']">
          Publish Task
        </a>
      </li>
      <li *ngIf="!userIsLoggedIn">
        <a [routerLink]="['LoginComponent']">Login</a>
      </li>
      <li *ngIf="userIsLoggedIn">
        <a href="#" (click)="logout($event)">Logout</a>
      </li>
    </ul>
  </div>
</nav>
<pomodoro-router-outlet
  protectedPath="tasks/editor"
  loginUrl="login">
</pomodoro-router-outlet>

Last but not least, in order to try out this solution, we still need to do two things: remove (or comment out) the @CanActivate() decorator in the task editor component and then tweak the routerCanDeactivate() router hook to look like this:

app/tasks/task-editor.component.ts

routerCanDeactivate(
  next: ComponentInstruction,
  prev: ComponentInstruction) {
  return !AuthenticationService.isAuthorized() ||
    this.changesSaved ||
    confirm('Are you sure you want to leave?');
}

This way, we can smoothly deactivate the component if the user is not logged in. Why should we do this? Basically, this is the flow the user will follow:

  • The user asks for a protected component
  • The router directive checks if the user is authenticated
  • If logged in already, the directive does nothing and the component is activated
  • If not, although the component will be activated, a redirection will be performed at the same time and the user will land safely on the comfort of the login page

In order to properly try out this solution, remove the conditional from the route setting. We want to actually display the link but redirect the user if not logged in. The code is as follows:

app/app.component.html

...
<ul class="nav navbar-nav navbar-right">
  <li><a [routerLink]="['TasksComponent']">Tasks</a></li>
  <li><a [routerLink]="['TimerComponent']">Timer</a></li>
  <li>
    <a [routerLink]="['TaskEditorComponent']">
      Publish Task
    </a>
  </li>
  <li *ngIf="!userIsLoggedIn">
    <a [routerLink]="['LoginComponent']">Login</a>
  </li>
  <li *ngIf="userIsLoggedIn">
    <a href="#" (click)="logout($event)">Logout</a>
  </li>
</ul>
...

Note

There is an important caveat in this solution: the protected component will be actually rendered on screen before bouncing the user to the login page (whenever login is required). This is definitely not good, since there is a chance that the protected contents will flicker on screen if the secure component instantiation takes longer than expected or either the canDeactivate() method poses some conditions in order to move on (hence the change we introduced.

Otherwise, the transition will be so fast that the chances are that the end user will not even notice these components have been instantiated. In any event, use this with care and watch out for the CanDeactivate function in all your implementations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset