Enable authentication in KAPTL Angular app

Stas Demchuk

by Stas Demchuk on 11/18/2015

Angular

Angular doesn't have a built-in system for handling authentication and authorization, but it gives us everything we need to create our own. In this article we'll take a look at how to create your own auth system using Angular's built-in features.

Remember, you should always rely on the server to do the actual validation of user credentials and Angular (or any other frontend framework) should only be used to let the user know he's not authenticated and perform necessary actions to log him in.

AngularJS provides a component called interceptor, which intercepts HTTP requests for pre- or post-processing. We'll use it to check if the server responds with 401 Unathorized or 403 Forbidden and broadcast the auth:not-authenticated or auth:not-authorized event.

Offtop: I think it's a good idea to keep event names in a central place and I like to use Angular's constants for this purpose. It allows us to change event names anytime we need without having to change it in each file and each watcher that uses it. Example:

(function () {
  "use strict";

  angular
    .module("app")
    .constant("APP_СONSTANTS", {
      Events: {
        auth: {
          notAuthenticated: "auth:not-authenticated",
          notAuthorized: "auth:not-authorized"
        }
      }
    })
})();

To start intercepting failed requests, you need to create a factory and name it with something like AuthInterceptor or reuse the httpInterceptor factory we already created for you. This factory should return an object with a responseError method which we will use to check the response code and perform a proper action. Here is an example:

(function () {
  "use strict";

  angular
    .module("app.auth")
    .factory("AuthInterceptor", AuthInterceptor);

  AuthInterceptor.$inject = ["$rootScope", "$q", "APP_CONSTANTS"];

  function AuthInterceptor ($rootScope, $q, APP_CONSTANTS) {
    return {
      responseError: function (response) { 
        $rootScope.$broadcast({
          401: APP_CONSTANTS.Events.auth.notAuthenticated,
          403: APP_CONSTANTS.Events.auth.notAuthorized
        }[response.status], response); // 'response' is here in case you need to get any additional response data
        return $q.reject(response);
      }
    };
  });

})();

If you're reusing httpInterceptor factory, just copy and paste the $rootScope.broadcast part of the example right above the return $q.reject(response) statement.

This interceptor should also be added to $httpProvider.interceptors array. The best place to do this is your app's config block:

app.config.js
(function () {
  "use strict";

  angular
    .module("app.auth")
    .config(config);

  config.$inject = ["$locationProvider", "$httpProvider", "blockUIConfig"];

  function config ($locationProvider, $httpProvider, blockUiConfig) {
    $locationProvider.html5Mode(true);
  
    blockUiConfig.template = "<div class=\"block-ui-wrapper text-center\"><i class=\"fa fa-spinner fa-spin fa-4x\"></i></div>";
  
    $httpProvider.interceptors.push("httpInterceptor");
    // copy and paste the line above and change "httpInterceptor"
    // to the name of your factory if you created one
  });

})();

After that, it's up to you to decide what to do when these events occur. You can register a listener for auth events in run block of your app and redirect the user to the login page when one of these events occur. You can also, for example, create a login popup directive and make it listen for those events, showing the popup when they are fired.

What about roles and permissions for different users?

First of all, make sure you have user data stored somewhere in the application. $rootScope or some sort of a user service are perfect candidates for that. Also, take care about the logic that gets this user data from the service each time the user enters the app.

After that, you need to define roles for your app. Again, constants are very helpful here:

(function () {
  "use strict";

  angular
    .module("app")
    .constant("APP_СONSTANTS", {
      Events: {
        auth: {
          notAuthenticated: "auth:not-authenticated",
          notAuthorized: "auth:not-authorized"
        }
      },
      Roles: {
        admin: "admin",
        moderator: "moder",
        user: "user",
        guest: "guest"
      }
    })
})();

Now you can get the list of roles anywhere you need by just injecting APP_CONSTANTS into your component and using APP_CONSTANTS.Roles in your code. I'd recommend doing this in your controllers if you need certain actions to be available only for certain roles and then using the roles in your views like this:

<button ng-click="delete()" ng-if="isAuthorized(roles.admin)">Delete</button>

isAuthorized is a method that just returns true if the user has a role passed as a parameter. Here's an example:

// in authService.js
function isAuthorized (roles) {
  if (!Array.isArray(roles)) { // you can pass one or more roles to this method
    roles = [roles]; // convert single role to an array for convenience
  }
  
  return roles.indexOf($rootScope.currentUser.role) !== -1;
};

function isAuthenticated () {
  return !!$rootScope.currentUser.id; // just an example, you should use a more sophisticated approach
};
  
// in your controller
$scope.isAuthorized = AuthService.isAuthorized;

What if you need to restrict access to a particular route only for particular roles? You can define a data property in your route and assign it an object with authorizedRoles array.

Here's an example:

$stateProvider.state("admin", {
  url: "/admin",
  templateUrl: "admin/index.html",
  data: {
    authorizedRoles: USER_ROLES.admin
  }
});

NOTE: I'm using UIRouter here, but the data part should work the same for both ngRoute and UIRouter.

Now, all we need to do is check if the user has the role needed to access a route. For this, we'll create a listener for $stateChangeStart event (or $routeChangeStart in ngRoute) and see if $rootScope.currentUser.role is present in next.data.authorizedRoles:

(function () {
  "use strict";

  angular
    .module("app")
    .run(run);

  run.$inject = ["$rootScope", "APP_CONSTANTS", "AuthService"];

  function run($rootScope, APP_CONSTANTS, AuthService) {
    $rootScope.$on("$stateChangeStart", function (event, next) {
      var authorizedRoles = next.data.authorizedRoles;
      if (!AuthService.isAuthorized(authorizedRoles)) {
        event.preventDefault();
        if (AuthService.isAuthenticated()) {
          // user is not allowed
          $rootScope.$broadcast(APP_CONSTANTS.Events.auth.notAuthorized);
        } else {
          // user is not logged in
          $rootScope.$broadcast(APP_CONSTANTS.Events.auth.notAuthenticated);
        }
      }
    });
  }
})();

Again, it's your call on how to act when a particular event is fired (show a popup, redirect to another route, etc.).

Have any questions or suggestions? Let us know in the comments below!