Thursday, November 28, 2013

Deal with authentication in AngularJS

What is Authentication?

Authentication is the process of identifying a user that wants to access a protected resource. We use authentication is our everyday lives: ID cards, user names and passwords, security cards, etc. The process that comes next is called ‘Authorization’ and they are very strong related and sometimes confused. With ‘Authorization’, we can check for user rights and see if they have or not access to a specific resource after they have been authenticated. However, make no mistake; there could be no ‘Authorization’ without ‘Authentication’. In this article we will discuss about the process of identifying ‘who is this user’ using AngularJS.

Considerations

Remember that all actions take place on the client side, which means that the client has full control over the browser and can overpass security checks. Therefore, it is very important to make the verification on the back-end also.

Setting up client-side Routing

Here we can decide which pages will need authentication and set the routing for the application. A route is defined providing at least the template or templateUrl and the controller of that page. In addition, I have added the ‘access’ property with ‘allowAnonymous’ attribute. This way we now if the current route needs authentication or it is a free access page. In our example we have the ‘Login’ page which is accessible by anyone and the ‘MembersPage’ that needs authentication.

// in app.js
var myApp = angular.module('myApp',['ngResource', 'ngCookies', 'ngRoute']);   myApp.config(function ($httpProvider, $routeProvider) {
       window.routes = {
            '/Login':
{ templateUrl: '/Account/Login',
  controller: 'AccountController',
  access : {allowAnonymous : true}
},
            '/MembersPage:
{ templateUrl: '/Home/SomePage,
  controller: SomePageController',
  access: {allowAnonymous:false}}};
        for (var path in window.routes) {
            $routeProvider.when(path, window.routes[path]);
        }
        $routeProvider.otherwise({ redirectTo: '/Login' });
    });   

The next thing to do is to recognize an authenticated user and check if it has access to the routes.

Recognize an authenticated user

There are several ways for doing that but I prefer using the power of AngularJS throughout the use of ‘Services’. Therefore, I have created a ‘UserService’ where we store the current user name and a value-indicating if is authenticated or not.
// in UserService.js
myApp.factory('userService', function () {
    var user = {
        isLogged: false,
        username: '',       
    };

    var reset = function() {
        user.isLogged = false;
        user.username = '';
    };

    return {
        user: user,
        reset : reset
    };
});

After we have the service in place, it is time to use it and implement the check functionality for a route. There are several methods that intercept the route change event, but we are interested only in those that occur before the user was redirected so we can check if is authenticated: $routeChangeStart, $locationChangeStart. Here we can check if the route that the user is going to allows anonymous access and if the user is logged in. If the case of failure, we can display an error message and redirect the user to the login page.

// in RootController.js
myApp.controller('RootController',
  function ($scope, $route, $routeParams, $location, $rootScope, authenticationService, userService, toaster) {
     $scope.user = userService.user;
     $scope.$on('$routeChangeStart', function (e, next, current) {               
         if (next.access != undefined && !next.access.allowAnonymous && !$scope.user.isLogged) {
                    $location.path("/Login");                   
                }
            });

            $scope.logout = function () {
                authenticationService.logout()
                    .success(function (response) {
                        userService.reset();                       
                        toaster.pop("info", 'You are logged out.', '');
                    });
            };
           
 $rootScope.$on("$locationChangeStart", function (event, next, current) {
  for (var i in window.routes) {
    if (next.indexOf(i) != -1) {
     if (!window.routes[i].access.allowAnonymous && !userService.user.isLogged) {
           toaster.pop("error", 'You are not logged in!', '');
              $location.path("/Login");                                                 
                        }
                    }
                }
            });
        });


Authentication Service

This service provides a way of communicating with the server and sets up the login status. We are interested in login/logout methods for the moment. On the back-end it is used a Web API service.
The login method is a post request sending the ‘login’ data consisting of the username and password. Notice also the ‘RequestVerificationToken’ that is used to avoid cross-site request forgery attacks.

//in AuthenticationService.js
myApp.factory('authenticationService',
    function ($http, $log, $location) {
        return {
            login: function (login, antiForgeryToken) {
                return $http({
                    method: 'POST',
                    url: '/api/Account/AuthenticateUser',
                    data: login,
                    headers: { 'RequestVerificationToken': antiForgeryToken }
                });
            },
            logout: function () {
                return $http.post('/api/Account/Logout');
            }
        };
    });


Putting up all together

The only thing that remains is to create a view to collect login information and a controller where we can use the authentication service and the user service together.
A very simple login form would look like the example below. We have three input fields wrapped up in a form.

//in Login.cshtml
@model Model.LoginModel
@{ Layout = null; }
@functions {
  private String GetAntiForgeryToken()
  {
    string cookieToken, formToken;
    AntiForgery.GetTokens(null, out cookieToken, out formToken);
    return cookieToken + ":" + formToken;
  }
}

<div class="container">
  <form name="loginForm" class="form-signin">
    <input id="antiForgeryToken"
           data-ng-model="antiForgeryToken"           
           data-ng-init="antiForgeryToken='@GetAntiForgeryToken()'" type="hidden"/>
    <h2 class="form-signin-heading">Authentication</h2>
    <br />
    <input type="email" required="required" name="username" class="form-control"
      ng-model="userData.username" placeholder="Email address" />
    <input type="password" required name="password" class="form-control"
      ng-model="userData.password" placeholder="Password" />
    <label class="checkbox">
      <input type="checkbox" value="remember-me">Remember me</label>   
    <button type="submit" class="btn btn-lg btn-primary btn-block" ng-click="login(userData, loginForm)">Login</button>
  </form>
</div>

When the user clicks login button, the form data is sent to the account controller. Using the authentication service, the data is sent in a POST request. In case of success we use the user service to store the data and redirect the user to the desired page, otherwise we show an error message and keep the user on the login page.

// in AccountController.js
myApp.controller('AccountController',
    function AccountController($scope, $cookies, $log, $location, authenticationService, toaster, userService) {

  $scope.login = function (userData, loginForm) {
      if (loginForm.$valid) {
          authenticationService.login(userData, $scope.antiForgeryToken)
              .success(function (response) {
                 if (response.status) {
                    userService.user.username = response.data.userName;
                    userService.user.isLogged = response.data.isLogged;
                    toaster.pop('success', 'You are signed in!', '', 2000, true);
                    $location.path("/MembersPage");
                    } else

{
         toaster.pop('error', 'Invalid username or password!', '', 2000, true);}
}).error(function (data, status, headers, config) {
       $log.info(data);});}
  };
});


This is all it takes to have a rudimentary authentication implementation with AngularJS.