Thursday, January 09, 2014

HTTP Basic Authentication in AngularJS

Angular provides lots of support for REST services, so putting an Angular application in front of existing REST services looks easy at first sight. Indeed it is - as long as your REST services are exposed with no security at all.

Recently I changed the security in SimplePVR, which is my spare-time pet project. SimplePVR is a typical Angular application, where the back-end is a Sinatra application exposing both the JavaScript for the Angular application and a set of REST services used by the Angular application.

I used to secure everything in the back-end with HTTP Basic Authentication, with no special handling in the Angular application. This worked well, but had some problems:
  • The user was shown the browser's native log-in dialog, which is not pretty.
  • There was no obvious way to "log out", i.e. clear the HTTP Basic credentials.
  • Some browsers would show the log-in dialog for every single file fetched from the back-end, resulting in a very user-unfriendly experience.
Still, HTTP Basic Authentication is very nice for REST services, and adequately secure as long as you do HTTPS. I wanted to stick with HTTP Basic Authentication but give the user a better experience and a "log out" feature.

I thought that this had been done numerous times before, since Angular has great REST support, and HTTP Basic Authentication is widely used for REST services. It turned out to be much harder than I had anticipated, and hopefully this blog post will be useful to others. Join my odyssey...

About HTTP Basic Authentication

In case you don't know what HTTP Basic Authentication is all about, it works like this: The client starts by requesting a URL. The server looks at the requests, thinks "hey, I need to know who you are", and replies with HTTP status code 401 and the header "WWW-Authenticate" set to

Basic realm="My app"

The realm name can be whatever you want. A back-end can expose several different realms, in case different parts of the application requires different credentials.

The client receives this 401 response, see that it knows the authentication scheme set in the header ("Basic"), and in the case of a browser it will show a default log-in dialog to the user. The client will then retry the request with the "Authorization" header set to something like:

Basic dXNlcjpwYXNzd29yZAo=

where the gibberish part is the Base64-encoded user name and password separated by a colon - the above example is "user:password".

The server will decode the Base64 string and check the credentials. If the user name and password are correct, the real response will be returned. If not, the back-end will send another 401 response.

Since Base64 is just an encoding, not an encryption scheme, HTTP Basic Authentication in practice sends the user name and password in cleartext over the wire. Therefore, you really shouldn't use it for anything sensitive unless you're running HTTPS - just like any other log-in method where you send the user name and password directly to the server at any point.

It's a really nice and simple authentication scheme, and since all command-line utilities and all HTTP libraries out there support HTTP Basic Authentication in some way, it's perfect for REST services.

My initial plan

I had read a blog about handling authentication in AngularJS using promises. It's a great read, and it's a really beautiful use of promises: Register an HTTP interceptor which "catches" 401 errors, put these requests aside and let the application show a log-in dialog. Once the log-in dialog is completed, retry the requests with the new credentials. Since we're using promises, the code sending the original HTTP requests will never know or care about what's happening, even if the user takes hours filling in the log-in dialog.

So my plan was:
  • Secure only the REST services in the back-end, not the JavaScript code.
  • Plug in the aforementioned HTTP Authentication interceptor and handle the notifications sent by this by showing a nice Twitter Bootstrap log-in dialog.
  • Store the credentials somewhere in the client.
  • Implement a "log out" button which deletes these credentials.

Securing just the REST services

This was really easy: I created a Sinatra controller called SecuredController which secured controllers would inherit from. This would use Rack::Auth::Basic, like this:

  use Rack::Auth::Basic, 'SimplePVR' do |username, password|
    username == real_username && password == real_password
  end

The other controllers (those serving the Angular application) would be accessible without authentication.

Plugging in the HTTP Authentication interceptor

This was also quite easy. And I created an Angular directive which would simply show or hide a Twitter Bootstrap modal dialog (oh, so pretty!) according to the events sent by the interceptor:

directive('loginDialog', function() {
   return {
       templateUrl: '/app/templates/loginDialog.html',
       restrict: 'E',
       replace: true,
       controller: CredentialsController,
       link: function(scope, element, attributes, controller) {
           scope.$on('event:auth-loginRequired', function() {
               element.modal('show');
           });

           scope.$on('event:auth-loginConfirmed', function() {
               element.modal('hide');
               scope.credentials.password = '';
           });
       }
   } 
});

The form in the loginDialog.html template would, on submit, call a logIn function from the CredentialsController which would set the correct HTTP Basic Authentication header:

var encodedUserNameAndPassword = encodeBase64(userName + ':' + password);
$http.defaults.headers.common['Authorization'] = 'Basic ' + encodedUserNameAndPassword;

