You are here:   Blog
Register   |  Login

 

Jul 28

Written by: Michael Washington
7/28/2013 10:33 AM  RssIcon

image

Logging into a website using a small keyboard, such as the one used on a cell phone, can be painful. A username and password that takes a user 5 seconds to enter on a full sized keyboard can take up to a full minute when using a small keyboard on a mobile device. This can make some mobile applications unappealing to the end-users.

Using lsQuickCode

Note: Create an account and use the live version at: https://quickcodes.lightswitchhelpwebsite.com/HTMLClient

image

The first step is to click the Register button.

image

Next, create an account and click Create User.

image

You will be logged in. Click continue to access the site.

 

image

Next, click the Set QuickCode button.

 

image

Enter a code and click the Save QuickCode button.

(if you get a 500 error at this point, you have a device that is not supported)

image

You will be returned to the Home page, Click the Logout button.

image

You can now simply enter the QuickCode in the QuickCode box and click Login to log in.

Note, you can only set one QuickCode per account per device at one time.

 

image

You will be logged into the application.

How lsQuickCode Works

The Database

image

The QuickCodes sample project consists of a LightSwitch HTML Client application with a WCF RIA Service.

image

The application requires a single table called QuickCode consisting of the following fields:

  • Id – Counter field.
  • UserName – One record per user. This field is set as include in Unique Index.
  • QuickCodeString – The code the user enters as their QuickCode.
  • SecretCodeString – A long random string that is generated by the application and stored in the user’s HTML5 storage.
  • LastUpdated – The last day and time the QuickCode was updated.
  • LastIpAddress – The last IP address that the QuickCode was updated from.

 

In addition, the following code is used to restrict a person to only accessing their own QuickCode and to only being allowed to insert or update a QuickCode under their own account:

 

    public partial class ApplicationDataService
    {
        // A user only has access to their own record
        partial void QuickCodes_Filter(ref Expression<Func<QuickCode, bool>> filter)
        {
           filter = e => e.UserName == this.Application.User.Identity.Name;
        }
        // When inserting, always set the UserName to the logged in user
        partial void QuickCodes_Inserting(QuickCode entity)
        {
            entity.UserName = this.Application.User.Identity.Name;
        }
        // When updating, always set the UserName to the logged in user
        partial void QuickCodes_Updating(QuickCode entity)
        {
            entity.UserName = this.Application.User.Identity.Name;
        }
    }

 

The WCF RIA Service

Because we restrict a user to accessing to their own QuickCode, we need a way to validate a QuickCode. For that we use a WCF RIA Service:

namespace WCF_RIA_Project
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server;
    using System.Data.EntityClient;
    using System.Web.Configuration;
    using LightSwitchApplication.Implementation;
    // This class is used to return data from the  WCF RIA Service
    public class QuickCodeRecord
    {
        [Key]
        public int ID { get; set; }
        public string UserName { get; set; }
        public string QuickCodeString { get; set; }
        public string SecretCodeString { get; set; }
    }
    public class WCF_RIA_Service : DomainService
    {
        // This Context property is code that connects to the LightSwitch database
        // The code in the Database connection region can be reused as it is 
        // in your WCF RIA Services using Visual Studio 2012
        #region Database connection
        private ApplicationData m_context;
        public ApplicationData Context
        {
            get
            {
                if (this.m_context == null)
                {
                    string connString = System.Web.Configuration.WebConfigurationManager
                        .ConnectionStrings["_IntrinsicData"].ConnectionString;
                    EntityConnectionStringBuilder builder =
                        new EntityConnectionStringBuilder();
                    builder.Metadata =
                        "res://*/ApplicationData.csdl|res://*/ApplicationData.ssdl|res://*/ApplicationData.msl";
                    builder.Provider =
                        "System.Data.SqlClient";
                    builder.ProviderConnectionString = connString;
                    this.m_context = new ApplicationData(builder.ConnectionString);
                }
                return this.m_context;
            }
        }
        #endregion
        [Query(IsDefault = true)]
        public IQueryable<QuickCodeRecord> GetAllQuickCodeRecord()
        {
            // We must have a default method that takes no parameters
            // In this case we do not want it to actually return any records
            var colQuickCodeRecords = new List<QuickCodeRecord>();
            return colQuickCodeRecords.AsQueryable();
        }
        public QuickCodeRecord GetQuickCodeRecord(string QuickCodeString, string SecretCodeString)
        {
            // Get the requested QuickCode that matches the
            // QuickCodeString and the SecretCodeString
            var objQuickCodeRecord = (from QuickCodes in this.Context.QuickCodes
                                      where QuickCodes.QuickCodeString == QuickCodeString
                                      where QuickCodes.SecretCodeString == SecretCodeString
                                      select new QuickCodeRecord
                                       {
                                           // The  ID
                                           ID = QuickCodes.Id,
                                           // The UserName
                                           UserName = QuickCodes.UserName.ToLower(),
                                           // The QuickCodeString
                                           QuickCodeString = QuickCodes.QuickCodeString,
                                           // The SecretCodeString
                                           SecretCodeString = QuickCodes.SecretCodeString
                                       }).FirstOrDefault();
            return objQuickCodeRecord;
        }
        // Override the Count method in order for paging to work correctly
        protected override int Count<T>(IQueryable<T> query)
        {
            return query.Count();
        }
    }
}

 

