Secure login using AJAX

Author: Samuel Williams When: Thursday, 08 October 2009

A website is only as secure as its weakest link. We should assume that an attacker has access to everything that is on the server. To this end, transmitting the password to a server in clear text isn't such a great idea.

It is possible to minimise the chance of a password being intercepted by simply not transmitting it at all, and instead sending a password digest. SHA can be used on the client-side to produce a password digest along with a random nounce to prevent replay attacks.

If the client doesn't have JavaScript, it simply supplies the password (as per what is typical).

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()">
		<legend>Login Form</legend>
			<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>
		<input id="password_hash" name="password_hash" type="hidden" />
<script type="text/javascript">
	function updateLoginHash() {
	        url: "/account/login_salt", 
	        type: 'GET',
	        async: false,
	        cache: false,
	        success: function(login_salt) {
	            password = $.sha1($('#password').val());

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

def secure_digest_b64(key,salt="")
	return Base64.encode64(secure_digest(key, salt)).chomp

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 256).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)
	def digest_authenticate(login_digest, login_salt)
		return false if sha1_hexdigest == nil

		return login_digest == secure_digest(login_salt + sha1_hexdigest)
	def plaintext_authenticate(password)
		return false if sha1_digest == nil
		return secure_digest(password) == sha1_hexdigest
	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
			return nil

Here is a server using Ramaze.

class AccountController < Controller
	set_layout_except :default => [:login_salt]

	def login
		@title = "Mail Administration Login"
			account = MailAccount.with_address(request[:username])
			success = false
			if account "Authenticating account #{}..."

				if request[:password_hash] "\twith #{request[:password_hash]} and #{session[:login_salt]}"
					success = account.digest_authenticate(request[:password_hash], session[:login_salt])
				elsif request[:password] "\twith #{request[:password]}"
					success = account.plaintext_authenticate(request[:password])
			if success "Authentication successful!"
				session[:mail_account] =
				redirect MainController.r(:index)
			Ramaze::Log.warn "Authentication failed!"

	# Generate salt for the login process
	def login_salt
		response["Content-Type"] = 'application/json'
		session[:login_salt] = ::SecureRandom.hex(32)
	def logout
		redirect "/"

Finally, it is important to remember that this approach is not inherently secure. It is just one option to ensure that password does not travel in clear text. I highly recommend this article by Troy Hunt "Our password hashing has no clothes" which discusses the risk of using SHA for password hashing. I personally recommend using BCrypt.


[City], [Country]
Publicly displayed.
Your email won't be displayed.
The following tags are preserved: <pre>, <em> and <a>. All comments are moderated.

Please note, you can leave a comment that uses (limited) XHTML and Textile syntax.