Single Sign-On Enterprise Security for Web Applications

 

Paul D. Sheriff
PDSA, Inc.

February 2004

Applies to:
    Microsoft® ASP.NET

Summary: Discover a technique for enabling single sign-on for multiple Web applications. This article also provides sample code to give you a head start creating a robust enterprise security system complete with single sign-on. (31 Printed pages)

Download the code samples for this article. (See Installing the Samples before downloading them.)

Contents

Introduction
Overview of a Single Sign-on Solution
Classes and Pages
Tables
AppLauncher Web Application
Apps Class
Retrieving Tokens in the Web Applications
Enhancing Your Single Sign-on System
Installing the Samples
Conclusion
Related Books

Introduction

If your organization is like most, then you likely have many Web applications supporting your business. Most of these Web applications need to be secure because you do not want users going into any Web application they want. For example, only certain users should be allowed into the executive summary decision support system, and others should only be allowed into the customer information system. There may also be external users for these systems who are not part of your Microsoft® Windows® domain. You can control this through a security mechanism that forces each user to log on to the system. The problem is the user is constantly forced to log on to each different system. In an organization with even five Web applications, the user who is allowed to access each one will get pretty tired of constantly logging on and off. There must be a better way!

In this article you will learn one method of employing a single sign-on solution across your enterprise. Your organization's internal users will be able to use Windows authentication to the applications, while external users will be forced to log on (see figure 1).

Topics Covered in This Article

Single sign-on for Web applications

  • Generating a single sign-on token
  • Mapping users to applications
  • Forms-based authentication
  • Windows integrated authentication

Overview of a Single Sign-on Solution

Looking at figure 1 you can see a series of steps that both internal and external users will go through to log on to a Web site that is hosted on a corporation's Intranet. From the 50,000 foot level there are just four components to this system: a Windows authenticated Web site, Active Directory, a database server, and one or more forms-based authentication Web sites.

ms972971.singlesignon_01(en-us,MSDN.10).gif

Figure 1. An example of a single sign-on solution spanning internal and external users

The Intranet Solution

Starting from the upper left hand corner of figure 1 you find an internal user brings up a browser and navigates to a specific Web site (Step 1). This Web site verifies (Step 2) the user against their Windows credentials (through Active Directory). If the user is a valid Windows user, he is allowed to enter this site. Once he is authenticated, the user's identity is retrieved and a call is made to a database table (Step 3) that contains a list of applications that the specified user can run. These applications are displayed in a DataGrid for the user to select.

The user will click on the application he wants to run and a unique, one-time use token is generated (Step 4). This token and the user's identity will be stored in another database table. The token is then passed to a special page (via the query line) in the Web application the user wants to run. This special page will read this token from the query line and verify the token exists in the database table (Step 5). If the token exists, it will retrieve the login id from the database and delete the record where this token was stored. This prohibits anyone else from reusing this token and sends the login id back to the Web application.

Now that you have the user's identity within this Web application, you need to generate an ASP.NET forms authentication ticket since all Web applications in your intranet will use forms-based authentication. This ticket will then be used by the user who is navigating through all of the secure pages on this site.

The Extranet Solution

External users (people not in your domain) who want to come into your Web application are directed to a different start page than internal users (see figure 2). The Web application that both internal and external users are directing to is secured with forms-based authentication. When an external user attempts to navigate to any page within this Web application they will be automatically redirected to a login page. This login page is a different page from the page the internal users are authenticated by. The user must enter their credentials and a call to the same database will be made to determine if the user is valid for this application. If they are then the normal forms-based authentication cookie will be generated for this user's session.

Classes and Pages

There are a few classes and Web pages you will need to create to support the enterprise security system. Figure 2 shows each class and Web page you will need to write for this system. Each class shown in this figure is discussed later in this article. Following this figure, each class is listed with a description of what each class or Web page's main function is within your system.

ms972971.singlesignon_02(en-us,MSDN.10).gif

Figure 2. Just a few classes and pages are needed to develop your single sign-on solution

Apps Class

This class is responsible for retrieving the list of applications for a specified user. This class will also generate a new token, retrieve the user's identity information given a specified token, and delete a token.

Table 1. Methods of the Apps class

Method Name Description
GetAppsByLoginID Given a user's domain login id, it will look up the applications associated with this user and return a DataSet of the applications.
CreateLoginToken Creates a new login token and returns this token.
GenerateToken Method to generate the new token. In this version, a simple GUID is used as the token.
VerifyLoginToken Given a token, this method will verify that it exists in the database. It creates an instance of the AppToken class, fills in the appropriate information and returns this new object.
DeleteToken Removes the token from table.