The Login Page

image

To see the login code, switch to File View.

image

Here we can see the Login page.

The login page contains the following JavaScript to retrieve the QuickCode, if one is stored, and set a hidden field on the page so it will be passed if the QuickCode login is used:

    <script type="text/javascript">
        $(document).ready(function () {
            // If there is no LocalStorage available
            // Hide QuickCode elements
            if (typeof (Storage) == "undefined") {
                $("#quickcode").hide();
            } else {
                // We have LocalStorage - Try to get QuickCode
                var paramQuickCode = localStorage.getItem('QuickCode');
                if (paramQuickCode !== null) {
                    // We have a QuickCode - Set the hidden field
                    document.getElementById("hfQuickCode").value = paramQuickCode;
                };
            }
        });
    </script>

 

The following code is used to log the user in:

 

  public partial class LogIn : System.Web.UI.Page
    {       
        protected void btnLogin_Click(object sender, EventArgs e)
        {
            // Check for username and password if both are filled in
            if (txtlogin.Text.Trim().Length > 0 
                && txtlogin.Text.Trim().Length > 0)
            {
                // Validate user
                if (Membership.ValidateUser(txtlogin.Text, txtPassword.Text))
                {
                    FormsAuthentication.SetAuthCookie(txtlogin.Text, true);
                    Response.Redirect("HTMLClient/default.htm");
                }
                else
                {
                    lblError.Text = "invalid login";
                }
            }
            else
            {
                // Check for QuickCode if the box is filled in
                if (txtQuickCode.Text.Trim().Length > 0 && hfQuickCode.Value.Trim().Length > 0)
                {
                    bool isAuthenticated = false;
                    string strUserName = "";
                    // This block skips authentication to allow the table to be read
                    // even though the user is not logged in
                    using (ServerApplicationContext context = 
                        ServerApplicationContext.CreateContext(
                        ServerApplicationContextCreationOptions.SkipAuthentication)) 
                    {
                        // Try to get the QuickCode from the WCF RIA Service
                        var objQuickCode = 
                            context.DataWorkspace.WCF_RIA_ServiceData
                            .GetQuickCodeRecord(HashString(txtQuickCode.Text), HashString(hfQuickCode.Value));
                        if (objQuickCode != null)
                        {
                            if (objQuickCode.QuickCodeString != "")
                            {
                                // QuickCode found
                                isAuthenticated = true;
                                strUserName = objQuickCode.UserName;
                            }
                        }
                    }
                    // Log user in or not
                    if (isAuthenticated)
                    {
                        // Log user in
                        FormsAuthentication.SetAuthCookie(strUserName, true);
                        Response.Redirect("HTMLClient/default.htm");
                    }
                    else
                    {
                        lblError.Text = "invalid login";
                    }
                }
            }
        }
        #region HashString
        public static string HashString(string paramString)
        {
            string HashedString =
                FormsAuthentication.HashPasswordForStoringInConfigFile(
                paramString, System.Web.Configuration.FormsAuthPasswordFormat.MD5.ToString());
            return HashedString;
        }
        #endregion
    }

 

Setting the QuickCode

The HTML Client screen uses the following JavaScript code to set the QuickCode:

 

myapp.SetQuickCode.SaveQuickCode_execute = function (screen) {
    // Call SetQuickCode.ashx 
    $.ajax({
        type: 'post',
        data: {
            // Pass the QuickCode entered
            paramQuickCode: screen.QuickCode
        },
        url: '../web/SetQuickCode.ashx',
        success: function success(result) {
            // Parse the JSON returned
            var objQuickCode = jQuery.parseJSON(result);
            // Set the SecretCodeString in localStorage
            localStorage.setItem('QuickCode', objQuickCode.SecretCodeString);
            // Navigate back to Home
            myapp.navigateBack();
        },
        error: function (jqXHR, exception) {
            if (jqXHR.status === 0) {
                alert('Not connect.\n Verify Network.');
            } else if (jqXHR.status == 404) {
                alert('Requested page not found. [404]');
            } else if (jqXHR.status == 500) {
                alert('Internal Server Error [500].');
            } else if (exception === 'parsererror') {
                alert('Requested JSON parse failed.');
            } else if (exception === 'timeout') {
                alert('Time out error.');
            } else if (exception === 'abort') {
                alert('Ajax request aborted.');
            } else {
                alert('Uncaught Error.\n' + jqXHR.responseText);
            }
        }
    });
};

 

