I'm building a client with Angular.js to consume an API I've created. When I sign up/sign in an access token is returned, and then it's sent in subsequent requests in the headers to authenticate with the API. I've got this working just fine, but my solution is not very clean, and I suspect someone with a more advanced knowledge of Angular could probably help me improve it. I'm including a bunch of code, with everything but the important bits removed, and other stuff renamed and whatnot.
$routeProvider.
when("/sign_up", { templateUrl: "...", controller: ..., redirectTo: authenticationRoutesConstraint }).
when("/sign_in", { templateUrl: "...", controller: ..., redirectTo: authenticationRoutesConstraint }).
when("/forgot_password/:access_token", { redirectTo: accountAfterSettingAccessTokenConstraint }).
when("/forgot_password", { templateUrl: "...", controller: ..., redirectTo: authenticationRoutesConstraint })
}]);
function accountAfterSettingAccessTokenConstraint($routeProvider) {
localStorage.setItem("access_token", $routeProvider.access_token);
// We need to use window here to reload the whole page so
// the HTTP authorization headers will get set from the localStorage
// because we don't have access to the $http object here.
window.location = "/an_authenticated_path_where_they_can_reset_their_password";
}
function authenticationRoutesConstraint() {
if (localStorage.getItem("access_token") != null) {
return "/some_authenticated_path";
}
}
app.config(function($httpProvider) {
var authenticationInterceptor = ["$rootScope", "$location", "$q", function($rootScope, $location, $q) {
function success(response) {
return response;
}
function error(response) {
if (response.status == 401) {
localStorage.removeItem("access_token");
if ($location.path() != "/sign_in") {
$location.path("/sign_in");
}
}
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
}
}]
$httpProvider.responseInterceptors.push(authenticationInterceptor);
});
function AppController($scope, $http, $location) {
$http.defaults.headers.common["Authorization"] = "Token " + localStorage.getItem("access_token");
$scope.setAccessToken = function(accessToken) {
localStorage.setItem("access_token", accessToken);
$http.defaults.headers.common["Authorization"] = "Token " + accessToken;
}
$scope.removeAccessToken = function() {
localStorage.removeItem("access_token");
}
$scope.signOut = function() {
$scope.removeAccessToken(undefined);
$location.path("/");
}
}
Summary:
- I'm using local storage to store the access token.
- I didn't want users to be able to get to certain pages (sign in, sign out, forgot password), if they were already signed in. A simple check for the existence of access token is fine for this.
- The way password recovery works, a link with a new access token is sent to the user, they follow the link and it sets the access token and logs them in. That's probably a mis-use of redirectTo, but it works. The biggest issue is that from within that function I have no way of updating the $http default headers. So I have to force a page reload using window.location. What's a better solution for this?
- I'm using an interceptor to check for a 401 Unauthorized response from the API. If one is received, it removes the access token from local storage and redirects them to the sign in page, unless of course they're already on the sign in page.
- When the app loads, for example if a user has left the app and come back to it, or manually refreshed the page, as it loads it will pull the access token out of local storage and set it in the default headers.
- There's also a few convenience functions on the AppController (root controller) that I created to make setting and removing the access token a bit easier.
The biggest problem is that it's all very kludgey. I have access to some things in some places, but not others. The only consistent interface I have to the access token is local storage. Is it possible to watch local storage with Angular and have the default headers in $http be automatically updated if it's changed or removed? That would probably be most ideal, but I'm open to any better solutions. Thanks!