AppToken Class

This class contains four properties that are needed to return token information from the VerifyLoginToken method in the Apps class. Table 2 describes the four properties in this class.

Table 2. Properties of the AppToken class

Property Description
LoginID A string value representing the user's login id.
AppName A string value representing the application name to which this AppToken record relates.
LoginKey An integer value that is the primary key for the user in the esUsers table.
AppKey An integer value that is the primary key for the application in the esApps table.

AppUserRoles Class

This class is responsible for retrieving information about the user who is attempting to login to an application. One method will check to see if a login is valid given a login id and an application key. One method will return the set of roles for a given user. One method will return the esUser table's primary key given a login id and an application key.

Default.aspx in the AppLauncher Application

This Web page class will retrieve the Windows user who authenticated to IIS and return the list of applications to which they are allowed access. It will display this list in a DataGrid (figure 3) and allow the user to click on a specific application. Once the user clicks on an application this page will generate a new token, store the token and user id into the esAppToken table, and then call the application passing the token to the AppLogin.aspx page in that Web application.

ms972971.singlesignon_03(en-us,MSDN.10).gif

Figure 3. The application launcher displays a list of applications the logged in user is allowed to run

AppLogin.aspx in Each Web Site

This Web page class is only called from the application launcher. If any other application tries to call this page, it will simply redirect the user to the Default.aspx page of the Web site. Since each Web application uses forms-based authentication, this redirect will force ASP.NET to redirect the user to the Login.aspx page within the site.

If this page is called with a token from the application launcher site, this page will invoke methods on the Apps class to verify that this token is valid. If the token is valid an AppToken object will be returned from the Apps class so this page can use the information within this object to create a forms-based authenticated user.

Login.aspx in Each Web Site

This is a normal login Web page that will ask for user's credentials, check those credentials against the database to ensure they are a valid user, and, if valid, create an authentication ticket and redirect to the page the user was requesting when they entered the site.

Default.aspx in Each Web Site

This is the main landing page in each Web site. This page, and any other page in the site, can only be navigated to if the user has been authenticated through either the AppLogin or Login Web pages.

Tables

There are a few tables you will need to create in your database to support this single sign-on enterprise security system. The tables in this article do not have a lot of fields, but are enough to get the idea across. Figure 4 shows the relationships between each of the tables within the database that you will need to create. Following figure 4 you will find a list of each of these tables, and a description of what each table is used for in this solution.

ms972971.singlesignon_04(en-us,MSDN.10).gif

Figure 4. A few simple tables are all that is needed to implement a single sign-on system complete with roles

esApps

This table contains a list of all of the Web applications in your enterprise. Besides just the name of the application, a long description of the application and the URL to where this application is located is held in this table. The URL is the full URL and should always have the AppLogin.aspx page as the endpoint. The default.aspx page in the Application launcher will take care of adding the "Token=<GeneratedToken>" to the URL prior to redirecting to the Web application.

Click here to see larger image

Figure 5. Sample data for the esApps table

esUsers

This table lists all users who can use your applications. You will need to duplicate your user's domain login id for any internal users. You might also want to add a password field for external users.

ms972971.singlesignon_06(en-us,MSDN.10).gif

Figure 6. Sample data for esUsers table

esAppsUsers

This table relates users in the esUsers table to the applications they can run within the esApps table. The only data within this table are foreign keys to the esUsers table and esApps table.

esAppRoles

This table contains a set of roles for each application. For example, one application may contain the roles "Admin" and "User," and another application may have the roles "User" and "Supervisor."

Click here to see larger image

Figure 7. Sample data for esAppRoles table

esAppUsersRoles

This is list of each role for each user within an application. User "Joe" may be a "Supervisor" in the "HR" application, but may be an "Admin" and "User" in the "Payroll" application.

esAppToken

The esAppToken contains the tokens that are generated and passed from the login portal application to the individual Web applications. Normally these records should only persist about two seconds or less because the Web application that is being called will delete the token as soon as it gathers the information from this table. This prevents anyone from re-using a token.

Click here to see larger image

Figure 8. Sample data for the esAppToken table

AppLauncher Web Application

The application launcher solution (figure 9) is made up of a Windows-authenticated site and a class library project. This Windows-authenticated site will read in the user's domain id directly from the thread and use that to look up the user in the user's table. Let's look at the Web page that displays the application's a user can run.

