Captain Codeman Captain Codeman

Angular2 Route Security

Restricting access based on auth state or roles

Contents

Introduction

Once you move beyond the quick-starts and examples and start building a real app with Angular2 you soon find you need to handle things that the examples often leave out or pass over.

Securing routes with the new component router is one of these and it can be difficult to figure out. Here’s the approach I’m using which seems to be working well for me and has been reusable across multiple projects.

First of all, this is for declarative security only. That is, where the rules can be statically defined on the route. It should be adaptable to work with whatever authentication system your app uses but is particularly suited to using JSON Web Tokens which can contain the roles that your user has been granted.

If you need per-object permission checks I favour doing these within the component and / or service that is responsible for accessing and handling them (some people prefer to use CanActivate). That kind of dynamic check is out of scope of this article.

The natural place to declare the permissions for a route is on the route config which provides a data property. While we could add a flag to indicate if a route should be public or not, it seems a little superfluous as routes are public by default and so the flag would only ever add new information if set to false. As we’re also going to define roles that the user requires we can make that do double-duty and use it’s presence to indicate that we need authentication but it can be empty if we don’t care about any specific roles which is the same as saying we only care that the user is authenticated.

Here’s an example of three routes, the first one is open to all, the second one only available to authenticated users (but no roles specified) and the last one requiring the user has the role ‘admin’.

@RouteConfig([
{ path:'/open2all', component:OpenComponent, name:'Open' }
{ path:'/needauth', component:AuthComponent, name:'Auth', data:{ roles:[] }}
{ path:'/needrole', component:RoleComponent, name:'Role', data:{ roles:['admin'] }}
])

So that’s the permissions for our routes defined - the easy part! We want to be able to check these permissions as part of the routing process and the natural place to do this is the <router-outlet> so we are going to create out own <secure-outlet> version that will override the activate method to do the permission checks. But what if permission fails? What do we want to happen? There are two scenarios:

A route that requires authentication when the user has not been authenticated.

The natural thing to do is to redirect to the sign-in route to allow the user to sign-in and then return back to our protected route.

A route that requires certain roles than an authenticated user does not have.

The user doesn’t have access and should be redirected to a permission denied route.

So we need our secure router to be configureable with two routes - one for sign-in and one for unauthorized access. We can pass these as properties in the view of our routing component which will look like this:

<secure-outlet signin="/Signin" unauthorized="/Denied"></secure-outlet>

All the magic will happen in the <secure-outlet> component but we don’t want to couple it directly to our app-specific authentication service as this can make it harder to re-use. Instead we’ll define an interface that our auth service needs to supply to allow it to be used by this component.

export abstract class IAuthService {
  // is the current user authenticated?
  abstract isAuthenticated():boolean;

  // does the current user have one of these roles?
  abstract hasRole(roles: string[]):boolean;
}

This should be straightforward to implement into any AuthService our app uses but one subtle thing to ensure is that when we ask for an instance of the IAuthService we’re actually give the instance of the AuthService used by the app (whatever it is called). We do this by setting the provider to use in our app bootstrap using the useExisting option which prevents us getting a separate instance when the AuthService itself will also likely be configured:

bootstrap(App, [
  AUTH_HTTP_PROVIDERS,
  // more providers ...
  provide(IAuthService, { useExisting: AuthService }),
  AuthService,
]);

Finally, we’re ready to create our <secure-outlet> component which will provide the permissions checks and handle the route redirects if they fail:

import {Directive, Attribute, ElementRef, DynamicComponentLoader} from 'angular2/core';
import {Router, RouteData, RouterOutlet, ComponentInstruction} from 'angular2/router';

@Directive({selector: 'secure-outlet'})
export class SecureRouterOutlet extends RouterOutlet {
  signin:string;
  unauthorized:string;

  private parentRouter: Router;
  private authService: IAuthService;

  constructor(_elementRef: ElementRef, _loader: DynamicComponentLoader,
              _parentRouter: Router, @Attribute('name') nameAttr: string,
              authService:IAuthService,
              @Attribute('signin') signinAttr: string,
              @Attribute('unauthorized') unauthorizedAttr: string) {
    super(_elementRef, _loader, _parentRouter, nameAttr);
    this.parentRouter = _parentRouter;
    this.authService = authService;
    this.signin = signinAttr;
    this.unauthorized = unauthorizedAttr;
  }

  activate(nextInstruction: ComponentInstruction): Promise<any> {
    var roles = <string[]>nextInstruction.routeData.data['roles'];

    // no roles defined means route has no restrictions so activate
    if (roles == null) {
      return super.activate(nextInstruction);
    }

    // if user isn't authenticated then redirect to sign-in route
    // pass the URL to this route for redirecting back after auth
    // TODO: include querystring parameters too?
    if (!this.authService.isAuthenticated()) {
      var ins = this.parentRouter.generate([this.signin,{url:location.pathname}]);
      return super.activate(ins.component);
    }

    // if no specific roles are required *or* the user has one of the
    // roles required then the route can be activated
    if (roles.length == 0 || this.authService.hasRole(roles)) {
      return super.activate(nextInstruction);
    }

    // user has insufficient role permissions so redirect to denied
    var ins = this.parentRouter.generate([this.unauthorized]);
    return super.activate(ins.component);
  }

  reuse(nextInstruction: ComponentInstruction): Promise<any> {
    return super.reuse(nextInstruction);
  }
}

Now we just need to make sure our applications AuthService (whatever it is called) provides the necessary pieces:

import { IAuthService } from '../components/secure-outlet/secure-outlet';

@Injectable()
export class AuthService extends IAuthService {
  user:User;

  isAuthenticated():boolean {
    return this.user !== null;
  }

  hasRole(string[] roles):boolean {
    return this.isAuthenticate() && [check intersection of user roles]
  }

  // other auth functionality, sign-in, token handling etc
}

And there we have it. Permissions declared on our routes and handled for us.

Just be sure to have the sign-in route check for the url parameter it is sent and redirect back to it after authentication has been completed.