How to more cleanly handle authentication life-cycles with an API; routes, redirectTo, interceptors, localStorage, default headers and more?

771 views
Skip to first unread message

Brandon Weiss

unread,
Mar 10, 2013, 5:27:32 PM3/10/13
to ang...@googlegroups.com
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.

I'll try to summarize the confusing and/or important bits after the code:

app.config(["$routeProvider", "$locationProvider", function($routeProvider, $locationProvider) {

  $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!

Brandon Weiss

unread,
Mar 17, 2013, 1:55:22 PM3/17/13
to ang...@googlegroups.com
I've made some progress on this, in case anyone is interested.

The interceptor looks almost exactly the same, except now I'm broadcasting an event.

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) {
        $rootScope.$broadcast("event:unauthenticated");

        if ($location.path() != "/sign_in") {
          $location.path("/sign_in");
        }

        // var deferred = $q.defer();
        // return deferred.promise;
      }

      return $q.reject(response);
    }

    return function(promise) {
      return promise.then(success, error);
    }
  }]

  $httpProvider.responseInterceptors.push(authenticationInterceptor);

});

My app controller now watches for two types of events:

function AppController($scope, $http, $location) {

  $http.defaults.headers.common["Authorization"] = "Token " + localStorage.getItem("access_token");

  $scope.$on("event:authenticated", function(event, user) {
    localStorage.setItem("access_token", user.access_token)
    $http.defaults.headers.common["Authorization"] = "Token " + user.access_token
    $location.path("/resources")
 })

  $scope.$on("event:unauthenticated", function(event) {
    localStorage.removeItem("access_token")
    $http.defaults.headers.common["Authorization"] = undefined
  })

}

Then when signing in or signing out I emit the appropriate events:

$scope.$emit("event:authenticated", data.user)
$scope.$emit('event:unauthenticated')

It's a little cleaner, but mirroring the access token in local storage to $http is still kludgey, as is adding logic to routes using redirectTo functions. I guess I could do that stuff in the controller; for some reason it just seemed cleaner to do it at the router level.

Adam Burmister

unread,
Mar 18, 2013, 5:50:02 AM3/18/13
to ang...@googlegroups.com
Hi Brandon

I'm a little late to your posts - but sounds good. 

I'm doing something very similar:
  • Broadcasting events for login (so everyone can react)
  • Intercepting 401s
  • Setting tokens on login event (although I've passed around an OAuth2 service, which implements an Auth interface - so we can swap it out easiy)
The trick I seemed to have missed is in the securing of routes using redirectTo - I'm implementing a resolve function which cancels the route when it's secure - but I think I like this hack.

One little nicety which I'd like to share is marking routes as secure or not within the route...

.when('/welcome', {
        templateUrl: 'views/controllers/welcome.html',
        controller: 'WelcomeCtrl',
        title: "Welcome",
        secure: true,
        resolve: { beforeFilter: beforeFilter }
      })

Then my interceptor checks for the secure param in the current route.

I think this is such a common use case that this stuff needs to be baked into the project starting structure. In my mind most SPA would have some level of auth.

My prediction is that there's yet another level of abstraction coming for AngularJS - something that delivers a Rails-esque experience for frontend, that includes an array on common helpers (page titles, secure routing, string stuff) and patterns (including auth).

Brandon Weiss

unread,
Mar 18, 2013, 1:28:56 PM3/18/13
to ang...@googlegroups.com
Interesting. Would you mind posting an example of how you do it? I'd love to see. Especially regarding the secure param and the beforeFilter.

Adam Burmister

unread,
Mar 18, 2013, 5:08:03 PM3/18/13
to ang...@googlegroups.com
I'm doing something like this for secure routes:

      var currentPath = $location.path();

      $rootScope.$on("$routeChangeStart", function (event, next, current) {
        // Only protect valid routes and secured routes with login dialog
        if (next.$route) {
          if(next.$route.secure === true) {
            if(!OAuth2.isAuthenticated()) {
              OAuth2.reset();
              $rootScope.$broadcast(snLoginDirectiveEvents.show, "login", currentPath);
            }
          } else {
            $rootScope.$broadcast(snLoginDirectiveEvents.hide);
          }
        }
      });

      // Trigger login onready - we need to do this as the directive isn't setup to listen to events by this stage
      $rootScope.$on('$viewContentLoaded', function (e) {
        if($route.current && $route.current.$route.secure) {
          if (!OAuth2.isAuthenticated()) {
            $timeout(function() {
              $rootScope.$broadcast(snLoginDirectiveEvents.show, "login", currentPath);
            })
          }
        }
      });

--
You received this message because you are subscribed to a topic in the Google Groups "AngularJS" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/_WxN4F-yF7c/unsubscribe?hl=en-US.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Witold Szczerba

unread,
Mar 18, 2013, 8:20:03 PM3/18/13
to ang...@googlegroups.com

Hi,
I have been thinking about authentication long time ago, when I was developing my first single-site web app using AngularJS. It was a beta project back then.
Then I was doing another app, and I had came up with similar idea. So I have written about the subject:
www.espeo.pl/2012/02/26/authentication-in-angularjs-application
and later I have created a repository on github:
https://github.com/witoldsz/angular-http-auth

Now, I use this in yet another application, but all of them are using traditional cookie based approach.
However, I think the overall concept suits any authentication process, it just need some adjustements.

See if you can get anything from all that to suit your case.

Regards,
Witold Szczerba
---
Sent from my mobile phone.

You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.

Brandon Weiss

unread,
Mar 19, 2013, 6:59:57 PM3/19/13
to ang...@googlegroups.com
When you hook an event to $viewContentLoaded, doesn't that get triggered every time the view changes? Or just when the application loads the first time?

Brandon Weiss

unread,
Mar 19, 2013, 7:00:35 PM3/19/13
to ang...@googlegroups.com
Yes, I read that post, it was a very helpful start. Thank you :)
Reply all
Reply to author
Forward
0 new messages