Secure Login Using Ajax

Author: Samuel Williams When: Thursday, 08 October 2009

I 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

There are currently no comments.
Your Icon:
[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.