Creating an AngularJS application with user authentication

Creating an authentication system within a Single Page App (SAP) can be a bit tricky; hopefully this post will help alleviate some of the challenges. Before you begin, it's a good idea to set up your environment so as to decouple the front and backends. There's a good post explaining the process over at StackOverflow. (The StackOverflow answer deals with a Laravel backend, but the code is easily modified for any other backend service.)

Our application will be structured as follows:

auth_demo/                              - application root
auth_demo/backend/                      - backend (PHP app) root
auth_demo/frontend/                     - frontend (AngularJS app) root
auth_demo/frontend/index.html           - AngularJS index
auth_demo/frontend/js/                  - JavaScript libraries
auth_demo/frontend/js/app/app.js        - core AngularJS application
auth_demo/frontend/js/app/controllers/  - AngularJS controllers
auth_demo/frontend/js/app/directives/   - AngularJS directives
auth_demo/frontend/js/app/partials/     - AngularJS partials
auth_demo/frontend/js/app/services      - AngularJS services

Let's start with our basic index.html setup:

<!DOCTYPE html>
<html data-ng-app="auth_demo">
<head>
  <title>Auth Demo</title>
</head>
<body>
  <div class="user-info" data-ng-show="user_email">
    
    | <a href="" data-ng-click="logout()">Logout</a>
  </div>

  <div data-ng-view></div>

  <script type="text/javascript" src="js/angular.min.js"></script>
  <script type="text/javascript" src="js/angular-route.js"></script>
  
  <script type="text/javascript" src="app/app.js"></script>
</body>
</html>

There are a few things going on here... First, we declare this DOM as an AngularJS app named "auth_demo." Second, we create a user-info DIV (which will only be shown when the ng-show directive's expression evaluates to true), bind the user_email variable, and provide a logout link which will trigger the logout() function on click. Finally, we use the ng-view directive to display the content for our menu route, and then load the various JavaScript files required by our app.

Now we can create our basic App (in app.js), routing a few paths to appropriate controllers and partials:

var app = angular.module('auth_demo', [
  'ngRoute'
]);

app.config(function ($routeProvider) {
  $routeProvider
    .when('/', {
      controller: 'LoginController',
      templateUrl: '/app/partials/login.html',
      resolve: {
        factory: isAnon
      }
    })
    .when('/user', {
      controller: 'UserController',
      templateUrl: '/app/partials/user.html',
      resolve: {
        factory: isAuth
      }
    })
    .otherwise({
      redirectTo: '/'
    });
});

Note that we declare ngRoute as a dependency for our App, and use the resolve object property to map dependencies that will be injected into the controller. Our isAnon and isAuth dependencies will be promises, and Angular's router will wait for them to be resolved or rejected before instantiating the controller. This means we can use them as a sort of "filter" to control access to our routes.

Now we will create the isAuth and isAnon promises:

/**
 * Ensures user is authenticated, otherwise redirects to /
 * @param  {[type]}  $q         [description]
 * @param  {[type]}  $http      [description]
 * @param  {[type]}  $rootScope [description]
 * @param  {[type]}  $location  [description]
 * @return {Boolean}            [description]
 */
var isAuth = function($q, $http, $rootScope, $location) {
  var defered = $q.defer();

  $http.get('/ws/api/v1/auth', { }).success(function (data) {
    if (data.status !== 'ok') {
      return;
    }

    // user is authenticated
    if (data.auth === 1) {
      $rootScope.user_email = data.user_email;
      defered.resolve(true);
    }
    // user is anonymous
    else {
      defered.reject();
      $location.path('/');
    }
  });

  return defered.promise;
};

/**
 * Ensures user is anonymous, otherwise redirects to /user
 * @param  {[type]}  $q         [description]
 * @param  {[type]}  $http      [description]
 * @param  {[type]}  $rootScope [description]
 * @param  {[type]}  $location  [description]
 * @return {Boolean}            [description]
 */
var isAnon = function($q, $http, $rootScope, $location) {
  var defered = $q.defer();

  $http.get('/ws/api/v1/auth', { }).success(function (data) {
    if (data.status !== 'ok') {
      return;
    }

    // user is anonymous
    if (data.auth === 0) {
      defered.resolve(true);
    }
    // user is authenticated
    else {
      defered.reject();
      $location.path('/user');
    }
  });

  return defered.promise;
};

Both of the promises perform a GET request against /ws/api/v1/auth and work with the returned JSON. Our example backend will return a JSON object which will contain a string status property (set to either ok or error), a boolean auth property (current authentication state), and a string user_email property. Depending on the values in returned JSON, the promises will either resolve or reject.

Now that the promises are set up (and the backend is returning the appropriate JSON response) we can create the login form in partials/login.html:

<form id="login-form" class="partial-popup" data-ng-submit="process()">
  <h1>Login</h1>

  <messages-list></messages-list>

  <input type="text" name="email" placeholder="email" data-ng-model="form_data.email" />

  <input type="password" name="password" placeholder="password" data-ng-model="form_data.password" />

  <input type="submit" value="Login" />
</form>

This form creates two textfields, binds their values to a form_data object, and calls the process() function upon form submission.

Next up is the the loginController we refenced in our route provider. Declare it in controllers/loginController.js, and don't forget to load the file in your index.html.

You'll note that the controller uses a (so far undefined) service called authService. Let's create it in services/authService.js...