
Access, Account, Authentication, Login
Logging in to perform an online exam, Stuart is presented with a standard username-password form. He enters his username and password, but a few seconds later, the form's becomes red, with an error message underneath. He switches off Caps Lock and re-enters his credentials. This time, he's in. The form morphs to show his name and balance, and a new menu appears alongside the form.
Login is a necessary evil - it should be as transparent as possible.
Casual users may not bother to log in if the process interrupts their browsing experience.
Login requires interaction with the server, in order to validate the username and password.
The password should not travel in plain-text, where it could be intercepted while it travels to the server.
Authenticate the user with an “XMLHttpRequest Call” instead of form-based submission, hashing in the browser for improved security. The essence of this pattern is a routine transformation from a submission-based approach to an Ajaxian, ??? interaction style. But as discussed below, there's a very useful, though optional, technique here, which involves Javascript-based hashing, and is specific to the login process.
Conventional authentication usually requires the user and password to be uploaded as a standard form submission. The server usually converts the password to a hash (or "validator") value and checks it against a stored hash value in the database. If they match, the user's in.
There are two problems with this approach. First, flushing the page can be a distraction. It might not take long, but it will usually leave the user in a different context, which discourages people from logging in. Even more troublesome is the stream of pages that ensue when a password must be recovered, especially in this security-conscious era maiden names, last purchase dates, and your favourite pet canary. The other problem relates to security of the transmission; if the password is uploaded as plain-text, there's a risk of interception.
Direct Login addresses the page refresh problem, and optionally the transmission problem too. In the simplest approach, you can implement “Direct Login” by simply sending the username and password in plain-text using an XMLHttpRequest POST to the validation service. Then, the server behaves similar to a conventional server: checks if the password matches using a hash function, and prepares for session management. XMLHttpRequest deals with cookies as with regular form-handling, so this will allow the session to be established in the same way as a conventional form submission. The only difference is the response content: instead of outputting a new HTML page, the server will be outputting an XML or plain-text acknowledgement, as well as any personalised content.
Passing the credentials over with XMLHttpRequest will improve usability, but with the password still being transferred in plain-text, there's still a security threat. Ensuring the whole transaction runs overs HTTPS is always the best measure, as it generally makes it secure from interception. However, many websites don't provide such a facility. Fortunately, there's a compromise that can be used to prevent transmission of plain-text passwords. The technique is, strictly speaking. orthogonal to the Direct Login approach - you could apply it to conventional submitted-based authentication as well. But since it makes heavy use of browser-side processing, it fits nicely with Direct Login.
The trick is to perform hashing in the browser. Javascript is capable and fast enough to transform the password into a hash value, and there are libraries around to implement popular algorithms.
With hashing, the naieve way would be to just hash the password to match what should be in the database. But any interceptor would then be able to perform a replay attack, logging in using the same details. So we need a more sophisticated approach, leading to the following double hashing algorithm.
With double hashing, the server generates a one-time random seed (S) The browser then hashes twice: first, it hashes the password (P) to yield what's hopefully stored on the database (Ha; Hash attempt). But instead of sending that, the browser combines it with the one-time seed to form a new hash (Da; Double-hash attempt). This new hash is sent to the server. The server then pulls out the stored hash (H) from the database and combines it with the original one-time seed (S) to form a new hash, which must match the hash (H) that was uploaded. This works because in both cases, the initial password (P) has been passed through the same two hash functions. In the browser, the user's attempt is passed through a fixed hash function and the result is immediately passed to a new hash function with one-time seed. And in the server, the database already holds the result of hashing the real password using the fixed hash function. As long as the server uses the same seed and algorithm as the browser used to perform a second hashing, the two results should match. The server is also responsible for clearing the one-time seed after a successful login, otherwise an interceptor could log in later on by uploading the same data. Here's a summary of the algorithm:
User visits website.
Server outputs initial page.
Server generates one-time seed (S) and stores it.
Server outputs page, including login form, and with one-time seed embedded somewhere on the page (e.g. in a Javascript variable).
User enters username (U) and password (P).
Browser handles submission.
Browser hashes password (P) using permanent hash function, to arrive at the attempted hash value (Ha) that should be held in the database.
Browser combines attempted hash (Ha) with one-time seed (S) to create one-time, double-hashed, value (Da).
Browser uploads username (U), double-hashed value (Da), and one-time seed (S).
Server authenticates.
Server verifies one-time seed (S) is valid.
Server extracts stored hash for this user (H) and combines it with the seed (S) to get one-time, double-hashed, value (D).
Server compares the double-hashed values (D and Da). If successful, it logs the user in (e.g. creates a new session and outputs a successful response code) and clears the one-time seed (S). If not, it either re-generates a new seed, or decrements a usage counter on the existing seed.
You're going to be hashing in the browser as well as the server, so you'll need a portable algorithm. Two popular standards are MD5 and SHA-1, and both have implementations on Javascript and just about any server-side language you're likely to use.
The double-hashing algorithm hinges on the one-time seed being used once only, and ensuring the user authenticates with the seed that the server provided. There are a few decisions were:
In theory, the seed's lifetime shouldn't matter much since it will only be used once - there's no risk of someone intercepting a successful upload and reusing that data to authenticate. However, you'll probably want to periodically clear any unused seeds, e.g. once a day. More important than lifetime is number of validation attempts - perhaps you only want to allow three login attempts against the same seed. In this case, you'll need to associate a counter with the seed.
The algorithm above requires the seed to be uploaded, but the server could instead track the session with a unique session ID, and use that to look up the most recent seed it sent out. It's probably better to upload the seed in most cases, as it keeps the conversation as stateless as possible. With the seed already having been downloaded in plain-text, there's no significant threat by uploading it back.
The NetVibes portal handles the entire login process without any page refreshes.
The Protopage portal pops up a login box without opening up a new page, though it sends you to a new page after the credentials are submitted.
Treehouse Magazine, has a sidebar with a Login “Microlink”. When clicked, it expands to form a login area, which can in turn morph into a registration area. It also degrades to use standard form submission if Javascript is disabled.
James Dam's Ajax Login presents a standard HTML form (Figure 1.91, “Direct Login Demo”). Submission is disabled and handled instead by callback methods, registered on initialisation:
<form action="post" onsubmit="return false">
<div id="login" class="login">
<label for="username">Username: </label>
<input name="username" id="username" size="20" type="text">
<label for="password">Password: </label>
<input name="password" id="password" size="20" type="password">
<p id="message">Enter your username and password to log in.</p>
</div>
<label for="comments">Comments:</label>
<textarea rows="6" cols="80" id="comments"></textarea>
</form>
As soon as the user signals intent to authenticate, indicated by form field focus, a random, one-time, seed is retrieved from the server, if there isn't already one present. The response comes in two parts: an id for the seed along with the seed itself, and both are saved as Javascript variables. The server can later use the id to retrieve the seed it sent:
function getSeed() {
...
if (!loggedIn && !hasSeed) {
http.open('GET', LOGIN_PREFIX + 'task=getseed', true);
http.onreadystatechange = handleHttpGetSeed;
http.send(null);
}
...
}
function handleHttpGetSeed() {
...
if (http.readyState == NORMAL_STATE) {
results = http.responseText.split('|');
// id is the first element
seed_id = results[0];
// seed is the second element
seed = results[1];
}
...
}
The seed is then used to hash the password upon submission. Notice hex_md5() is used twice; the double-hashing operation.
// validateLogin method: validates a login request
function validateLogin() {
...
// compute the hash of the hash of the password and the seed
hash = hex_md5(hex_md5(password) + seed);
// open the http connection
http.open('GET',
LOGIN_PREFIX +
'task=checklogin&username='+username+'&id='+seed_id+'&hash='+hash, true);
...
}
The server then validates by locating the seed it had previously sent out, and checking if the hash value matches a hash of the seed and the stored password hash. If so, it deletes the seed to ensure it's only used once:
sql = 'SELECT * FROM seeds WHERE id=' . (int)$_GET['id'];
...
if (md5($user_row['password'] . $seed_row['seed']) == $_GET['hash']) {
echo('true|' . $user_row['fullname'']);
...
mysql_query('DELETE FROM s WHERE id=' . (int)$_GET['id']);
}
After calling for validation, the browser receives a response and the form is morphed to show whether login was successful or not.
“Lazy Registration” is focused on first-time registration as well as deferred login, and makes use of Direct Login.
“Host-Proof Hosting” also uses Javascript to perform encryption-related functionality.
The idea comes from James Dam's demo and write-up.
Peter Curran of Close Consultants provided some valuable feedback, helping to clarify the algorithm's explanation.