Adam Albrecht

ruby & javascript developer in columbus, ohio.

Authorization with Angular.js and UI-Router

While working on an angular.js application recently, I found myself needing some form of authorization logic (not to be confused with authentication / login). I needed to restrict content in my app based on a user’s role as well as some other factors. At first, I created a single AuthService service that dealt with login, authorization, and session management. But this felt messy and violated the Single Responsibility Principle, so I decided to make something cleaner. My goal was for the API to look something like this:

(Warning: lots of coffeescript ahead!)

LoginService.login(email, password).then((u) ->
  Session.setCurrentUser(u)
)
# ... Elsewhere ....
user = Session.getCurrentUser()
authorizer = new Authorizer(user)
authorizer.canAccess(APP_PERMISSIONS.viewAdminSettings) # returns a boolean

By doing it this way, I was fairly sure I could split my formerly monolithic AuthService into 3 separate services that had no dependencies on one another. I won’t go too detailed into the login and session services because they are fairly straight forward. LoginService has one method that simply makes an HTTP request with a username and password and, if successful, returns the user object. Session is a singleton service that, given a user, can create or destroy the current session. But my solution to Authorization was fairly interesting, so I thought I’d share.

In the ruby world, I’ve used both CanCan and Pundit and so I drew a lot of inspiration from them. But at the same time, I was mindful that client-side authorization is never as complex as server side. You should never need to use client side authorization to filter data (that should be done server side), but only to show/hide pages and pieces of content.

So first, I created a constant containing a set of permissions:

app.constant('APP_PERMISSIONS', {
  viewAdminSettings: "viewAdminSettings"
  editAdminSettings: "editAdminSettings"
  viewLibrary: "viewLibrary"
  editLibrary: "editLibrary"
  viewBusinessAssociates: "viewBusinessAssociates"
  editBusinessAssociates: "editBusinessAssociates"
  # ...
})

You’ll notice that the keys and values are the same. I only made this an object rather than an array so I could refer to them with a dot syntax and wouldn’t have magic strings floating around.

Next, as a good programmer does, I added a test for the Authorizer service I was about to make.

describe "Authorizer", ->
  Authorizer = null
  APP_PERMISSIONS = null

  beforeEach(angular.mock.module('privacypro.auth'))
  beforeEach(inject((_Authorizer_, _APP_PERMISSIONS_) ->
    Authorizer = _Authorizer_
    APP_PERMISSIONS = _APP_PERMISSIONS_
    return # always add return statements to injection blocks in Coffeescript.
  ))

  describe "canAccess()", ->
    authorizer = null
    describe "An admin user", ->
      user = { role: "admin" }
      beforeEach ->
        authorizer = new Authorizer(user)
      it "can view the admin settings", ->
        expect(authorizer.canAccess(APP_PERMISSIONS.viewAdminSettings)).toBeTruthy()

    describe "A normal user", ->
      user = { role: "normal" }
      beforeEach ->
        authorizer = new Authorizer(user)
      it "cannot view the admin settings", ->
        expect(authorizer.canAccess(APP_PERMISSIONS.viewAdminSettings)).toBeFalsy()
      it "can view the library OR view the admin settings", ->
        expect(authorizer.canAccess([APP_PERMISSIONS.viewLibrary, APP_PERMISSIONS.viewAdminSettings])).toBeTruthy()
      it "throws an error if passed a bad permission", ->
        expect(-> authorizer.canAccess("foobar")).toThrow()

My specs included a number of examples and more complex scenarios, but you get the point. So next, I started work on my Authorizer service. This class can be as simple or complex as your authorization requirements demand. In reality, my code is quite a bit more complex than below, but you’ll understand the basics from this example:

app.service("Authorizer", (APP_PERMISSIONS, USER_ROLES) ->
  return (user) ->
    {
      canAccess: (permissions) ->
        permissions = [permissions] unless angular.isArray(permissions)
        for permission in permissions
          if !APP_PERMISSIONS[permission]?
            throw "Bad permission value"
          if user && user.role
            switch permission
              when APP_PERMISSIONS.viewAdminSettings, APP_PERMISSIONS.editAdminSettings
                return (user.role == USER_ROLES.admin)
              when APP_PERMISSIONS.editLibrary
                return (user.role == USER_ROLES.admin || user.role == USER_ROLES.normal)
              # etc...
          else
            return false
        false
    }
)

Now I’m ready to add permissions to some of my routes. I always use UI-Router, but something similar can be done with ngRoute.

$stateProvider
  .state("library", {
    url: "/library",
    templateUrl: "..."
    data: {
      permissions: [APP_PERMISSIONS.viewLibrary]
    }
  })
  .state("admin", {
    url: "/admin",
    templateUrl: "...",
    data: {
      permissions: [APP_PERMISSIONS.editAdminSettings]
    }
  })

But how do I use this permission data and prevent a state from loading? To do this, you simply have to subscribe to ui-router’s $stateChangeStart event and prevent it from propegating when necessary. Put this code inside of an angular run block.

app.run(($rootScope, Session, Authorizer, AUTH_EVENTS) ->
  $rootScope.$on "$stateChangeStart", (event, next) ->
    permissions = if next && next.data then next.data.permissions else null
    user = Session.getCurrentUser()
    authenticator = new Authorizer(user)
    if permissions? && !authenticator.canAccess(permissions)
      event.preventDefault()
      if !user
        $rootScope.$broadcast AUTH_EVENTS.notAuthenticated
      else
        $rootScope.$broadcast AUTH_EVENTS.notAuthorized

Notes:

Finally, I needed to restrict individual pieces of content on the page. One easy way to do this would be to create a helper method on the scope and use it in tandem with ngIf like so:

<a href='' ng-if="canAccess('editAdminSettings')">Edit Admin Settings</a>

But this is a bit messy and I hate to pollute the scope unless absolutely necessary. So instead, I created a custom directive that will work similarly:

<a href='' ng-if-permission="editAdminSettings">Edit Admin Settings</a>

I was hoping to simply extend the ngIf directive with a specific set of logic, but I couldn’t find a way to do this (if you know how to do this, please let me know!). So instead, I simply copied the ngIf source code, changed the name to ngIfPermission, and made a few minor enhancements to the link function:

# the beginning is the same besides the name
link: ($scope, $element, $attr, ctrl, $transclude) ->
  block = undefined
  childScope = undefined
  previousElements = undefined

  # There is no logic in the watch, so we can use $attr.$observe instead
  $attr.$observe "ngIfPermission", (value) ->
    # Check if we can access the permission(s)
    permissions = value.split(",")
    user = Session.getCurrentUser()
    authenticator = new Authorizer(user)
    if authenticator.canAccess(permissions)
      unless childScope
        $transclude (clone, newScope) ->
          childScope = newScope
          # change the contents of the placeholder comment
          clone[clone.length++] = document.createComment(" end ngIfPermission: " + $attr.ngIfPermission + " ")

# the rest is the same
)

So now we have a way to restrict access to pages as well as individual pieces of content. Remember, client side authorization is no substitute for proper server-side authorization. Anybody who is half-way decent with the Chrome dev tools can figure out how to manipulate your API requests.

I hope this helps you in your journey to create a complex and full-featured angular app. If you have any questions or have a suggestion on how to improve the code, let me know on twitter, where I’m @adam_albrecht. Thanks!