I've been struggling to get Project Auriga to set HTTP Auth from a nice pretty login form, and think I have it working.
What follows is a very technical discussion--if you're a business reader, you should probably skip this post...
HTTP Auth is a specific mechanism for handling authentication. HTTP Auth is built into Apache and IIS, and so the server can handle authentication purely through configuration, offering many different back ends for storing the data. Browsers also handle HTTP Auth natively, popping up a normal login box whenever it gets a Basic Authentication request from the server. But this login box is ugly, and doesn't provide a friendly experience to allow people to create an account, get a password resent, or anything--it falls back to a basic error page. You can, of course, customize the error page, but not necessarily help people with the password login itself.
There are several benefits to using HTTP Auth, though. First of all, other applications on the same server can accept the same credentials, allowing you to sign in once and access multiple applications without having to log into each one. Secondly, you can set up stronger authentication methods, such as client-side certificates. Also, you can configure the server to protect large parts of a web site very easily, reducing exposure to information disclosure.
So how do you make a sign-in form on a web application set http auth? Browsers do not allow you to access these settings via script. You can use an XmlHttpRequest object to set authentication, but only after the proper challenge has been sent from the server. The biggest problem is, if the server sends this challenge twice in a row, your browser will intercept the second request and pop up the ugly password prompt. So designing a form that keeps this login prompt from popping up under most circumstances is quite the challenge.
The gist of the issue is that while you can open an XmlHttpRequest object with a user and password for http authentication, the browser will only actually use those credentials after the server has rejected a request. The process looks like this:
- Your script creates and sends an XmlHttpRequest with http auth username and password.
- The browser submits the request to the server, without sending the username and password.
- The server responds with 401 requires authentication, and a WWW-Authenticate header specifying a realm.
- The browser looks in its cache to see if it already has http auth set for that domain and realm. If it does, it sends those credentials, NOT THE ONES you specified in your XmlHttpRequest. If it does not have those credentials, only then will it set http auth to what your script asked for.
- The server responds. Generally, if the username or password are incorrect, the server will repeat the 401 response, and WWW-Authenticate.
- The browser gets its second 401 in a row, and pops up its password box. Your script never gets a chance to intercept this. So if the stored http auth credentials are wrong, or the user mistypes the password, their browser takes over and you get a password prompt.
How do you handle this situation? It turns out you need to engage in some trickery on both the client and the server.
Here's a basic flow of how you need to handle this, from both the server and the client perspective:
- First, collect the credentials from the user, and create your request as outlined above.
- Browser sends request without credentials.
- Server responds with 401 and WWW-Authenticate.
- Browser sends cached credentials, if they exist, or your credentials if not.
- If credentials are accepted, server allows log in and responds with 200. If credentials are not accepted, server returns an error code OTHER THAN 401, and does not send a WWW-Authenticate:
- We use 403 not authorized for a credential failure here. You might also use 400 Bad Request.
- Because the response was something other than 401, your browser caches the bad credentials.
- XmlHttpRequest status reflects the error condition.
- Your script checks the result for the error your server has returned. Now comes the crucial part:
- Your script submits a new request with different credentials to some server location that will return successfully. For example, we call a login method on our application, passing username "public" and password "?".
- The browser sends the new credentials and submits the request.
- The server returns 200.
- The browser updates its http auth cached credentials with the new bogus ones.
- Now you can present an error to the user, and ask for new credentials.
The key to the above process is that if the browser gets two 401 responses without having a 200 somewhere between, it will pop up its password box and there's nothing you can do about it. So the key is to use a different error code to indicate bad credentials, and do an intervening request that will return 200 so that you can re-authenticate.
You cannot really log out of HTTP Auth. But you can change the credentials to a known bad user. That's a key technique we use to effectively log out of an application, and we re-use this method to reset after bad credentials.
On the server
I'm very much still in development with this. You can see the server side code for Project Auriga logins here.
In this system, we do set a cookie after successful login, to keep from having to check credentials again. This script also allows for cookie-only logins without using http auth. The important bits:
- action=logout: if this is called, the script always returns successfully. This allows the client script to provide new bogus credentials. It passes a username of "public" to log out completely.
- action=httpauth: if this is called, and there are no http auth credentials or the http auth username is "public", return a 401 and WWW-Authenticate. This is always the first request from a browser, and triggers the browser to re-request with the credentials.
- action=httpauth, with http auth username set, and it's not "public": The second or later requests, we never want to return a 401 or the browser will pop up its password prompt. So we return 403 (or 400) if the credentials are bad, or allow the script to continue processing if its good. In this case, our authenticate method returns true if credentials are good, false if the user is not found, and throws an exception if the credentials are bad.
That's basically what you need to do on the server side. Now for the client.
We're using the Dojo Toolkit extensively in Project Auriga, so the login functions are using dojo.xhr* requests to wrap the XmlHttpRequest objects and provide convenient callback functions. You can see our login code here. Key items:
- auriga.login is called by the login form. Note that if this is the first time to this page, the dojo.xhrPost actually happens twice: first time with no credentials, and the second time with them. If the second post is accepted, auriga.login_complete is called. If the second post returns any kind of error, auriga.login_err is called.
- auriga.login_complete is easy... it just redirects to wherever the server response designates.
- auriga.login_err is the real trick here. If it detects the error code we've chosen for bad passwords, it immediately calls the server logout method, to get a good response so the next time the browser gets a 401, it won't immediately pop up the password box.
You can see the code in action on our demo server.
- Actually doing single sign-on is hard. We're trying out different strategies for detecting whether a user already has http auth set, by calling our login method once on page load, but haven't gotten that figured out. In our current script, just clicking Login with the form blank but authenticated elsewhere on the same domain and Realm, will log you in with your existing credentials.
- Because your browser stores credentials based on the domain and the realm together, all applications that you set to share these items must accept the same credentials. If you have a different password on a different system on the same server, you must set a different realm, or logging into one will log you out of the other.
- Using http auth can actually allow users to disable cookies, if your application is RESTful. In Project Auriga, the session login script supports either--the client pages and logins work with either a cookie or http auth. The login process attempts to set http auth and a session cookie. On subsequent attempts, it uses the cookie to avoid re-authenticating every request.
- Finally, a note on security: Basic Authentication provides no protection against passwords being sniffed over the network. If you need a secure login, be sure the server conversation uses SSL--otherwise neighbors on your wireless network can easily sniff out your password. HTTP Auth does not make your application more secure--it just makes it easier to share authentication with other resources on the same server.