ms972971.singlesignon_09(en-us,MSDN.10).gif

Figure 9. The AppLauncherxx solution contains the AppLauncherxx project and a reference to the AppLauncherDataxx project

Default.aspx in AppLauncherxx

There is only one Web page needed in the Application Launcher Web site: default.aspx. This Web page will read the user's Windows login id from the User object on the page and load a list of applications defined in the database for this user. Below is the code for the Page_Load event procedure that is called when the default.aspx page is loaded.

// C#
private void Page_Load(object sender, System.EventArgs e)
{
  // Display the User Name
  // Without the Domain prefix
  lblLogin.Text = "Applications Available for: " + 
    Apps.LoginIDNoDomain(User.Identity.Name);

  AppLoad();
}

' VB.NET
Private Sub Page_Load(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles MyBase.Load
  ' Display the User Name
  ' Without the Domain prefix
  lblLogin.Text = "Applications Available for: " & _
    Apps.LoginIDNoDomain(User.Identity.Name)

  AppLoad()
End Sub

The LoginIDNoDomain static method in the Apps class is called to strip out the domain prefix. If a user with a login id of "Ken" is authenticated on a domain called "PDSA," then the User.Identity.Name property will return "PDSA\Ken." This method will simply return the string "Ken."

Loading the Applications This User Can Run

The AppLoad method will use an instance of the Apps class to retrieve a DataSet of applications this user is allowed to run. The method GetAppsByLoginID method in the Apps class will be shown later in this article.

// C#
private void AppLoad()
{
  Apps app = new Apps();

  try
  {
    // Load applications for this user
    grdApps.DataSource =
      app.GetAppsByLoginID(User.Identity.Name);
    grdApps.DataBind();

  }
  catch (Exception ex)
  {
    lblMessage.Text = ex.Message;
  }
}
' VB.NET
Private Sub AppLoad()
  Dim app As New Apps

  Try
    ' Load applications for this user
    grdApps.DataSource = app.GetAppsByLoginID(User.Identity.Name)
    grdApps.DataBind()

  Catch ex As Exception
    lblMessage.Text = ex.Message
  End Try

End Sub

The LinkButton Control

Once the list of applications for the specified user is displayed in the DataGrid control on the default.aspx page (see figure 3), the user may select one of these applications. The control used in the DataGrid to display a hyperlink for the user to click is a LinkButton. This LinkButton is defined as follows on the Web page.

<asp:LinkButton id=lnkApp runat="server" 
AppID='<%# DataBinder.Eval(Container.DataItem, "iAppID") %>' 
UserID='<%# DataBinder.Eval(Container.DataItem, "iUserID") %>' 
CommandArgument='<%# DataBinder.Eval(Container.DataItem, "sURL") %>' 
Text='<%# DataBinder.Eval(Container.DataItem, "sAppName") %>'>
</asp:LinkButton>

As you can see in the above code, there are a couple of added attributes filled in with the primary key from the esApps table (iAppID) and the primary key from the esUsers table (iUserID). These attributes and the URL in the CommandArgument serve to provide enough user profile information to store in the database. These extra attributes will be retrieved in the ItemCommand event procedure you will learn about next.

Tip   You may add any attributes to a server control that you wish. ASP.NET will ignore these attributes, but you can retrieve their values using the Attributes property on the server control.

ItemCommand Event

When the user clicks on the LinkButton control in the DataGrid the ItemCommand event procedure is invoked. In this method you will need to create a new instance of an Apps class, retrieve the LinkButton control so you can get at the attributes, and then call the CreateLoginToken method in the Apps class to store this data into the esAppToken table in the database. Finally, after the Token is retrieved from this method, the URL in the CommandArgument property of the LinkButton will then be concatenated together with this Token; a Response.Redirect is then invoked to call the Web application passing in the Token.

// C#
private void grdApps_ItemCommand(object source, 
  System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
  Apps app = new Apps();
  bool redirect = false;
  string token = String.Empty;
  LinkButton lb;

  try
  {
    lb = (LinkButton) e.Item.Cells[0].Controls[1];
    
    // Create a Token for this user/app
    token = app.CreateLoginToken(
      lb.Text, 
      User.Identity.Name, 
      Convert.ToInt32(lb.Attributes["UserID"]), 
      Convert.ToInt32(lb.Attributes["AppID"]));

    redirect = true;
  }
  catch (Exception ex)
  {
    redirect = false;
    lblMessage.Text = ex.Message;
  }

  if (redirect)
  {
    // Redirect to Web application 
    // passing in the generated token

    Response.Redirect(e.CommandArgument.ToString() +
      "?Token=" + token, false);
  }
}
' VB.NET
Private Sub grdApps_ItemCommand(ByVal source As Object, _
  ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) _
  Handles grdApps.ItemCommand
  Dim app As New Apps
  Dim boolRedirect As Boolean
  Dim token As String
  Dim lb As LinkButton

  Try
    lb = DirectCast(e.Item.Cells(0).Controls(1), LinkButton)

    ' Create a Token for this user/app
    token = app.CreateLoginToken(lb.Text, _
      User.Identity.Name, _
      Convert.ToInt32(lb.Attributes("UserID")), _
      Convert.ToInt32(lb.Attributes("AppID")))

    boolRedirect = True

  Catch ex As Exception
    boolRedirect = False
    lblMessage.Text = ex.Message

  End Try

  If boolRedirect Then
    ' Redirect to Web application 
    ' passing in the generated token
    Response.Redirect(e.CommandArgument.ToString() & _
      "?Token=" & token, False)
  End If
End Sub

You will notice in the above code that you retrieve the LinkButton control from the e.Item argument. The e.Item is a reference to the row in the DataGrid that you clicked on. You can retrieve the specific instance of the LinkButton control that you clicked on by going after the column where the LinkButton is located. In this case this is in Cells(0). Within that cell you can grab the control by going after the Controls(1). The reason the control is at location one (1) is the ItemTemplate that is used in the DataGrid to allow us to put a LinkButton into a cell is considered element zero (0).

Once you have retrieved the LinkButton you can then use the Attributes property to retrieve the UserID and AppID that you stored when you set up the LinkButton. The Attributes property is a collection of any additional attributes that you add to the server control that are not part of the original control's definition.

Modify the Web.Config

Before you can authenticate users in a Web application you must set the <authentication> element in the Web.Config file to "Windows." In addition, you must deny anonymous users in the <authorization> element in the Web.config.

<authorization>
  <deny users="?" />
</authorization> 

By setting these two elements, you are forcing the server to retrieve the Windows credentials of the user from the browser. Of course, this is only going to work if you are using Internet Explorer within a domain on which the user has logged in.

One final entry you need to make is to store the connection string for getting at the tables in the database. In the samples for this article, I simply used the <appSettings> section of the Web.config to store the connection string.

<appSettings>
<add key="eSecurityConnectString" 
   value="server=(local);Database=eSecurity;uid=myUserID;pwd=myPassword" />
</appSettings>

In this article, I created a database in SQL Server™ called eSecurity and created all the tables shown earlier. This article's sample code includes a SQL script you can run to create the tables within a SQL Server database. If you are using a different database system, you will need to modify these scripts as appropriate for your database.

Apps Class

Let's now look at the Apps class that performs the majority of the work for this enterprise security system. Each of the three classes in the AppLauncherData assembly take care of loading applications for users, loading roles for users, and working with security tokens. It is a good idea to keep the functionality of interacting with the database and for manipulating tokens out of the user interface tier. This allows you to modify how the tokens are created and how you interact with the database without changing the user interface.

The Apps class is responsible for working with tokens and loading the applications for users. Let's take a look at this class definition.

// C#
public class Apps
{
  string mConnectString;

  public Apps()
  {
    mConnectString = ConfigurationSettings.
      AppSettings["eSecurityConnectString"];
  }

...
}
' VB.NET
Public Class Apps
  Private mConnectString As String

  Public Sub New()
    mConnectString = ConfigurationSettings. _
      AppSettings("eSecurityConnectString")
  End Sub

...
End Class

As you can see, the first thing this class does is load a member variable with a connection string that it retrieves from the Web.config file.

GetAppsByLoginID Method

To load the applications for a specified user you pass the user's login id to the GetAppsByLoginID method. This method is responsible for performing the Join to retrieve all the appropriate information. The SQL Join you use needs to get information from the esApps and esAppsUsers table. However, you also need to join to the esUsers table since you want to retrieve just the application for a specific user and all we have available are their Login ID. Therefore, we must look up the primary key of the user in the esUsers table to join to the other tables.

Note   Dynamic SQL is used in this article only to show concepts. In a real enterprise security system you will want to use stored procedures for all the SQL calls.

// C#
public DataSet GetAppsByLoginID(string loginID)
{
  DataSet ds = new DataSet();
  SqlCommand cmd;
  SqlDataAdapter da;
  string sql;

  sql = "SELECT esApps.iAppID, esAppsUsers.iUserID, ";
  sql += " esApps.sAppName, esApps.sDesc, esApps.sURL ";
  sql += " FROM esApps";
  sql += " INNER JOIN esAppsUsers ";
  sql += " ON esApps.iAppID = esAppsUsers.iAppID ";
  sql += " INNER JOIN esUsers ";
  sql += " ON esAppsUsers.iUserID = esUsers.iUserID ";
  sql += " WHERE sLoginID = @sLoginID ";
  sql = String.Format(sql, Apps.LoginIDNoDomain(loginID));

  try
  {
    cmd = new SqlCommand(sql);
    cmd.Parameters.Add(new 
      SqlParameter("@sLoginID", SqlDbType.Char));
    cmd.Parameters["@sLoginID"].Value = 
      Apps.LoginIDNoDomain(loginID);
    cmd.Connection = new SqlConnection(mConnectString);

    da = new SqlDataAdapter(cmd);

    da.Fill(ds);

    return ds;
  }
  catch (Exception ex)
  {
    throw ex;
  }
}

' VB.NET
Public Function GetAppsByLoginID(ByVal LoginID As String) _
  As DataSet
  Dim ds As New DataSet
  Dim cmd As SqlCommand
  Dim da As SqlDataAdapter
  Dim sql As String

  sql = "SELECT esApps.iAppID, esAppsUsers.iUserID, "
  sql &= " esApps.sAppName, esApps.sDesc, esApps.sURL "
  sql &= " FROM esApps"
  sql &= " INNER JOIN esAppsUsers "
  sql &= " ON esApps.iAppID = esAppsUsers.iAppID "
  sql &= " INNER JOIN esUsers "
  sql &= " ON esAppsUsers.iUserID = esUsers.iUserID "
  sql &= " WHERE sLoginID = @sLoginID "
  sql = String.Format(sql, Apps.LoginIDNoDomain(LoginID))

  Try
    cmd = New SqlCommand(sql)
    cmd.Parameters.Add(New _
      SqlParameter("@sLoginID", SqlDbType.Char))
    cmd.Parameters("@sLoginID").Value = _
      Apps.LoginIDNoDomain(LoginID)
    cmd.Connection = New SqlConnection(mConnectString)

    da = New SqlDataAdapter(cmd)
    da.Fill(ds)

    Return ds

  Catch ex As Exception
    Throw ex
  End Try
End Function

The CreateLoginToken Method

Once the user clicks on an application in the DataGrid a new token must be created. The CreateLoginToken method is responsible for performing this task.

// C#
public string CreateLoginToken(string appName, 
  string loginID, int userID, int appID)
{
  SqlCommand cmd = new SqlCommand();
  SqlParameter param;
  string token;
  string sql;

  // Generate a new Token
  token = GenerateToken();

  sql = "INSERT INTO esAppToken(sToken, sAppName, ";
  sql += " sLoginID, iUserID, iAppID, dtCreated) ";
  sql += " VALUES(@sToken, @sAppName, @sLoginID, ";
  sql += "        @iUserID, @iAppID, @dtCreated) ";

  param = new SqlParameter("@sToken", SqlDbType.Char);
  param.Value = token;
  cmd.Parameters.Add(param);

  param = new SqlParameter("@sAppName", SqlDbType.Char);
  param.Value = appName;
  cmd.Parameters.Add(param);

  param = new SqlParameter("@sLoginID", SqlDbType.Char);
  param.Value = Apps.LoginIDNoDomain(loginID);
  cmd.Parameters.Add(param);

  param = new SqlParameter("@iUserID", SqlDbType.Int);
  param.Value = userID;
  cmd.Parameters.Add(param);

  param = new SqlParameter("@iAppID", SqlDbType.Int);
  param.Value = appID;
  cmd.Parameters.Add(param);

  param = new SqlParameter("@dtCreated", SqlDbType.DateTime);
  param.Value = DateTime.Now;
  cmd.Parameters.Add(param);

  try
  {
    cmd.CommandType = CommandType.Text;
    cmd.CommandText = sql;

    cmd.Connection = new SqlConnection(mConnectString);
    cmd.Connection.Open();

    cmd.ExecuteNonQuery();
  }
  catch (Exception ex)
  {
    throw ex;
  }
  finally
  {
    if (cmd.Connection.State != ConnectionState.Closed)
    {
      cmd.Connection.Close();
      cmd.Connection.Dispose();
    }
  }

  return token;
}

' VB.NET
Public Function CreateLoginToken(ByVal AppName As String, _
  ByVal LoginID As String, ByVal UserID As Integer, _
  ByVal AppID As Integer) As String
  Dim cmd As New SqlCommand
  Dim param As SqlParameter
  Dim token As String
  Dim sql As String

  ' Generate a new Token
  token = GenerateToken()

  sql = "INSERT INTO esAppToken(sToken, sAppName, "
  sql &= " sLoginID, iUserID, iAppID, dtCreated) "
  sql &= " VALUES(@sToken, @sAppName, @sLoginID, "
  sql &= " @iUserID, @iAppID, @dtCreated)"
  sql = String.Format(sql, token, AppName, _
    Apps.LoginIDNoDomain(LoginID), UserID, AppID, _
    DateTime.Now.ToString())

  param = New SqlParameter("@sToken", SqlDbType.Char)
  param.Value = token
  cmd.Parameters.Add(param)

  param = New SqlParameter("@sAppName", SqlDbType.Char)
  param.Value = AppName
  cmd.Parameters.Add(param)

  param = New SqlParameter("@sLoginID", SqlDbType.Char)
  param.Value = Apps.LoginIDNoDomain(LoginID)
  cmd.Parameters.Add(param)

  param = New SqlParameter("@iUserID", SqlDbType.Int)
  param.Value = UserID
  cmd.Parameters.Add(param)

  param = New SqlParameter("@iAppID", SqlDbType.Int)
  param.Value = AppID
  cmd.Parameters.Add(param)

  param = New SqlParameter("@dtCreated", SqlDbType.DateTime)
  param.Value = DateTime.Now
  cmd.Parameters.Add(param)

  Try
    cmd.CommandType = CommandType.Text
    cmd.CommandText = sql

    cmd.Connection = New SqlConnection(mConnectString)
    cmd.Connection.Open()

    cmd.ExecuteNonQuery()

  Catch ex As Exception
    Throw ex

  Finally
    If cmd.Connection.State <> ConnectionState.Closed Then
      cmd.Connection.Close()
      cmd.Connection.Dispose()
    End If
  End Try

  Return token
End Function

To create the token, the GenerateToken method is called. The reason this is separated from the CreateLoginToken method is because it would allow you to change what type of token you generate in the future. Some ideas are presented at the end of this article. This method uses the Guid class to generate a new GUID as the token.

// C#
public string GenerateToken()
{
  return System.Guid.NewGuid().ToString();
}

' VB.NET
Public Function GenerateToken() As String
  Return System.Guid.NewGuid().ToString()
End Function

Retrieving Tokens in the Web Applications

To test launching an application from the application launcher you should create a test Web application that can be called from the launcher (see figure 10). This test Web application will use the AppLauncherDataxx project described earlier.

ms972971.singlesignon_10(en-us,MSDN.10).gif

Figure 10. Each Web application will use the AppLauncherDataxx project in combination with an AppLogin, Login, and Default page

Modify the Web.Config

To create a Web application that integrates with the single sign-on system, you first need to modify the Web.config file and create a section <appSettings> to store the connection string to interface with the eSecurity database. You also need to store the application id and application name for those times when external users are coming in. Since you have not created a token in those cases, you need to know which application this is so you can load an AppToken object for use when building roles for a user. You will see how to do that later in this article.

<appSettings>
  <add key="eSecurityConnectString"
    value="server=(local);Database=
           eSecurity;uid=mUserID;pwd=myPassword"></add>
  <add key="eSecurityAppID" value="1"></add>
  <add key="eSecurityAppName" value="Payroll"></add>
</appSettings>

You also need to set the Web application up to use forms-based authentication. To do this, modify the <authentication> element as shown below.

<authentication mode="Forms">
    <forms name="AppTest" loginUrl="Login.aspx" />
</authentication>

Finally you also need to modify the <authorization> element in order to deny anonymous users.

<authorization>
  <deny users="?" />
</authorization> 

The AppLogin.aspx Page

Remember that each application you call from the application launcher must call the AppLogin page and pass to it the token generated. The AppLogin page will verify that this token is correct, retrieve the appropriate information from the esAppToken table, and then delete the record in the esAppToken so it can not be reused.

// C#
private void Page_Load(object sender, System.EventArgs e)
{
   VerifyToken();
}

private void VerifyToken()
{
  Apps app = new Apps();
  AppToken al;

  try
  {
    al = app.VerifyLoginToken(
      Request.QueryString["Token"].ToString());

    if(al.LoginID.Trim() == "")
    {
      // Not a valid login
      // Redirect them to default page 
      // This will put them at the login page
      Response.Redirect("default.aspx");
    }
    else
    {
      // Create a Forms Authentication Cookie
      // Set Forms authentication variables
      FormsAuthentication.Initialize();
      FormsAuthentication.SetAuthCookie(
        al.LoginID.ToString(), false);

      // Set the Application Token Object
      Application["AppToken"] = al;

      // Redirect to Default page
      Response.Redirect("default.aspx");
    }
  }
  catch
  {
    // Redirect them to the login page via the Default page
    Response.Redirect("default.aspx");
  }
}

' VB.NET
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As 
System.EventArgs) Handles MyBase.Load
  VerifyToken()
