Authentication in AngularJS (or similar) based application.  drukuj

Autor: Witold Szczerba
Tagi:

———————

2012-08-17 UPDATE

Implementation of the concept described below and also a demo application is available here:
https://github.com/witoldsz/angular-http-auth.

———————

Hello again,

today I would like to write a little bit about how am I handling authentication in an application front-end running inside web browser, using AngularJS.

Traditional server ‘login form’? Well… no, thank you.

At the beginning, I did not realize that traditional and commonly used form based authentication does not suit my client-side application. The major problem lies in a key difference between traditional – server-side, and client-side applications. In server-side applications, no one else but server itself knows user state and intentions, whereas in client-side applications this is no longer true.

Let’s take a look at a sample server-side web application flow of events:

  • user asks for a web page: something.com,
  • server generates markup and sends it back,
  • user chooses to visit a secured sub-page: something.com/secured,
  • server figures out that user does need to authenticate itself, so it:
    1. remembers what user asked for,
    2. responds with a login form (or a redirect to) instead of a requested content,
  • once user sends credentials back to the server, it serves what user initially asked for,
  • user keeps visiting secured pages and filling secured forms until their authorization expires (for whatever reason),
  • server once again responses with a login form and once user provides credentials, server redirects them back whenever they wanted to go.

Same application, but different flow:

  • user asks for: something.com/secured/formXyz,
  • server sends a login form,
  • user logs in, fills a long and complicated form, but they are doing it so long that theirs session expires,
  • user submits a form, but since the session is not valid anymore, login screen appears,
  • once user logs in, server can process the submitted form, no need to re-enter everything again.

Now let’s see how it is in client-side application, running inside a web browser:

  • user types somewhere.com in an address bar,
  • browser sends a “Content-Type: text/html” request,
  • server sends back a page with client-side application code,
  • code starts to execute and asks for user name (e.g. it wants to display user name in the upper right corner), so next request is issued, e.g.: “Content-Type: application/json”
  • traditional login form does not make sense here, as browser is not requesting a web page, but some data instead. Something is not right here.

OK, so let’s try other way around:

  • user asks for somewhere.com
  • entire site is hidden behind a login form authentication mechanism, so instead of an application, user is presented a form, so they can provide credentials,
  • once user submits, the originally requested page is provided, so as it was before: application loads, issues a new data request (application/json) for user name and receives it back, everything is nice so far… but let’s assume that our session expires (for whatever reason) while we are in the middle of a long and complicated form…

Guess what? We are exactly in the same place as before: our application is up and running, but our session is not valid any more and form based authentication is useless at this point. Or isn’t it?

Let’s try to adapt. Using AngularJS, we can simply write an http interceptor. Such a interceptor can check every response and once it detects a login form, we can… well…

  • We can redirect ourselves to a login page, but this is a complicated task, because server does not know where we are at the moment (or to be more precise: what is our state, what were we doing). Remember, client-side application is client-side, how is server supposed to figure out what to do next, after we logged in? From server-side point of view we are sitting on one page all the time.
  • We can be smarter: we can bring an IFRAME to life, it will show login form. But this is also complicated: we have to figure out somehow what is happening inside such an IFRAME. How to detect successful login? Is it easy? Hard? Not that hard but tricky?

Of course everything is doable, but after investigation, I did something else. Very simple and clean, but requires server side adjustments.

Solution: client-side login form when server answers: status 401.

