Sort of RESTish Authentication with Angular JS
Posted by Tom on 2013-11-17 22:07
I've been experimenting with AngularJS for some pet projects (with Flask on the server) and I'm trying to persuade people to consider it at work (with .NET WebAPI on the back). I've been gradually shifting more and more thinking to the client and it just seemed like the next logical step. Why did I choose Angular? It pretty much came down to Angular or Ember, and I pretty much flipped a coin for it. Sorry, Ember.
The official tutorials for Angular are getting better. When I first looked at them they had a tonne of examples which they tell you in the very next paragraph are the bad and wrong of doing things (for example: all of the examples weren't using modules), but they seem to be much more consistent now. Something that still feels notable by its absense is the lack of any tutorials for authentication. Here's what I came up with, bearing in mind I have very little Angular experience. Hopefully something better will come along and make this all redundant. Obselete me. Please.
This tutorial will not be going into lots of detail, and assumes that you've spent a few hours getting used to the basics. If you can't create a module, set the config, set up some routes and hook the controllers up then you're going to have a rough time. Go and check out those aforementioned tutorials and come back. It's cool - I'll wait.
Oh, and let's get something else clear from the off: this is not REST. I'm going to attempt to be at least slightly RESTy. Partially RESTified. Moderately RESTicated. Notice there's a lot of qualifiers in there, because no-one in the world seems to apply REST in all its byzantine glory, or even agree on what it is. I guess I'm probably at level 2 according to Leonard Richardson's REST Maturity Model, but the fact that I want stateful authentication at all would make a true REST advocate crap out their liver in disgust. The real zealots seem to advocate storing the username and password client-side and sending it via HTTP basic auth on every request, but that's frankly pants-on-head crazy. You can't log out, the browser-supplied mechanisms are crap, I may not set up TLS in which case I'm sending credentials in the clear on every request and I'd really rather not have them stored in the client anyway.
So we're not stateless, and therefore not REST. But we're stealing some of their ideas. Pragmatism - it's what's for breakfast.
Enough rambling - here's the code for the Angular service which will perform the actual authentication.
var testServices = angular.module('testServices', ['ngResource']).
factory('Session', function($resource) {
return $resource('api/v1/sessions/', {}, {
save: { method: 'POST' }
});
});
So there's our service - the brains are on the server, of course. The service endpoint accepts a username and password, and if they match we'll get a 200 status and a session token back, or a 401 Unauthorized if they don't. I won't get into the details of that. Just grab your server-side of choice and do the needful. Next we need to create our controller.
testApp.controller('LoginCtrl', ['$scope', '$http', 'Session', function($scope, $http, Session) {
$scope.details = new Session();
$scope.login = function() {
$scope.details.$save(
function(d) { $http.defaults.headers.common['X-Auth'] = d.Token; },
function() { alert('login failed'); }
);
};
}]);
When we see the result of a successful login we set a new default HTTP header called X-Auth, which can then be picked up by the server and checked to ensure its validity on each request. Nice and simple. If the login is unsuccessful then we poop out a Javascript alert box. Classy. You'll want something better looking than that, but I'll leave that as an exercise to the reader. We have more pressing matters to attend to!
Such as: how do we direct the user to login when we see a 401 from the server, either as a result of an unauthenticated user arriving at the site, or an existing session timing out? Both of these should dump you back to a login page. These responses from the server can be intercepted using an interceptor, natch, and handled accordingly. First we create a factory method for our interceptor:
testApp.factory('authRedirectInterceptor', ['$q', '$location', function($q, $location) {
return {
response: function(response) {
return response || $q.when(response);
},
responseError: function(rejection) {
if (rejection.status == 401) {
$location.path('/login');
}
return $q.reject(rejection);
}
};
}]);
And then add the interceptor inside our module's config function, so it ends up looking something like this:
testApp.config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider, $rootScopeProvider) {
$routeProvider.
/* Routes to actually usefule stuff, and then . . . */
when('/login', { templateUrl: 'partials/login.html', controller: 'LoginCtrl' });
$httpProvider.interceptors.push('authRedirectInterceptor');
}]);
Taking it a step further
That about covers the basics. Let's look at some more details.
Currently, if the page is reloaded for any reason rather than going through the usual routing and hash trickery then you lose your session, since the token doesn't survive a page load. That's pretty fragile. Too fragile, really. So we need to put it somewhere persistent and pull it back out again when the app spins up. The cleanest option is to use browser storage, but you can also fall back to cookies if you're on an old and janky browser that doesn't support it (although at this stage in the game I feel fine telling anyone still using IE7 to fuck right off). The classy way of using browser storage from within Angular to use the dependency injector. By using the .value method (technically on $provide, but also available via the application module) to add window.sessionStorage to the list of things Angular can inject. If you want the tokens to survive through a browser or tab closure you can go for window.localStorage instead.
testApp.value('browserStorage', window.sessionStorage);
So now our successful login callback looks like this:
function(d) {
browserStorage.setItem('token', d.Token);
$http.defaults.headers.common['X-Auth'] = d.Token;
}
And we add this little snippet to our app module's run() method to make sure an existing token is added in the even that the page is reloaded.
testApp.run(['$http', 'browserStorage', function($http, browserStorage) {
if (browserStorage.getItem('token') !== undefined)
$http.defaults.headers.common['X-Auth'] = browserStorage.getItem('token');
}]);
~fin
There are still some holes, obviously. We need to handle logouts, but that's just a matter of accepting a DELETE to the session endpoint to invalidate the token, and then removing the X-Auth line from the default headers. There needs to be better messages to the user (why am I on the login page? Session timeout? Authorisation required? Unsuccessful authentication?). But it got me up and running and should provide a starting point for people playing with Angular.