End Sub

Private Sub VerifyToken()
  Dim app As New Apps
  Dim al As AppToken

  Try
    al = app.VerifyLoginToken( _
      Request.QueryString("Token").ToString())

    If al.LoginID.Trim() = "" Then
      ' Not a valid login
      ' Redirect them to default page 
      ' This will put them at the login page
      Response.Redirect("default.aspx")
    Else
      ' Create a Forms Authentication Cookie
      ' Set Forms authentication variables
      FormsAuthentication.Initialize()
      FormsAuthentication.SetAuthCookie( _
        al.LoginID.ToString(), False)

      ' Set the Application Token Object
      Application("AppToken") = al

      ' Redirect to Default page
      Response.Redirect("default.aspx")
    End If

  Catch
    ' Redirect them to the login page via the Default page
    Response.Redirect("default.aspx")
  End Try
End Sub

In the VerifyLogin method you first check the token to see if it is valid. This is done with a call to the VerifyLoginToken method in the Apps class. This method returns an instance of the AppToken class. If the LoginID property of this class is filled in, then you know you have a valid user. If not, then the token was not valid. In the case of a non-valid token, this method will redirect the user to the default.aspx page in the Web site. Of course, with forms-based authentication turned on, this will force the user to be redirected to the Login.aspx page and they will be asked to log on.

