Elegant token-based API access with AngularJS
Learn how to handle token-based API access with AngularJS in an elegant, Don’t Repeat Yourself manner by globally transforming requests and handling failure and token re-issue using response interceptors.
Single page web apps have been growing in popularity over the last couple of years, notable pioneers include Zendesk and airbnb.
These applications, built using frameworks such as AngularJS, Ember, Backbone and Meteor are downloaded from the server and the source run within the user’s browser. Invariably during operation they’ll need to request additional data from the server or save state back to a database, typically through stateless REST APIs.
Accessing these APIs securely is problematic. In a server side app, suppose you want to interact with Amazon AWS. You’d expose your client ID and secret directly to the code, and if you’re doing it right secrets will not be checked into source code but instead part of your server environment (ideally managed via Puppet or Chef).
In a single page web app, the entire application payload is downloaded to the user’s browser, so you can’t bundle credentials as part of the app’s environment - that environment is the user’s browser potentially on millions of machines you have no control over.
A solution is to ask the user to sign in (i.e. they supply their own credentials) and the server returns a token. From hereon in the token can be sent with any API requests and all the server needs to do is to verify the token on each request. In an Angular service, using the built-in $http
service, retrieving some data might look like:
$http.get('/entity/'+id+'?access_token='+token).then(function(response) {
// do stuff
}
or via headers (my preference):
$http({method: 'GET', url: '/entity/'+id+'?access_token='+token, headers: {'Authorization': 'Bearer '+token}}).then(function(response){
// do stuff
});
However this has two drawbacks:
- Every interaction with the server needs access to the token and needs to bundle it with every request. Even small apps might have dozens of such interactions with the server.
- The user can easily be using the app longer than the time to live of your token. Or, the issuer could revoke the token. Therefore you need to handle token expiry - that is
401 Unauthorized
response from your API. Are you going to handle re-authing the user and obtaining a new token every time you interact with the API?
In Angular, we can make use of two features of $http
to deal with these problems more elegantly.
Let’s start with a DRY approach to sending the tokens on every request. We can use the transformRequest
property of $http
to modify every subsequent request made and append your token automatically.
angular.module('myApp',myDependanciesArray)
.config(['$routeProvider', function($routeProvider) {
// application config here, maybe define some routes?
$routeProvider.when('/entity/:id', {templateUrl: 'partials/template.html', controller: 'EntityCtrl'});
})
.run(['$rootScope', '$injector', function($rootScope,$injector) {
$injector.get("$http").defaults.transformRequest = function(data, headersGetter) {
if ($rootScope.oauth) headersGetter()['Authorization'] = "Bearer "+$rootScope.oauth.access_token;
if (data) {
return angular.toJson(data);
}
};
});
If available, a token stored in $rootScope.oauth.access_token
is appended to the headers on every request. As shown above, I like to place this code in the application’s run()
method - Angular’s much maligned but improving documentation explains why:
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application.
Next we’ll deal with token expiry or revokation. You’ll notice above I also included the .config
block, let’s refer again to the docs:
Configuration blocks get executed during the provider registrations and configuration phase. Only providers and constants can be injected into configuration blocks. This is to prevent accidental instantiation of services before they have been fully configured
Here we’ll use a property of the $httpProvider
, responseInterceptors
, to deal with the 401 situation. Response interceptors are functions which are called between the response of the http call being received, and it being handed off to your application code. I suppose they are a bit like express middleware in NodeJS.
What we want to do is intercept the response, see if it was a 401 request and if so re-obtain an OAuth token before continuing. Our server has an endpoint that allows re-issue of a token transparently if it deems the user session to still be valid. If not, we’ll redirect the user to sign in again.
config(['$routeProvider','$httpProvider', function($routeProvider,$httpProvider) {
$routeProvider.when('/entity/:id', {templateUrl: 'partials/template.html', controller: 'EntityCtrl'});
// intercept for oauth tokens
$httpProvider.responseInterceptors.push([
'$rootScope', '$q', '$injector','$location',
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK').then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request - transformRequest in .run() below will add the new OAuth token
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
}]
);
A couple of notes on this:
- Our response interceptor deals with the promise generated by the original request. The
promise.then()
function is equivalent to$http().then()
and thus has two parameters, a function for success, and a function for fail. - The first function passed to promise.then() is therefore the success case, i.e. no action is required, and the interceptor simply passes it onto the application - business as usual.
- If the promise was not successful, we take a look at why. If it’s a token fail we intervene - we’re using deferred promise API
$q
to stall the original request whilst we re-query for a new token. Docs here. - We devolve the decision about whether to issue a new token or not to an endpoint
/some/endpoint/that/reissues/tokens
and call this using JSONP. - For the JSONP request, we have to use
$injector.get('http')
to avoid circular dependency errors being reported by Angular - remember we are in the.config()
method here and services cannot be used here (refer to earlier explanation above). However we are merely registering a function that will be called later, not actually invoking that function here. So$injector.get
avoids Angular going all postal on us. - If the JSONP is successful, it contains a new token in the response, we set this to
$rootScope
and we retry the original request. Remember, thetransformRequest
will kick in and attach the new token to this retry. - If a new token cannot be issued transparently, we redirect the browser to a new Angular route which allows the user to sign in again.
- If the original promise did not fail due to 401, this is not about auth so simply reject.
Finally, in the dozens (or hundreds) of places in your services where your developers use $http
, your code becomes:
$http.get('/entity/'+id).then(function(response) {
// do stuff
}
The combination of transformRequest
and responseInterceptors
handle attaching the token to the header and gracefully dealing with a token fail/re-issue/login transparently.
Furthermore, using this method you can issue relatively short TTLs on your tokens and allow the user’s actual logged in session to be relatively lengthy.
Finally, I cannot stress the importance of handling any interaction involving tokens using https
to protect against sniffing of tokens or cookies.