My solution assumes the following server side behaviour: for every /resources/* call, if user is not authorized, response a 401 status. Otherwise, when user is authorized or when not a /resources/* request, send what client asked for. No login forms, but we still need some login URL, so our application can send login and password there. Plain-old cookie based sessions? Why not, they work for me, web browsers and application servers handle them automatically by default.

AngularJS has a $http service. It allows custom interceptors to be plugged in:

myapp.config(function($httpProvider) {
  function exampleInterceptor($q,$log) {
    function success(response) {
      $log.info('Successful response: ' + response);
      return response;
    }
    function error(response) {
      var status = response.status;
      $log.error('Response status: ' + status + '. ' + response);
      return $q.reject(response); //similar to throw response;
    }
    return function(promise) {
      return promise.then(success, error);
    }
  }
  $httpProvider.responseInterceptors.push(exampleInterceptor);
});

Nice thing is that from ‘response’ parameter we can rebuild the request. To fully understand how the $http interceptor works, we need to understand $q.

The goal is to be able to:

  • capture 401 response,
  • save the request parameters, so in the future we can reconstruct original request,
  • create and return new object representing server’s future answer (instead of returning the original failed response),
  • broadcast that login is required, so application can react, in particular login form can appear,
  • listen to login successful events, so we can gather all the saved request parameters, resend them again and trigger all the ‘future’ objects (returned previously).

Nice thing about the solution above is that when you request something, but server responds with status 401, you do not have (and you cannot) handle this. Interceptor will handle this for you. You will eventually receive the response. It will come as nothing had happened, just a little bit later (unless user won’t provide valid credentials).

OK, so here is a bit of code.

/**
 * $http interceptor.
 * On 401 response - it stores the request and broadcasts 'event:loginRequired'.
 */
myapp.config(function($httpProvider) {
  var interceptor = ['$rootScope','$q', function(scope, $q) {

    function success(response) {
      return response;
    }

    function error(response) {
      var status = response.status;

      if (status == 401) {
        var deferred = $q.defer();
        var req = {
          config: response.config,
          deferred: deferred
        }
        scope.requests401.push(req);
        scope.$broadcast('event:loginRequired');
        return deferred.promise;
      }
      // otherwise
      return $q.reject(response);

    }

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

  }];
  $httpProvider.responseInterceptors.push(interceptor);
});
myapp.run(['$rootScope', '$http', function(scope, $http) {

  /**
   * Holds all the requests which failed due to 401 response.
   */
  scope.requests401 = [];

  /**
   * On 'event:loginConfirmed', resend all the 401 requests.
   */
  scope.$on('event:loginConfirmed', function() {
    var i, requests = scope.requests401;
    for (i = 0; i < requests.length; i++) {
      retry(requests[i]);
    }
    scope.requests401 = [];

    function retry(req) {
      $http(req.config).then(function(response) {
        req.deferred.resolve(response);
      });
    }
  });

  /**
   * On 'event:loginRequest' send credentials to the server.
   */
  scope.$on('event:loginRequest', function(event, username, password) {
    var payload = $.param({j_username: username, j_password: password});
    var config = {
      headers: {'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'}
    }
    $http.post('j_spring_security_check', payload, config).success(function(data) {
      if (data === 'AUTHENTICATION_SUCCESS') {
        scope.$broadcast('event:loginConfirmed');
      }
    });
  });

  /**
   * On 'logoutRequest' invoke logout on the server and broadcast 'event:loginRequired'.
   */
  scope.$on('event:logoutRequest', function() {
    $http.put('j_spring_security_logout', {}).success(function() {
      ping();
    });
  });

  /**
   * Ping server to figure out if user is already logged in.
   */
  function ping() {
    $http.get('rest/ping').success(function() {
      scope.$broadcast('event:loginConfirmed');
    });
  }
  ping();

}]);

