Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Secure AJAX Authentication without SSL

3.13/5 (8 votes)
3 Jan 2012CPOL4 min read 67.3K   1.4K  
This is about creating a secure AJAX login without using SSL.

Introduction

What I try to in this article is to create a login page that authenticate client side (via Ajax requests) and secure the data transmition process form client to server without using SSL.

Secure Socket Layers and Transport Layer Security (SSL/TLS) is the foundation of Web security. Banks, travel booking sites, social networks like Facebook and Twitter, email services and a plethora of other industries built their security based on the fact that it is very hard to crack SSL. SSL encryption protects data in transit from the client to the server. This communication happens very rapidly and the encryption effectively makes a secure tunnel for information.

As using SSL is very efficent and robust way to secure authentication process it's not free (and sometimes expensive). so what if I want to have a secure site and can't spend a lot of money.

This is what we will discuss in this article.

Background

The basic idea in this procedure is to encrypt data in client and send it to server. we use Public-key cryptography for the encrypting and RSA algorithm for this (as SSL/TSL use this algorithm).

the public key will be passed to client. client get the entered username and apssword and encrypt them using the public key and send them to the server (via Ajax request). server (Web service) decrypt recived data with private key and validate username and password and pass the result to client.

so if some hacker sniff all data we passed between client and server, he/she will has these data:

  1. public key
  2. encrypted username and password

and with these data he can't get any useful data (except he bruteforce data to find private key but he should wait about 100 years!)

Using the code

First of all we need an RSA javascript library. I used the library provided by Tom Wu. I combine amnd minify them to create RSA.min.js.

after that we should have a web service to validate username and password. remember that we want to authenticate in client so we shoul add attribute [System.Web.Script.Services.ScriptService] to our web service:

C#
[System.Web.Script.Services.ScriptService]
public class LoginService : WebService
{

    /// <summary>
    /// Gets the public key and add it to cache for 1 day
    /// </summary>
    /// <returns></returns>
    /// <remarks></remarks>
    [WebMethod(CacheDuration = 24 * 60 * 60)]
    public byte[] GetPublicKey()
    {
        RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
        //Add Key Pair (Public + Private Key) to Cache to be used by ValidateUser
        HttpRuntime.Cache["KeyPair"]= rsa.ToXmlString(true);
        RSAParameters param = rsa.ExportParameters(false);

        string keyToSend= ToHexString(param.Exponent) + "," +
             ToHexString(param.Modulus);

        // Encrypting public key to block Man-in-the-Middle attack
        byte[] encrypted;
        using (RijndaelManaged myRijndael = new RijndaelManaged())
        {
            string[] key = File.ReadAllLines(Server.MapPath("~/App_Data/AESKey.txt"));
            myRijndael.Key = Convert.FromBase64String(key[0]);
            myRijndael.IV = Convert.FromBase64String(key[1]);
            ICryptoTransform encryptor = myRijndael.CreateEncryptor();
            using (MemoryStream msEncrypt = new MemoryStream())
            {
                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                    {

                        swEncrypt.Write(keyToSend);
                    }
                    encrypted = msEncrypt.ToArray();
                }
            }
        }
        return encrypted;
    }

    /// <summary>
    /// Validates the user
    /// </summary>
    /// <param name="encUsername">The encrypted username</param>
    /// <param name="encPassword">The encrypted password</param>
    /// <param name="rememberMe">Wheather Remember Me selected by user</param>
    /// <returns></returns>
    /// <remarks></remarks>
    [WebMethod(EnableSession=true)]
    public bool ValidateUser(string encUsername, string encPassword,bool rememberMe)
    {
       // Check request number from this ip is in allowed range
         if(!ActionValidator.IsValid(ActionValidator.ActionTypeEnum.FirstVisit))
             return false;           
      
        //read Key Pair (Public + Private Key) from Cache
        string domainKey = (string)HttpRuntime.Cache["KeyPair"];
        
        RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
        rsa.FromXmlString(domainKey);
        string username = Encoding.UTF8.GetString(rsa.Decrypt(ToHexByte(encUsername), false));
        string password = Encoding.UTF8.GetString(rsa.Decrypt(ToHexByte(encPassword), false));
        if(Membership.ValidateUser(username, password))
        {
            FormsAuthentication.SetAuthCookie(username, rememberMe);
            return true;
        }
        return false;
    }
	
	public static string ToHexString(byte[] byteValue)
	{
		...
	}
	public static byte[] ToHexByte(string str)
	{
		...
	}
}