A forms authentication cookie is sent out by calling the FormsAuthentication.Initialize and the FormsAuthentication.SetAuthCookie methods. This causes a memory cookie to be sent to the browser. This cookie is then checked by the ASP.NET runtime each time the user comes back to the site.

The VerifyLoginToken Method

This method takes the generated token passed in via the query line and check to ensure that this token is valid. It goes to the esAppToken table and looks up this token. If the token is found in the table, then all of the values from the table are placed into the individual properties of the AppToken object. This AppToken object is returned from this method.

// C#
public AppToken VerifyLoginToken(string Token)
{
  AppToken al = new AppToken();
  DataSet ds = new DataSet();
  SqlCommand cmd;
  DataRow dr;
  SqlDataAdapter da;
  string sql;

  sql = "SELECT iAppTokenID, sAppName, sLoginID, ";
  sql += " iAppID, iUserID ";
  sql += " FROM esAppToken";
  sql += " WHERE sToken = @sToken ";

  try
  {
    cmd = new SqlCommand(sql);
    cmd.Parameters.Add(new 
      SqlParameter("@sToken", SqlDbType.Char));
    cmd.Parameters["@sToken"].Value = Token;
    cmd.Connection = new SqlConnection(mConnectString);

    da = new SqlDataAdapter(cmd);
    da.Fill(ds);

    if (ds.Tables[0].Rows.Count > 0)
    {
      dr = ds.Tables[0].Rows[0];

      al.LoginID = dr["sLoginID"].ToString();
      al.AppName = dr["sAppName"].ToString();
      al.AppKey = Convert.ToInt32(dr["iAppID"]);
      al.LoginKey = Convert.ToInt32(dr["iUserID"]);

      DeleteToken(Convert.ToInt32(dr["iAppTokenID"]));
    }
  }
  catch (Exception ex)
  {
    throw ex;
  }

  return al;
}

