Select Page

The Setup

Super Bowl XLIX is just around the corner. And in my case, Super Bowl XLIX is literally just around the corner. I happen to live just a few miles from the stadium and you cannot help but get caught up into the hysteria that surrounds such an epic sporting event. Coincidentally, I recently had to create an HTTP interceptor to attach a session token to my HTTP headers so a football themed post is the natural thing to do. Right? RIGHT!

Super Bowl 2015

<

p style=”text-align: center”>Awesome Photo by Matthew Wheeler

This is an extension of the Build a Simple REST Application with AngularJS Pt 2 Master Detail Interface post with an authentication component built in. We are going to focus primarily on the pieces that pertain to user authentication and skip the basic AngularJS parts. The demo application has two parts; the website and the API and so make sure to download both and let the games begin! #groan

WARNING: Bad sport jokes will abound.

The Website The API

TL;DR Version

HTTP interceptors are a great way to define behavior in a single place for how a request or response is handled for ALL calls using the $http service. This is a game changer when you want to set an auth token on all outgoing calls or respond to a particular HTTP status error at the system level. You can pair interceptors with the incredibly useful Angular Storage module to cache an authenticated user and retrieve it the next run time.

The Resources

We are going to be introducing routes into our application with a route for the existing dashboard and another route for logging in, so we need to add in the library for AngularUI Router. We are also going to store the user information using Angular Storage. We will first add the JavaScript resources to our index.html file as seen below.

<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.13/angular-ui-router.min.js"></script>
<script src="//cdn.rawgit.com/auth0/angular-storage/master/dist/angular-storage.js"></script>

And then we will inject the angular-storage and ui.router submodules into our application.

angular.module('SimpleRESTWebsite', ['angular-storage', 'ui.router'])

We are now ready for kick off! #sigh

Angular Storage

The first technique that I want to discuss is the use of the store service to keep track of our user. The store service uses local storage if it is available and ngCookies if it is not. It allows us to store JavaScript objects and maintain type which is really nice. Using store is really simple as it primarily consists of a getter and a setter that allow us to store a value with a key and then retrieve that value.

In the UserService below, when a user is set, we are calling store.get and storing a reference to the user object. When service.getCurrentUser is called and currentUser does not exist, we are calling store.get to get the latest currentUser value. We will see how this comes in really handy in just a moment.

.service('UserService', function(store) {
    var service = this,
        currentUser = null;
    service.setCurrentUser = function(user) {
        currentUser = user;
        store.set('user', user);
        return currentUser;
    };
    service.getCurrentUser = function() {
        if (!currentUser) {
            currentUser = store.get('user');
        }
        return currentUser;
    };
})

And that is pretty much all there is to leveraging the store service. Getters and setters are pretty much the simplest play in the book.

The Interceptor

The next technique that I want to discuss is creating an interceptor that will modify all requests made with the $http service as well as intercept any response errors. We want to modify the outgoing requests so that we can add an authorization token to the header of our call. Doing it in the intercepter allows us to do it one place and not have to worry about setting it at the individual service level. If the session token has expired and we receive a 401 error from the server we also want to broadcast that status so we can redirect the user to log back in again.

We are going to create a service called APIInterceptor that has our two interceptors; we will define them as service.request and service.responseError. We are also going to inject $rootScope and UserService for use in our interceptors.

.service('APIInterceptor', function($rootScope, UserService) {
  var service = this;
  service.request = function(config) {
    return config;
  };
  service.responseError = function(response) {
    return response;
  };
})

In our request interceptor, we are going to call UserService.getCurrentUser to get the current user and, if one exists, we will grab the session token and assign it to a variable. The request interceptor takes a single argument which is an HTTP config object that we will use to assign the value of access_token to an authorization property on the config.headers object. Now that we have made the appropriate modification to the config object, we can return it for actual processing from the $http service.

.service('APIInterceptor', function($rootScope, UserService) {
    var service = this;
    service.request = function(config) {
        var currentUser = UserService.getCurrentUser(),
            access_token = currentUser ? currentUser.access_token : null;
        if (access_token) {
            config.headers.authorization = access_token;
        }
        return config;
    };
    service.responseError = function(response) {
        return response;
    };
})

When our session token expires or the user is not authenticated, the server will respond with a 401 error that we want to intercept so we can have the user log back into the application. We listen for this error in our service.responseError interceptor by checking the response.status code. If the response.status code is 401 then we will broadcast an unauthorized event on $rootScope. The last thing we want is for a user to be caught offside in our application. #totallyJustMadeThatJoke!