The first method generates key pair and return Public Key with proper format to client. Also it adds key pair to cache to be used by ValidateUser method (Second method). in WebMethod attribute I assign CacheDuration  to a day; so any user in current day will get the same key from cache and tomorrow we will have a new key.

before returning the public key I encrypt to with AES algorithm to our process of sending key be safe from Man-in-the-Middle attack (as the previous version was vulnerable to it).

Second method get's encrypted username and password an returns result of validating these.

Before I request data from DB I checked that this ip don't exceed the number of allowed request numbers using the way that Omar Alzabir described. I also use his ActionValidator class without change. with this checking it will be safe to Denial-of-Service attack.

Last two methods "ToHexString" and "ToHexByte" are to convert the encrypted data to a format that RSA js library understand.

Now that we have web service, we should create a Login page:

ASP.NET
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login"
    Async="true" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Login</title>

    <script src="Scripts/jquery.js" type="text/javascript"></script>

    <script src="Scripts/RSA.min.js" type="text/javascript"></script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
      ...
    </div>
    </form>

    <script type="text/javascript">
        ....
        function validateUser() {
            var pkey = $('#<%=txtKey.ClientID %>').val().split(',');
            var rsa = new RSAKey();
            rsa.setPublic(pkey[1], pkey[0]);
            var username = rsa.encrypt($('#<%=UserName.ClientID %>').val());
            var pass = rsa.encrypt($('#<%=Password.ClientID %>').val());
            $('.FailureText').css('color', 'blue');
            $('.FailureText div').text('Checking User/Pass....');
            $.ajax({
                type: "POST",
                url: "Services/LoginService.asmx/ValidateUser",
                data: "{'encUsername':'" + username + "','encPassword':'" + pass + "','rememberMe':'" + $('#<%=chbRememberMe.ClientID %>').attr('checked') + "'}",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function(data, status) { OnSuccessLogin(data, status); },
                error: OnErrorLogin
            });
        }
        function OnSuccessLogin(data, status) {

            if (data.d) {
                $('#FailureText').css('color', 'green');
                $('#FailureText div').text('Authentication was successfull. you will redirect to previous page');
                // return to coming page
                var firstPage = getParameterByName("ReturnUrl");
                if (firstPage)
                    window.location.href = firstPage;
            }
            else {
                $('#FailureText').css('color', 'red');
                $('#FailureText div').text('username / password is invalid');
            }
        }
        function OnErrorLogin(request, status, error) {
            $('.FailureText div').text('There is an error in aythenticating process');
        }
        .....

    </script>

</body>
</html>

and in the Page_Load:

C#
protected void Page_Load(object sender, EventArgs e)
    {
        LoginService service = new LoginService();
        byte[] data = service.GetPublicKey();
        using (RijndaelManaged rijAlg = new RijndaelManaged())
        {
            string[] key = File.ReadAllLines(Server.MapPath("~/App_Data/AESKey.txt"));
            rijAlg.Key = Convert.FromBase64String(key[0]);
            rijAlg.IV = Convert.FromBase64String(key[1]);

            // Create a decrytor to perform the stream transform.
            ICryptoTransform decryptor = rijAlg.CreateDecryptor(rijAlg.Key, rijAlg.IV);
            try
            {
                // Create the streams used for decryption.
                using (MemoryStream msDecrypt = new MemoryStream(data))
                {
                    using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                        {

                            // Read the decrypted bytes from the decrypting stream
                            // and place them in a string.
                            txtKey.Text = srDecrypt.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception)
            {
                ScriptManager.RegisterStartupScript(Page,Page.GetType(),"","alert('Public key is invalid.')",true);
            }
            

        }
...
}

In the Page_Load of Login page I get the encrypted public, decrypt it and insert it to a textbox in the page.

Then within javascript block I simply read it from the textbox and encrypt username and password with it. then I call the web service via JSON request to validate them and return the result. also if authentication is successful it redirects page to coming page.

Points of Interest 

Also I used 1024 keypair size because I found it optimum point on security vs performance. you can use smaller keys for better performance or larger keys for more security. I used textbox for keeping key that is unsafe but simpler to implement, you can use hiddenfield (input type="hidden") for safety.

Another thing is that you can seperate javascript from aspx, so the js file will be cachable. and you can get the public key asynchronously for better performance.


History

In this version I resolve the problem of Main-in-The-Middle attack (with special thanks to the kind reviewers that mentioned it), make web service safe of Denial-of-Service attack and make public key safer with changing it daily.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)