' VB.NET
Public Function VerifyLoginToken(ByVal Token As String) As AppToken
  Dim al As New AppToken
  Dim ds As New DataSet
  Dim cmd As SqlCommand
  Dim dr As DataRow
  Dim da As SqlDataAdapter
  Dim sql As String

  sql = "SELECT iAppTokenID, sAppName, sLoginID, "
  sql &= " iAppID, iUserID "
  sql &= " FROM esAppToken"
  sql &= " WHERE sToken = @sToken "

  Try
    cmd = New SqlCommand(sql)
    cmd.Parameters.Add(New _
      SqlParameter("@sToken", SqlDbType.Char))
    cmd.Parameters("@sToken").Value = Token
    cmd.Connection = New SqlConnection(mConnectString)

    da = New SqlDataAdapter(cmd)

    da.Fill(ds)

    If ds.Tables(0).Rows.Count > 0 Then
      dr = ds.Tables(0).Rows(0)

      al.LoginID = dr("sLoginID").ToString()
      al.AppName = dr("sAppName").ToString()
      al.AppKey = Convert.ToInt32(dr("iAppID"))
      al.LoginKey = Convert.ToInt32(dr("iUserID"))

      DeleteToken(Convert.ToInt32(dr("iAppTokenID")))
    End If

  Catch ex As Exception
    Throw ex
  End Try

  Return al
