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.
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'];
It worked!
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'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 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.
What not use btoa('user:pass') ?
ReplyDeleteThe 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.
DeleteHi,
ReplyDeleteThanks for your great tips. We all appreciate your tips. Keep posting these kind of nice blogs.
Authentication Services
Hi there.
ReplyDeleteI'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 ?
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
ReplyDeleteGreat post, it influenced a post that I wrote on how to implement your solution in ASP.NET with C#.
ReplyDeletehttps://goo.gl/4BTtqZ
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.
ReplyDeleteMore Information: AngularJS Training in Chennai
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.
ReplyDeleteSoftware Testing Training in Chennai | Best Software Testing Training in Chennai
angularjs 2 training in chennai | angularjs training institute in chennai
The authentication can be done at both client side (using Angular here) and also at the server side.
ReplyDeleteJust 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
Hi,
ReplyDeleteThanks for the article. Keep posting more.
- Priyadharshini,
Trainer at Kamal Technologies,
Best React Training Center - Chennai
I have read your blog its very attractive and impressive. I like it your blog.
ReplyDeleteAngularjs Development
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.
ReplyDeletePython training in marathahalli
AWS Training in chennai
AWS Training in bangalore
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
ReplyDeleteBlueprism training in marathahalli
Blueprism training in btm
Blueprism online training
I simply wanted to write down a quick word to say thanks to you for those wonderful tips and hints you are showing on this site.
ReplyDeleteData science training in tambaram | Data Science training in anna nagar
Data Science training in chennai | Data science training in Bangalore
Data Science training in marathahalli | Data Science training in btm
I have been meaning to write something like this on my website and you have given me an idea. Cheers.
ReplyDeleteangularjs Training in btm
angularjs Training in electronic-city
angularjs online Training
angularjs Training in marathahalli
angularjs interview questions and answers
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.
ReplyDeleteAWS Interview Questions And Answers
AWS Training in Chennai | Best AWS Training in Chennai
AWS Training in Pune | Best Amazon Web Services Training in Pune
Your blog stuffed with lot of latest technologies news. Thanks for refreshing my mind.
ReplyDeleteSelenium Training in Chennai
Best selenium training in chennai
iOS Training in Chennai
Digital Marketing Training in Chennai
.Net coaching centre in chennai
performance testing training in chennai
Hadoop Admin Training in Chennai
Big Data Administrator Training in Chennai
Excellent ! I am truly impressed that there is so much about this subject that has been revealed and you did it so nicely
ReplyDeleteThanks
Anika Digital Media
seo services
web design development
Impressive. Your story always bring hope and new energy. Keep up the good work.
ReplyDeletedevops online training
aws online training
data science with python online training
data science online training
rpa online training
Good Post. I like your blog. Thanks for Sharing
ReplyDeleteAngularJS Training in Noida
ReplyDeletenice course. thanks for sharing this post.
AngularJS Training in Noida
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
ReplyDeleteasikqq
dewaqq
sumoqq
interqq
pionpoker
bandar ceme terpercaya
hobiqq
paito warna terlengkap
bocoran sgp
This blog is very useful and gain knwoeledge.
ReplyDeleteweb design training programs
php training center in chennai
magento developer training
I liked your blog.Thanks for your interest in sharing the information.keep updating.
ReplyDeleteaws Training in Bangalore
python Training in Bangalore
hadoop Training in Bangalore
angular js Training in Bangalore
bigdata analytics Training in Bangalore
python Training in Bangalore
aws Training in Bangalore
Very nice post. thanks for sharing with us.
ReplyDeleteaws Training in Bangalore
python Training in Bangalore
hadoop Training in Bangalore
angular js Training in Bangalore
bigdata analytics Training in Bangalore
python Training in Bangalore
aws Training in Bangalore
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
ReplyDeletecoin haber - koin haber - kripto para haberleri - coin haber - instagram video indir - instagram takipçi satın al - instagram takipçi satın al - tiktok takipçi satın al - instagram takipçi satın al - instagram takipçi satın al - instagram takipçi satın al - instagram takipçi satın al - instagram takipçi satın al - binance güvenilir mi - binance güvenilir mi - binance güvenilir mi - binance güvenilir mi - instagram beğeni satın al - instagram beğeni satın al - google haritalara yer ekleme - btcturk güvenilir mi - binance hesap açma - kuşadası kiralık villa - tiktok izlenme satın al - instagram takipçi satın al - sms onay - paribu sahibi - binance sahibi - btcturk sahibi - paribu ne zaman kuruldu - binance ne zaman kuruldu - btcturk ne zaman kuruldu - youtube izlenme satın al - torrent oyun - google haritalara yer ekleme - altyapısız internet - bedava internet - no deposit bonus forex - erkek spor ayakkabı - tiktok jeton hilesi - tiktok beğeni satın al - microsoft word indir - misli indir - instagram takipçi satın al
ReplyDeleteHDPE 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.
ReplyDeletePERDE MODELLERİ
ReplyDeletesms onay
mobil ödeme bozdurma
NFTNASİLALİNİR.COM
ankara evden eve nakliyat
Trafik Sigortasi
Dedektör
kurma websitesi
aşk kitapları
SMM PANEL
ReplyDeleteSMM PANEL
iş ilanları
instagram takipçi satın al
hirdavatciburada.com
beyazesyateknikservisi.com.tr
SERVİS
Jeton Hilesi
tuzla alarko carrier klima servisi
ReplyDeletetuzla daikin klima servisi
çekmeköy toshiba klima servisi
maltepe lg klima servisi
kadıköy lg klima servisi
maltepe alarko carrier klima servisi
maltepe daikin klima servisi
kadıköy daikin klima servisi
kartal toshiba klima servisi
Thank you for sharing your awesome and valuable article this is the best blog for the students they can also learn.
ReplyDeletehttps://lookobeauty.com/best-interior-designer-in-gurgaon/