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!
<
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
The Website The APIWARNING: Bad sport jokes will abound.
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.
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.
Hey Lukas,
You forgot to write “Go Hawks!” at the end.
Being from Arizona… you know I cannot do that 😀
I miss login error control in your code.
I mean, if you enter bad credentials (wrong username/password) in the login form, the API (I guess) returns an HTTP 401 error code, so the interceptor try to change the state (no change should happens, because we are already in the login state), and no error is displayed.
So, I think in two ways of doing that login error control, but I don’t know which it’s better and how to get some things done: the first one, “hook” to the reject promise returned by LoginService.login, but I don’t know if there are any “side effect” for trying to change the state (in the interceptor) that could affect to any error message or so that I could show from the reject handler (in the LoginController). The second one, use the unauthorized event, but in this case, ¿how can I tell the difference between an error in login process and an unauthorized access or expired session?
Thanks.
You know you can also set default headers in $http, right? Fetching the current user and creating a header for each request is unnecessary load. Just set (or remove) the default headers on authorized & unauthorized events.
Lukas,
Very nice and interesting lesson!
I myself wrote an article about AngularJS Interceptors which might be used as a reference: http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/
You can but what happens if you refresh the page? You have to reset your headers again.
I love your article and have referenced it many times. Keep writing awesome stuff!
Miguel
I had the same issue. Even if you return a promise from LoginService you would still have problem because LoginService will POST to authentication and when you get 401 in return as it failed it will be intercepted and pass to LoginService and the circle closed. I think flow should go to LoginService in interceptor only if error code is 401 and !isLoginUrl(url)
Two of my favorite use cases for http interceptors are retrying failed requests and checking for mobile network access in a Cordova app.
As a cleanup item, you should also cleanup up the $rootScope listeners in MainCtrl controller when the controller goes out of scope (i.e. use the $scope.$on(‘$destroy’….) listener. The reason you want to do this is that each time you switch to the main view, the listeners are added.
Let’s say you go to the login page, then switch to the main page. Do that 4 more times. You now have 4 listeners for each of the $rootScope.$on calls. When the broadcast fires, the $rootScope listeners will get fired 4 times.
If you’re not expecting the calls to fire multiple times, it’s a hard bug to find. Just ran into this over the weekend.
Good point Rehan! I accept pull requests… 😀
Hi Lukas Ruebbelke, great write up and intro to Interceptors. It was a clear way for me to learn something new. However, I would advise that you update the article to explicitly point out the problem when you don’t manually reject the promise in the responseError or requestError handlers for the interceptor (e.g. $q.reject(response)) . Basically causing failed requests (eg. 40x response codes) being handled by the successHandler.
This is an issue highly elaborated on this GitHub issue: https://github.com/angular/angular.js/issues/2609
Thanks.
Thanks Max! Great point!
How do I trigger a login form to popup?
I would recommend redirecting the user to the login route / state on an unauthorized or error event. For instance, if you are using ui-router, you can do something like $state.go(‘login’); presuming that you have a login state. Let me know if you would like me to elaborate further.
Does this code actually work? $rootScope is unavailable within the responseError block when I try
$rootScope.$broadcast(‘unauthorized’);
Could it be related to angular version? I am using 1.4 with new router
Hmmmm maybe? haha let me pull it down and see if something has changed in the latest. Thanks for pointing this out.
A very useful, step by step, and easy to understand tutorial. Thank you very much =)
A great post, but I’m facing a problem. I tried it on localhost (My PC), it works fine. but when I placed the same code on my VPS, it’s not passing the authorization code. although, I used to add the following line in js file “config.headers.authorization = access_token;”
But, when I try to access it through PHP, using, “print_r(apache_request_headers())”, “authorization” is not in the header’s array. any Suggestion?
Very nice article. I implemented this article in my application. We are authenticating token at server side. I just want to know how it protect with XSRF issue? I am running IBM app scan and its throwing warning of XSRF. Need your insight on this?
responseError should return a promise using $q as per the angular example
Hello,
I’ve been trying to figure out an error I’ve had for hours while using your code. Using the APIInterceptor Service within the .config() block of my app, causes a unknown provider error for any dependancies inside the APIInterceptor (such as ‘UserService’)?
Does anyone else get the same issue or can you help?
Thanks
Can you put your code into a plunk or post the repo so I can see it?
Yeah sure here is a plunkr with some of the code extracted to it:
https://plnkr.co/edit/9b6O5c00erHqfbwRB2Pk
Any help would be appreciated, thank you
Hey Ben,
For the plunker, can you make sure that you include Angular, and that there are no errors in the console except the error that you were experiencing? I tried the project myself and it seemed to work ok, so I need the app in the plunker to run on its own and accurately reproduce the problem so I can debug it. Thanks!