End Function

Role Based Security

After the user is verified to the Web application, he is redirected to the default.aspx page. Since you sent an authentication cookie to the browser, this cookie is sent back each time. Before the user is routed to the requested page, the Application_AuthenticateRequest method in the global.asax file is invoked. It is in this method that you can build any roles you want to associate with this user. I will not show you this method here because there are other articles available that describe it. You can look at the sample code to see an example of how this works. The code for role-based security is very simple. You will most likely enhance the code to use caching, but it gives you an idea of where to start.

Enhancing Your Single Sign-on System

This article has described a great deal about how to create an enterprise security system. However, it is beyond the scope of this article to describe all aspects of creating an enterprise security system. For example, you will need to add a series of maintenance screens to allow an administrator to add users and map users to applications. This set of screens would also need to be secured so only users within a certain role would be allowed into them.

Another enhancement you might add to this system is the ability to automatically add a domain user who is not already in the system. This would help you add users to the system without having to enter all users by hand; or you might write an application to add users. You would most likely want to assign these users to a default role so they could get into certain applications but not others. In addition, an administrator might be informed via an e-mail message when a new user is added in this way.

Since this security system relies on a specific token being created and the Web application being called immediately after this creation, there could be an issue if the Web application does not respond to the request to start. If this happens, then the token is left in the database, which poses a potential security risk. You might want to create a scheduled job to delete tokens that are older than a specified number of minutes. Or you could create your own specific token that is made up of both a token and the time the token was created. Then you just need to modify the VerifyLoginToken method to check the time to make sure it was not greater than the specified number of minutes.

