In the demo download, CrossSiteAuthentication is the
"single sign-on" authentication site, consisting of a sql database,
an asp.net login page and a master page, two classes, and a web service. Let us
take a closer look at each one of them.
Database
CustomerDB, a sql database, is the backend data storage for
the application. There are two tables in the database, Customer and SiteInfo,
and four stored procedures. The Customer table holds the user contact
information including login credentials. The SiteInfo table specifies style
information for each third party site as well as a stored procedure name for
pulling a complete set of customer data for data transfer.
The four stored procedures are as follows:
·
Customer_Login - Verifies users'
credentials and returns CustomerID and names. More fields may be added as
needed.
·
SiteInfo_GetSiteInfo - Retrieves style
information based on SiteID, such as style sheet name, header image path and
header text, as well as a stored procedure name for data transfer.
·
Site1_GetCustomerInfo - Pulls a complete
set of customer data for the third party #1 site. There is only one select
statement in the stored procedure. However, more statements (with joins and
conditions) can be added based on business requirements. Multiple sql
statements will translate into multiple tables in a DataSet.
·
Site2_GetCustomerInfo - Retrieves data as
the above for third party #2 site.
Classes
There are two classes in the CrossSiteAuthentication
application: AuthenticationAssistant and CryptographyAssistant.
Three methods, as shown in Listing 1, in the
AuthenticationAssistant class are of our interest. The VerifyCredentials method
performs user credential verification and returns the user's identity and name
as a SqlDataReader. The number of fields returned is based on the stored
procedure - Customer_Login. The WellFormReturnUrl method creates a well formed
ReturnUrl by properly appending an encrypted QueryString parameter to the
original ReturnUrl. The RetrieveUserDataSet method pulls back a complete set of
user data for transferring to a third party. The argument siteID determines
what stored procedure to use. Code in each method is self explanatory.
Listing 1
public static SqlDataReader VerifyCredentials(string userName, string password)
{
//confirm credentials upon success, return a single record for this user
return ExecuteDataReader("Customer_Login", new object[]
{
userName, password
}
);
}
//this method retrieves a complete set of user data that a third party app needs
public static DataSet RetrieveUserDataSet(int siteID, string CustomerID)
{
//siteID determines storedproc name.
DataSet ds = ExecuteDataSet(GetDataTransferProc(siteID), new object[]
{
CustomerID
}
);
return ds;
}
//The return url to be used to send user back to a third party site needs to be
//parsed to add an encrypted parameter properly
public static string WellFormReturnUrl(string originalReturnUrl, string
encryptedParameter)
{
string WellFormedUrl = "";
//check if the original return url has parameters attached already
int Position = originalReturnUrl.IndexOf("?");
if (Position != - 1)
{
//? exists. original url has some parameters already, append the
//ecryptedParameter to the end with a "&"
WellFormedUrl = originalReturnUrl + "&EncryptedData=" +
HttpUtility.UrlEncode(encryptedParameter);
}
else
//original url does not have any parameters, append PmaInfo with "?"
{
WellFormedUrl = originalReturnUrl + "?EncryptedData=" +
HttpUtility.UrlEncode(encryptedParameter);
}
return WellFormedUrl;
}
The CryptographyAssistant class does encryption and
decryption. The encryption involves two steps. First of all, the user identity
(in this case, CustomerID), name and other relevant data returned in a
SqlDataReader during the user's credential verification need to be serialized
into one single string. This is performed by the BuildUrlParameterString method
as shown in Listing 2.
Listing 2
public string BuildUrlParameterString(string keyFieldName, SqlDataReader rd)
{
//keyFieldName is the primary key such as userID, CustomerID, contactID etc.
//This has to be included in the SqlDataReader
if (_siteID == null)
throw new Exception("SiteID property is not set.");
bool ContainPrimaryKey = false;
StringBuilder MyString = new StringBuilder();
for (int i = 0; i < rd.FieldCount; i++)
{
if (rd.GetName(i).ToLower() == keyFieldName.ToLower())
{
ContainPrimaryKey = true;
}
MyString.Append(rd.GetName(i));
MyString.Append("=");
MyString.Append(HttpUtility.UrlEncode(rd.GetValue(i).ToString()));
MyString.Append("&");
}
//check to make sure that "CustomerID" field exists
if (!ContainPrimaryKey)
throw new Exception("Primary key field - " + keyFieldName +
" is not returned from database");
//Add an expiration value to expire the string after a certain period of time
// elapsed.
MyString.Append("ExpirationDateTime");
MyString.Append('=');
MyString.Append(HttpUtility.UrlEncode(DateTime.Now.AddMinutes
(_validDurationInMinutes).ToString()));
MyString.Append("&");
//add siteID which is needed to get site information
MyString.Append("SiteID");
MyString.Append('=');
MyString.Append(HttpUtility.UrlEncode(_siteID));
//last item does not need "&"
return MyString.ToString();
}
This method basically loops through all fields in the
SqlDataReader and concatenate field name and value pairs into a single string
in the form of FieldName1=FieldValue1&FieldName2=FieldValue2, and so on.
Since this parameter will be encrypted and appended to the end of a ReturnUrl,
the field value should be UrlEncoded. ExpirationDateTime is included in the
parameter string in order to expire it when a specified period of time elapses.
The ExpirationDateTime is set by adding a few minutes (_validDurationInMinutes)
to the current time. The _validDurationInMinutes has a default value of 2
minutes and can be changed through the exposed property ValidurationInMinutes.
SiteID is also built into the string for later use in a web service call. As
indicated earlier, since user data is passed in as a SqlDataReader, the number
of fields in the string can be adjusted simply by changing the sql statement
for the reader. However, since this is for the purpose of authentication, it is
better to have less data unless more is absolutely required.
The second step is to encrypt the parameter string using
Microsoft Enterprise Library as shown Listing 3.
Listing 3
public string EncryptText(string plainText)
{
if (_securityProvider == null)
throw new Exception("No security provider has been specified");
return Cryptographer.EncryptSymmetric(_securityProvider, plainText);
}
The decryption is a reverse operation of the above. It also
includes two steps. First, an encrypted string is passed into the DecryptText
method to get decrypted. The GetNameValueCollection method then parses the name
and value pairs contained in the decrypted string to create a user
NameValueCollection object which is used in the web service. Decryption is also
done using Microsoft Enterprise Library.
Listing 4
public string DecryptText(string encryptedText)
{
if (_securityProvider == null)
throw new Exception("No security provider has been specified");
return Cryptographer.DecryptSymmetric(_securityProvider, encryptedText);
}
public NameValueCollection GetNameValueCollection(string serializedString)
{
//reverse operation of the BuildUrlParameterString
//Create a NameValueCollection from the serialized parameter string
//which has the form of p1=value1&p2=value2...
NameValueCollection MyCollection = new NameValueCollection();
string[]NameValuePairs = serializedString.Split('&');
for (int i = 0; i < NameValuePairs.Length; i++)
{
string[]NameValue = NameValuePairs[i].Split('=');
MyCollection.Add(NameValue[0], HttpUtility.UrlDecode(NameValue[1]));
}
if (DateTime.Now > Convert.ToDateTime(MyCollection["ExpirationDateTime"]))
throw new Exception("Url has expired. Please log in again.");
return MyCollection;
}
Web Service
AuthenticationService.asmx is a web service for the landing
page of a third party to consume in order to confirm a user’s authentication
status and to transfer user data. Upon receiving the EncryptedData
string (Request.QueryString[“EncryptedData”]), the third party landing page
calls the RetrieveUserDataSet web method (or any other web method as needed)
with the EncryptedData string. The web service utilizes the CryptographyAssistant
class to decrypt the string and to return a user NameValueCollection object
which contains the user identity. It then calls methods in the AuthenticationAssistant
class to retrieve a complete set of user data using the values in the
NameValueCollection object. During this process, if the EncryptedData
is expired or tampered with, the decryption will fail.
The web service provides three web methods, as described
below.
·
RetrieveUserDataSet - Decrypts the EncryptedData passed in by a third party caller. Upon
success, it returns a complete set of user data as a DataSet. Upon failure, it returns
a null and sends back an error messages in the reference parameter
returnMessage.
·
RetrieveUserDataXml - Does the same thing
as the above, but returns a complete set of user data as a serialized xml
string.
·
RetrieveUserID - Decrypts the EncryptedData passed in by a third party caller. Upon
success, returns UserID as a string. Upon failure, returns an empty string and
sends back an error messages in the reference parameter returnMessage. If you
do not need user data for transfer, but need only to confirm a user’s
authentication status, this is the method to call.
The RetrieveUserDataSet and RetriveUserDataXml web methods
return exactly the same data. The reason that two methods are exposed here is
to accommodate non .NET third party applications in which a DataSet is not a
recognized data type. Similarly, an error message is sent back to the caller through
a reference parameter ReturnMessage instead of throwing an exception for the
same reason. Depending on what platform a third party uses, more variation of methods
may be needed to return data in the right formats for a particular third party
application to consume.
Master Page and Login page
Graphic presentation style of the authentication site is
achieved using a Master page, AuthMaster.master. The style is dynamically
applied based on SiteID so that the authentication site looks similar to its
target third party site. In the demo, the style is relatively simple. However,
it does illustrate the methodology for more sophisticated graphic presentation.
There are two style sheets, two header images and two header
texts which match two third party sites in the demo application (actually two
landing pages) respectively. Code Listing 5 is the function that applies styles
in the AuthMaster.master page. The variable TableWidth controls the display
width of the page, and is directly placed on the master page in the HTML table
tag (<Table Width=<%# TableWidth%>>) and, therefore, DataBind()
needs to be called to bind the variable. The rest is either an asp.net server control
(for example, imgHeader and lblHeaderText) or html server control (for example,
MainStyle). Their properties change with SiteID.
Listing 5
private void ApplySiteStyle(int siteID)
{
//retrieve site style information from database
SqlDataReader rd = AuthenticationAssistant.ExecuteDataReader(
"SiteInfo_GetSiteInfo", new object[]
{
siteID
}
);
if (rd.Read())
{
MainStyle.Href = rd["StyleSheetName"].ToString();
imgHeader.ImageUrl = rd["HeaderImagePath"].ToString();
imgHeader.Width = Unit.Pixel((int)rd["HeaderWidth"]);
imgHeader.Height = Unit.Pixel((int)rd["HeaderHeight"]);
lblHeaderText.Text = rd["HeaderText"].ToString();
TableWidth = (imgHeader.Width == 0 ? "800" : imgHeader.Width.ToString());
DataBind();
}
rd = null;
}
The login page basically
does three things during the user authentication process.
·
Calls VerifyCredentials method in the AuthenticationAssistant
class to check user credentials against user data in database, and returns the UserID
and name in a SqlDataReader.
·
Serializes the user information returned during login with the
expiration data time and SiteID, and then encrypts the serialized data string
using the CryptographyAssistant class to create the EncryptedData
QueryString parameter. The expiration time is set to be 1 minute from the
current time, meaning that the EncryptedData will
expire in 1 minute from the time user credentials are verified.
·
Redirect the user to a third party site using a "well
formed" ReturnUrl.
The SiteID and ReturnUrl are requested in the Page_Load event
and kept in two invisible labels, lblSiteID and lblReturnUrl
(Label.Visible=false). Their values are used when the Login button is clicked. The
code in the Login button click event is self explanatory as seen below.
Listing 6
SqlDataReader rd = null;
try
{
rd = AuthenticationAssistant.VerifyCredentials(txtUserName.Text,
txtPassword.Text);
if (rd.Read())
{
NameValueCollectionqlDataReader
CryptographyAssistant ca = new CryptographyAssistant();
ca.ValidDurationInMinutes = 1;
ca.SiteID = (lblSiteID.Text == "" ? null : lblSiteID.Text);
ca.SecurityProvider = ConfigurationManager.AppSettings["SecurityProvider"];
string EncryptedParameter = ca.EncryptText(ca.BuildUrlParameterString(
"CustomerID", rd));
//Append the encrypted string to the end of the ReturnUrl and then redirect
//the user to the third party site
//&EncryptedData=EncryptedParameter
Response.Redirect(AuthenticationAssistant.WellFormReturnUrl
(lblReturnUrl.Text, EncryptedParameter));
}
else
{
lblError.Text = "No user with these credentials has been found.";
}
}
catch (Exception ex)
{
lblError.Text = ex.Message.ToString();
}
finally
{
if (rd != null)
rd = null;
}