Secure Login Using Ajax
Author: Samuel Williams When: Thursday, 08 October 2009I often end up writing authentication systems which have to work over an unencrypted protocol. This requires an additional layer of security, which can be done using client side javascript.
It is specifically important to ensure that passwords don't travel in the clear. Using jQuery SHA1 on the client-side to hash the password before it is sent to the server achieves this goal. A random login_hash is fetched from the server every time the password is entered. This is then digested and sent back the server, where it is checked.
If the user doesn't have JavaScript, it simply supplies the password (as per what is typical). We must also keep replay attacks in mind when designing this kind of system.

This diagram shows the basic of a secure hashing login system, which can be easily implemented.
Client Side
Here is a client using jQuery. The login hash is retrieved from the server using AJAX.
<form class="basic" id="login" method="post" action="/account/login" onsubmit="javascript:updateLoginHash()">
<fieldset>
<legend>Login Form</legend>
<dl>
<dt><label for="username">Username:</label></dt>
<dd><input type="text" id="username" name="username" /></dd>
<dt><label for="password">Password:</label></dt>
<dd><input type="password" id="password" name="password" /></dd>
<dd class="footer"><input type="submit" name="Login" /></dd>
</dl>
<input id="password_hash" name="password_hash" type="hidden" />
</fieldset>
</form>
<script type="text/javascript">
function updateLoginHash() {
$.ajax({
url: "/account/login_salt",
type: 'GET',
async: false,
cache: false,
success: function(login_salt) {
password = $.sha1($('#password').val());
$('#password').val("");
$('#password_hash').val($.sha1(login_salt+password));
}
});
}
</script>
Server Side
The database I am using as an example is for email accounts. It is slightly more complicated than a typical example.
require 'digest'
require 'base64'
def secure_digest(key,salt="")
return Digest::SHA1.hexdigest(key+salt)+salt
end
def secure_digest_b64(key,salt="")
return Base64.encode64(secure_digest(key, salt)).chomp
end
class MailAccount
include DataMapper::Resource
property :id, Serial
property :name, String
property :pw_ssha, String
property :pw_sha1, String
def password=(pw)
salt = (0...12).collect{(rand*255).to_i.chr}.join
sha1 = Digest::SHA1.digest(pw)
ssha = Digest::SHA1.digest(pw+salt) + salt
attribute_set(:pw_ssha, "{SSHA}" + Base64.encode64(ssha).chomp)
attribute_set(:pw_sha1, "{SHA1}" + Base64.encode64(sha1).chomp)
end
def digest_authenticate(login_digest, login_salt)
return false if sha1_hexdigest == nil
return login_digest == secure_digest(login_salt + sha1_hexdigest)
end
def plaintext_authenticate(password)
return false if sha1_digest == nil
return secure_digest(password) == sha1_hexdigest
end
def sha1_hexdigest
if pw_sha1.kind_of? String
digest = pw_sha1.sub("{SHA1}", "")
return nil if digest.empty?
return Base64.decode64(digest).unpack("H*").first
else
return nil
end
end
end
Here is a server using Ramaze.
class AccountController < Controller
set_layout_except :default => [:login_salt]
def login
@title = "Mail Administration Login"
if request.post?
account = MailAccount.with_address(request[:username])
success = false
if account
Ramaze::Log.info "Authenticating account #{account.name}..."
if request[:password_hash]
Ramaze::Log.info "\twith #{request[:password_hash]} and #{session[:login_salt]}"
success = account.digest_authenticate(request[:password_hash], session[:login_salt])
elsif request[:password]
Ramaze::Log.info "\twith #{request[:password]}"
success = account.plaintext_authenticate(request[:password])
end
end
if success
Ramaze::Log.info "Authentication successful!"
session[:mail_account] = account.id
redirect MainController.r(:index)
end
Ramaze::Log.warn "Authentication failed!"
end
end
# Generate salt for the login process
def login_salt
response["Content-Type"] = 'application/json'
session[:login_salt] = ::SecureRandom.hex(32)
end
def logout
session.delete(:mail_account)
redirect "/"
end
end
Comments
Please note, you can leave a comment that uses (limited) XHTML and Textile syntax.