This calls the Generic File Handler (SetQuickCode.ashx):

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Web.Security;
using System.Security.Cryptography;
namespace LightSwitchApplication.Web
{
    [Serializable]
    // Class to hold te response object
    public class QuickCode
    {
        public string SecretCodeString { get; set; }
    }
    public class SetQuickCode : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            // Create ServerApplicationContext to contact the LightSwitch back-end
            using (var serverContext = ServerApplicationContext.CreateContext())
            {
                // Get the QuickCode that was passed
                string strQuickCode = Convert.ToString(context.Request.Params["paramQuickCode"]);
                // Get the current user 
                // convert to lowercase so the compare works properly
                string strCurrentUserName = serverContext.Application.User.Name.ToLowerInvariant();
                // Create a SecretCodeString
                string strSecretCodeString = GetRandomString() + DateTime.Now.Ticks.ToString();
                // Try to get the QuickCode
                var objQuickCode = (from QuickCode in serverContext.DataWorkspace.ApplicationData
                                    .QuickCodes.GetQuery().Execute()
                                    where QuickCode.UserName.ToLowerInvariant() == strCurrentUserName
                                    select QuickCode).FirstOrDefault();
                if (objQuickCode == null) // No QuickCode found -- Create new one
                {
                    var varQuickCode = serverContext.DataWorkspace.ApplicationData.QuickCodes.AddNew();
                    varQuickCode.UserName = strCurrentUserName;
                    varQuickCode.QuickCodeString = HashString(strQuickCode);
                    varQuickCode.SecretCodeString = HashString(strSecretCodeString);
                    varQuickCode.LastIpAddress = context.Request.UserHostAddress;
                    varQuickCode.LastUpdated = DateTime.Now;
                    serverContext.DataWorkspace.ApplicationData.SaveChanges();
                }
                else // Update existing QuickCode
                {
                    objQuickCode.QuickCodeString = HashString(strQuickCode);
                    objQuickCode.SecretCodeString = HashString(strSecretCodeString);
                    objQuickCode.LastIpAddress = context.Request.UserHostAddress;
                    objQuickCode.LastUpdated = DateTime.Now;
                    serverContext.DataWorkspace.ApplicationData.SaveChanges();
                }
                // Create response
                QuickCode responseQuickCode = new QuickCode();
                responseQuickCode.SecretCodeString = strSecretCodeString;
                // Create JavaScriptSerializer
                System.Web.Script.Serialization.JavaScriptSerializer jsonSerializer =
                    new System.Web.Script.Serialization.JavaScriptSerializer();
                // Output as JSON
                context.Response.Write(jsonSerializer.Serialize(responseQuickCode));
            }
        }
        #region GetRandomString
        public string GetRandomString()
        {
            byte[] RandomValue = new byte[16];
            RandomNumberGenerator RndGen = RandomNumberGenerator.Create();
            RndGen.GetBytes(RandomValue);
            return Convert.ToBase64String(RandomValue);
        }
        #endregion
        #region HashString
        public static string HashString(string paramString)
        {
            string HashedString = 
                FormsAuthentication.HashPasswordForStoringInConfigFile(
                paramString, System.Web.Configuration.FormsAuthPasswordFormat.MD5.ToString());
            return HashedString;
        }
        #endregion
        #region IsReusable
        public bool IsReusable
        {
            get
            {
                return false;
            }
        } 
        #endregion
    }
}

 

Does Not Work On All Devices

I tested this on multiple devices and the only device I found a problem with was my Kindle Fire HD. I suspect it is because I am using the silk web browser. 

Use At Your Own Risk

Employing a custom authentication method always entails some risk. Do not use this if you do not understand the code and risks involved. No warranty or liability is expressed or implied. Also, You will always want to use SSL (Secure Sockets Layer) with your LightSwitch applications.

Download Code

The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

(you must have Visual Studio 2012 (or higher) installed to run the code)

1 comment(s) so far...


Gravatar

Re: LightSwitch lsQuickCode: Fast Login For Mobile Applications

Smart Idea
Thanks

By abuhelweh on   8/12/2013 3:51 PM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation