Jul
28
Written by:
Michael Washington
7/28/2013 10:33 AM
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
The first step is to click the Register button.
Next, create an account and click Create User.
You will be logged in. Click continue to access the site.
Next, click the Set QuickCode button.
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)
You will be returned to the Home page, Click the Logout button.
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.
You will be logged into the application.
How lsQuickCode Works
The Database
The QuickCodes sample project consists of a LightSwitch HTML Client application with a WCF RIA Service.
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
To see the login code, switch to File View.
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)
2 comment(s) so far...
Smart Idea Thanks
By abuhelweh on
8/12/2013 3:51 PM
|
Excellent idea, thanks for sharing... i keep improving my LS apps
By JohannQ on
8/28/2016 2:21 PM
|