.service('APIInterceptor', function($rootScope, UserService) {
    var service = this;
    service.request = function(config) {
        var currentUser = UserService.getCurrentUser(),
            access_token = currentUser ? currentUser.access_token : null;
        if (access_token) {
            config.headers.authorization = access_token;
        }
        return config;
    };
    service.responseError = function(response) {
        if (response.status === 401) {
            $rootScope.$broadcast('unauthorized');
        }
        return response;
    };
})

Now that we have defined our interceptor, we need to actually apply it to our application. Interceptors are added by pushing them into the $httpProvider.interceptors array as we have done at the bottom of our config block.

.config(function($stateProvider, $urlRouterProvider, $httpProvider) {
    $stateProvider
        .state('login', {
            url: '/login',
            templateUrl: 'app/templates/login.tmpl.html',
            controller: 'LoginCtrl',
            controllerAs: 'login'
        })
        .state('dashboard', {
            url: '/dashboard',
            templateUrl: 'app/templates/dashboard.tmpl.html',
            controller: 'DashboardCtrl',
            controllerAs: 'dashboard'
        });
    $urlRouterProvider.otherwise('/dashboard');
    $httpProvider.interceptors.push('APIInterceptor');
})

Authentication

Now that we have covered storing the user data and intercepting our service calls, I want to highlight the pertinent pieces in the sample application.

Login Screen

When signIn is called in our LoginCtrl, the user object is passed into a LoginService.login call which is resolved via a promise. When that promise is resolved, a few important steps are happening that warrant commentary.

  • We are taking the value of response.data.id, which in this case happens to be our session token, and assigning it to user.access_token. We are then calling UserService.setCurrentUser with our modified user object; this will ultimately use the store service to store a local, persistent reference.
  • We are also using $rootScope.$broadcast to send an authorized event that we will listen for in our top-level MainCtrl.
  • We are using the $state service to redirect the user to the dashboard view by calling $state.go(‘dashboard’)
.controller('LoginCtrl', function($rootScope, $state, LoginService, UserService){
    var login = this;
    function signIn(user) {
        LoginService.login(user)
            .then(function(response) {
                user.access_token = response.data.id;
                UserService.setCurrentUser(user);
                $rootScope.$broadcast('authorized');
                $state.go('dashboard');
            });
    }
    function register(user) {
        LoginService.register(user)
            .then(function(response) {
                login(user);
            });
    }
    function submit(user) {
        login.newUser ? register(user) : signIn(user);
    }
    login.newUser = false;
    login.submit = submit;
})

In our MainCtrl, we are listening for the authorized and unauthorized events. When a user logs in and an authorized event is fired, we respond to that event by updating main.currentUser so we can show the logout button in the nav bar. When the unauthorized event is fired, we null out the current user and redirect the application back to the login screen. This is useful if a user navigates directly to an internal state of the application and they do not have a valid session token as the error is caught and the user is prompted to authenticate.

.controller('MainCtrl', function ($rootScope, $state, LoginService, UserService) {
    var main = this;
    function logout() {
        LoginService.logout()
            .then(function(response) {
                main.currentUser = UserService.setCurrentUser(null);
                $state.go('login');
            }, function(error) {
                console.log(error);
            });
    }
    $rootScope.$on('authorized', function() {
        main.currentUser = UserService.getCurrentUser();
    });
    $rootScope.$on('unauthorized', function() {
        main.currentUser = UserService.setCurrentUser(null);
        $state.go('login');
    });
    main.logout = logout;
    main.currentUser = UserService.getCurrentUser();
})

And finally, in our nav bar at the top of the application, we are displaying a logout button and showing it only if there is a current user.

<body ng-controller="MainCtrl as main" ng-cloak>

<nav class="navbar navbar-default">
        <button ng-if="main.currentUser" class="btn btn-default navbar-btn" 
                ng-click="main.logout()">Logout <strong>{{main.currentUser.email}}</strong></button>
    </nav>
<div class="container-fluid">
<div class="row">
<div ui-view></div>

</div>

</div>
</body>

Review

This was a really fun project for me to dig into, and I owe a huge debt of gratitude to Martin Gonto for his input and excellent work on the Angular Storage module.

Let’s take a quick moment for a play-by-play recap of what we learned in this lesson.

  • Angular Storage is a great way to store local, persistent data. It exposes the store service with a simple getter-setter interface.
  • We can intercept HTTP calls by creating a service that has request, requestError, response, or responseError interceptor methods defined on it.
  • We activate an interceptor service by pushing that service into the $httpProvider.interceptors array in the config block of our application.
  • A session token can be added for all outgoing HTTP requests by adding it to the config.headers object in a request interceptor.
  • We can intercept HTTP response errors and handle specific errors by checking the response.status code.
  • $rootScope has limited utility when it comes to direct interaction, but it works well as an event bus as seen in the case of keeping our MainCtrl synchronized.

Resources

Angular Storage

AngularUI Router

$http Documentation

The Website The API