I found a nice Base64 encoding function here - by the way, that is one of the many blogs giving an incomplete solution to this whole very problem.

Storing the credentials

I'm not super proud of it, but I ended up storing the credentials in a cookie. I just stored the "encodedUserNameAndPassword" from above:

$cookieStore.put('basicCredentials', encodedUserNameAndPassword);

I did this because it was easier than checking whether the browser supports localStorage, and then storing it there - Angular comes with built-in Cookie support.

But if you think about it, this does not weaken the security of the system, since encodedUserNameAndPassword is sent in the Authorization header anyway.

One possible improvement would be to make the cookie secure when the back-end is running HTTPS, but I don't see an easy way to do this with Angular. I would definitely have done this for a more critical application.

That was supposed to be it

...but it didn't work. Before the HTTP Authentication interceptor could do anything with the HTTP 401 status code, the user was presented with this dreaded dialog (this is the Firefox version, but other versions are not prettier):

Surfing the web, there seems to be a lot of confusion about how to avoid this when accessing resources secured by HTTP Basic Authentication, but the conclusion is that it's not possible! The browser shows this dialog because it knows HTTP Basic Authentication, and only when the user enters invalid credentials will the calling JavaScript receive the response with status code 401.

So my pretty Twitter Bootstrap dialog would be shown only after the user had entered invalid credentials in the browser's native log-in dialog.

Damnation!

A work-around

Further surfing the web, I found somebody proposing simply changing the authentication method presented by the server when the request has the header "X-Requested-With: XMLHttpRequest", which e.g. JQuery automatically adds to all Ajax requests.

So instead of simply using Rack::Auth::Basic, I had to implement my own authentication in Sinatra, which acts as a normal HTTP Basic Authentication when not called by Ajax, and which presents something else than "Basic" for Ajax requests. I chose "xBasic", but as long as it's something that the browser does not recognise, all is fine.

Angular actually does not set the X-Requested-With header like JQuery does, so I had to insert this configuration line:

$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

It worked!

The REST calls from the Angular application would now actually fail without any native log-in dialog and let our application handle the error. My nice Twitter Bootstrap modal log-in dialog was shown, and the user could enter the credentials. After that, all requests would get the Authentication header set with the entered credentials.

All seemed well, but then I tried first entering wrong credentials, which would make the request retries fail, and then entering the correct credentials. Something weird happened: The new request retries still had the old credentials set in the Authorization header.

I scratched my head quite a lot - why didn't Angular see that I had changed the default headers with this line I showed you previously??

$http.defaults.headers.common['Authorization'] = 'Basic ' + encodedUserNameAndPassword;

Then it dawned on me: I had previously set the default value of the Authorization header with the wrong credentials, and all requests from then on would have this header set. When I was changing the default and the HTTP Authentication interceptor was retrying "old requests", these "old requests" had already got an Authorization header, and so Angular didn't care about the new default value.

So in the HTTP Authentication interceptor, right before the request retry, I had to insert this line of code to make room for the new default value:

delete config.headers['Authorization'];

You can see the altered interceptor service here.

It worked!

It's a pity that this requires changes in the server, as it means that you cannot just put an Angular application in front of an existing REST service without changing it.

But I was happy and thought I had got everything right. The Angular application was accessing the REST back-end perfectly, I had a log-out button, I had my pretty Twitter Bootstrap log-in dialog, the REST services were still directly accessible with HTTP Basic Authentication (which means that command-line tools and my XBMC plug-in worked with no changes).

But... just one more thing...

Sometimes the REST service would serve an image URL, and the URL would be put in a normal image tag. Now the browser was fetching the image from the secured REST service... and this guy was back:

I was in despair for some time - should I choose to expose the images without security, or should I ditch HTTP Basic Authentication altogether?

It occurred to me that I could implement something of a hack in my server-side authentication code: If the request does not specify HTTP Basic Authentication credentials in the standard header, it will look in the aforementioned cookie.

This further adds to what has to be changed in your REST service if you are planning "just" adding an Angular front-end, but all things considered it's still a very simple authentication scheme.

As for the back-end authentication code, here's the test, and here's the implementation.

Fixing a GUI lock-up

A final problem was the modal dialog: Hiding and showing it too quickly didn't correctly clean up the backdrop, resulting in a darker and darker, inaccessible page behind the modal dialog, even when the user eventually entered the right credentials. I ended up delaying the "show" part. Have a look at the code if you care.

32 comments:

  1. What not use btoa('user:pass') ?

    ReplyDelete
    Replies
    1. The original page with the Base64 decoder (http://wemadeyoulook.at/en/blog/implementing-basic-http-authentication-http-requests-angular/) has a comment mentioning that it probably only works on Mozilla and WebKit. The situation might have changed with e.g. newer IEs, but I don't have an IE to test on.

      Delete
  2. Hi,
    Thanks for your great tips. We all appreciate your tips. Keep posting these kind of nice blogs.
    Authentication Services

    ReplyDelete
  3. Hi there.
    I'm trying to do the same. In fact, when WWW-Authenticate is present, the browser will automatically send the Authorization header (as long as the browser is 'alive') for the next pages with the same domain (or context). But when you request the server with "X-Requested-With: XMLHttpRequest", the browser will not automatically add the Authorization header on futur requests, and you have to put it manually, as you did in your example.
    Did you notice the same behaviour ?

    ReplyDelete
  4. Nice post Ole Friis, thanks. You might be interested in how to use HTTP Basic Auth with raw javascript XmlHttpRequest interface. See http://zinoui.com/blog/ajax-basic-authentication

    ReplyDelete
  5. Great post, it influenced a post that I wrote on how to implement your solution in ASP.NET with C#.

    https://goo.gl/4BTtqZ

    ReplyDelete
  6. Angular’s Controllers hold the representation of the knowledge—the traditional model— and also act as the link between the user and the system. While the UI is automatically updated by using bidirectional binding to the $scope, Angular doesn’t provide a convention to represent richer models, other than assigning POJOs and functions to it: Basically, static properties.
    More Information: AngularJS Training in Chennai

    ReplyDelete
  7. Wow, really I am much interested to know our blog content is really good. I regularly watching your blog it is amazing and even I got some information through this only.
    Software Testing Training in Chennai | Best Software Testing Training in Chennai
    angularjs 2 training in chennai | angularjs training institute in chennai

    ReplyDelete
  8. The authentication can be done at both client side (using Angular here) and also at the server side.

    Just like form validation, it is better to be implemented at both frontend and backend sides.

    I work as a trainer at Kamal Technologies and I can be contacted for personal one-to-one trainings too.

    Please visit Angular course syllabus

    ReplyDelete
  9. Hi,
    Thanks for the article. Keep posting more.

    - Priyadharshini,
    Trainer at Kamal Technologies,
    Best React Training Center - Chennai

    ReplyDelete
  10. I have read your blog its very attractive and impressive. I like it your blog.
    Angularjs Development

    ReplyDelete
  11. Wow it is really wonderful and awesome thus it is very much useful for me to understand many concepts and helped me a lot. it is really explainable very well and i got more information from your blog.
    Python training in marathahalli
    AWS Training in chennai
    AWS Training in bangalore

    ReplyDelete
  12. Thank you for this post. Thats all I are able to say. You most absolutely have built this blog website into something speciel. You clearly know what you are working on, youve insured so many corners.thanks

    Blueprism training in marathahalli

    Blueprism training in btm

    Blueprism online training

    ReplyDelete
  13. When I initially commented, I clicked the “Notify me when new comments are added” checkbox and now each time a comment is added I get several emails with the same comment. Is there any way you can remove people from that service? Thanks.

    AWS Interview Questions And Answers

    AWS Training in Chennai | Best AWS Training in Chennai

    AWS Training in Pune | Best Amazon Web Services Training in Pune

    ReplyDelete
  14. Excellent ! I am truly impressed that there is so much about this subject that has been revealed and you did it so nicely
    Thanks

    Anika Digital Media
    seo services
    web design development

    ReplyDelete
  15. Good Post. I like your blog. Thanks for Sharing
    AngularJS Training in Noida

    ReplyDelete
  16. bandar 66 online.dan akan lebih mudah lagi saya akan berikan sedikit tentang permainan nya yang akan kamu nya baca dan kamu nya pahami di dalam agen domino99
    asikqq
    dewaqq
    sumoqq
    interqq
    pionpoker
    bandar ceme terpercaya
    hobiqq
    paito warna terlengkap
    bocoran sgp

    ReplyDelete
  17. Good. I am really impressed with your writing talents and also with the layout on your weblog. Appreciate, Is this a paid subject matter or did you customize it yourself? Either way keep up the nice quality writing, it is rare to peer a nice weblog like this one nowadays. Thank you, check also virtual edge and Top Fintech Webinars to Attend 2020

    ReplyDelete
  18. HDPE Pipe Fittings - Thanks for your marvelous posting! I really enjoyed reading it. you're a great author. I will be sure to bookmark your blog and will come back very soon.

    ReplyDelete
  19. Thank you for sharing your awesome and valuable article this is the best blog for the students they can also learn.


    https://lookobeauty.com/best-interior-designer-in-gurgaon/

    ReplyDelete