The token used in this article was a GUID, which forces you to use a call back to the database to retrieve a user's profile information. A nice enhancement to this system would be to use a token based on the WS-Security enhancement standard to encrypt the profile information and simply pass this as the token from the application launcher to each Web application. This would eliminate the round-trip to the database each time.

Of course, you will want to change all of the dynamic SQL calls in the code to use stored procedures and command objects with parameters to avoid any chance of SQL injection attacks and to fully secure these tables. You should use stored procedures and parameters on command objects.

Installing the Samples

There are two sample applications that are built for this article. They are each Web applications. There is also a class library included in the solutions for each of the Web applications. The samples are written in both Microsoft Visual Basic® .NET and C#, so you can choose which version you want to use. There are .SQL files included in the solution file to help you create the appropriate tables in a database. The .SQL files are targeted to SQL Server, but could be modified to work with other database systems fairly easily. Here are the steps you should perform to install the sample applications.

  1. Create a database in your DBMS called eSecurity.
  2. Execute the .SQL files to create the tables in the eSecurity database.
  3. Unzip the files in the supplied .ZIP file into a folder.
  4. Create virtual directories to point to each of the folders you want to use. For example, if you are using the VB.NET samples, then create two Virtual Directories called AppLauncherVB and AppTestVB that point to these two folders.
  5. Modify the .SLN files to point to your virtual directory folders.

Conclusion

A single sign-on system for Web applications can help users save time and ease frustrations with having to remember login ids and passwords across your enterprise. In addition, external users can be allowed to use your internal Web applications without having to add these users to your domain. All it takes to implement such a system are a few simple tables and classes. While there is a lot to creating a robust enterprise security system complete with single sign-on, the samples presented in this article will give you a good head start.

About the Author

Paul D. Sheriff is President of PDSA, Inc. (http://www.pdsa.com/products), a Microsoft consulting company and partner providing .NET consulting, products, and services, including an SDLC document and architectural framework. Paul is the Microsoft Regional Director for Southern California. His .NET books include ASP.NET Developer's Jumpstart (Addison-Wesley) and several eBooks listed on PDSA's Web site. You can contact Paul directly at PSheriff@pdsa.com.

© Microsoft Corporation. All rights reserved.