I wanted to provide a working example with a login window, using jsfiddle.net, but have to postpone it. It is getting a little bit late now, so I am finishing this entry here. I hope you like it :)

  • STEVER,

    thank you very much for your article!   Odpowiedz

  • Thomas Desfossez,

    Thanks, really good example. I'll try if soon.   Odpowiedz

  • Aki,

    Thanks for the doc, it was really helpful. But here, if the user gives invalid credentials while logging in which leads to 401, this message also gets pushed to the scope.request401 array. So once succesfful login happens and the previous invalid login message saved in the array will again be posted right. Shouldnt we have a control on pushing the message to the array.   Odpowiedz

  • firehist,

    Thank you, great article!   Odpowiedz

  • Anderson,

    Hi! Great article! :) When the server responds 401, the javascript code enters in the follow function: function error(response) Every error response appear 0 in the code. For example, when the server send a 401 error, in the error function, the variable response.status is 0. To verify, I opened the Google Chrome Developer Tools and Network tab and I saw "Status Code:401 Unauthorized". Have any ideas what happened? Thnks!   Odpowiedz

  • Roy Truelove,

    Very nice. Looks like someone just signed himself up to speak at an Angular meetup and post it on youtube??? +1 on posting the Spring backend - a end-to-end authenticated sample would be a huge benefit to the community. Thanks for your work!   Odpowiedz

  • Narretz,

    Witold, you have a great way of explaining! The concept is quite difficult, but you have a great ability of explaining it step-by-step and making the technical side understandable. This is a great talent. And obviously, your code is amazing. I really look forward to many more blog posts. Have you considered writing professionally? I am not kidding, good tech writers are needed desparately, especially for new frameworks.   Odpowiedz

  • Jon,

    Hi, Awesome article! Took me a while to work out what was happening but think I've got it now. I have one question though. When I get a 401 I make the user login. I then set a cookie with some details which will be used when the come back top the site so that they don't have to login again. However, when the list of URLs are retried they don't go with the new cookie information. D Do you know how I can make the buffered URLs execute with the cookie information? Thanks Jon   Odpowiedz

    • Witold Szczerba,

      Hi, once user logs in, the cookie gets attached to every new request issued by the browser, so this is how it all works out. So let's go through the process again: browser sends request, server responds 401, login window slides down, user provides credentials, server accepts credentials and asks browser to apply cookie to every new request, the buffered requests are re-requested again, browser's job is to attach cookie (as required by specs), re-requested messages are being responded, their original callbacks are being executed.   Odpowiedz

  • chief,

    I am just getting into angular, and one of the first things I started thinking about was how you would implement a login / authentication system. Then I saw stuff about using interceptors in the NG API ref, and it all solidified here. Many thanks for this write-up!   Odpowiedz

  • wtk300,

    Jim, please send mail to me. I have some working example which is still improving.   Odpowiedz

  • Jim,

    Does anyone have a working code base for this example? I've got the Spring background, but the AngularJS is new. Getting ready to implement the login form.   Odpowiedz

  • Aleksandar Vidakovic,

    Great! I was banging my head against the wall to find out how to use these interceptors. Thanks for saving me time!   Odpowiedz

  • Luciano Greiner,

    Hello. Very nice solution. Would you mind to share your Spring Security configuration? I am having a hard time trying to configure it not to redirect the request to a new URL or to return 404 instead. Thank you!   Odpowiedz

    • Witold Szczerba,

      Hi, sorry for late response. I will try to extract and publish the Spring configuration, stay tuned :)   Odpowiedz

  • xMort,

    Thanks for great post! It really helped me.   Odpowiedz

  • Lee,

    Where do you implement the loginRequired event?   Odpowiedz

    • Witold Szczerba,

      I have created a login form with a directive. The login form is hidden. Directive is listening on 'event:loginRequired' and once it's received, it: "slides down" the login window, hides everything else (well, excluding background and some decorations).   Odpowiedz

      • jabbett,

        Thanks so much for the interceptor. Could you share your login form directive code as well? Thanks!  

      • Witold Szczerba,

        Hello, I have started a mini project which implements the solution described in that blog. Will upload it to GitHub once it is ready. I hope I will be able to finish it next week...  

  • Rati,

    This is very interesting. I'm really looking forward to your working example as I'm really new to AngularJS.   Odpowiedz

  • Krzysztof Danielewicz,

    Please, could you improve markup of the listing. Having the whole listing in one row table kills an alternative html readers like kindles.   Odpowiedz

    • Witold Szczerba,

      Hi, there is not much I can do with the markup generated by WordPress. Does your device view the mobile or regular version of the page? In either way try the other one. Mobile is very minimalistic, should be fine. I can read the article on a 4 inch android phone.   Odpowiedz

  • Vojta Jína,

    Witold, great article ! You might put the request queue and related logic into a service instead of publishing it into a $rootScope....   Odpowiedz

    • Witold Szczerba,

      You are right, Vojta. Polluting a root scope is not such a great idea, but note that the requests401 array is the only thing attached to scope. Listeners would have to use scope's event system anyway, wouldn't they?   Odpowiedz

  • Igor Minar,

    Awesome! I'm glad that you figured out how to use $q and interceptors. Really nice stuff.   Odpowiedz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.
Wymagane jest wypełnienie pól oznaczonych symbolem *.

*


Poleć znajomemu



* - pola wymagane