On the Go with SSO

Create a Single Sign-On App Using Forms Authentication, HTTP Modules, and SQL Server

asp:Feature

LANGUAGES: VB.NET

ASP.NET VERSIONS: 1.1

On the Go with SSO

Create a Single Sign-On App Using Forms Authentication, HTTP Modules, and SQL Server

By Russell Lum

This article illustrates how you can use ASP.NET s built-in forms authentication to create a Single Sign-On (SSO) application. It uses a SQL Server database to store and retrieve permissions and an HTTP module to catch events between applications. This model can be incorporated into existing applications without any code changes. There are four parts to the project: the database, the SSO site, the HTTP module, and the web.config settings. Let s get started.

Part I: The Database

Create a common database to hold all your users and permissions. Figure 1 shows a simple version of the objects.


Figure 1: A simple database example.

The tables represent the minimum number of objects for the sample. There are a plethora of articles that cover this subject in great detail. You can replace these structures with your own existing model. Take note of the SSOEnabled, ProductionName, and AppDesc fields in the Applications table. These will need to be added to your database if you are going to add SSO to an existing database.

Next, create the structures for the SSO system. Because there is only one session in SSO - but many applications - we only need a single token.


Figure 2: There is only one session in SSO, but many applications.

The token in the AppTokens table consists of the IP address of the user who requested the site and the user s SessionID. Include the UserName, CreatedDate, and SessionEndTime for tracking statistics. The AppTokensDetail table will hold the application ID that the user has requested in his/her session and an active bit to track which application is active at any given time (again, see Figure 2). Next, create a few stored procedures for these tables. We need to be able to perform the following:

  • Add, get, and delete tokens
  • Update token sessions, sign out of sessions based on a token
  • Get SSO applications, get SSO applications by user
  • Get application names by ID

The stored procedures necessary to perform these tasks are in the SQL script included with the sample download accompanying this article (see end of article for details).

And now, on to the next step ... site creation.

Part II: The SSO Site

The SSO site is a central login site that will redirect the user to any requested page in other applications. It also acts as a destination for users providing a launch site to other applications.

Create a Web project and call it SSO. The site consists of the following Web forms: Default.aspx, Login.aspx, NewUser.aspx, and defaultError.aspx.

Create a folder under the SSO root and call it Secure. Add a web.config file to this directory and place the NewUser.aspx page in it. This page will allow anonymous access so new users can be added to the database. Add the settings to allow this in the web.config file using the under an section.

Add the controls to the NewUser.aspx page that allow the user to create a new account. Create a dropdown for the application; a Save button; and user name, first name, last name, password, and password confirmation textboxes. Add the information to the database using a direct call to a stored procedure. The idea is that the user will get a set of read only permissions until an administrator grants more privileges.

Let s wire up some events to our controls. In the click event for the Save button write the information to the database. Encrypt the password using the HashPasswordForStoringInConfigFile method, which produces a one-way hash. Call the AddNewUser method, which calls a stored procedure to insert the hashed password, user name, and application ID to the database. The button save click event is written as shown in Figure 3.

Private Sub btnSave_Click(ByVal sender As System.Object, _

                         ByVal e As System.EventArgs)

                         Handles btnSave.Click

 Dim Result As String

 If Not Page.IsValid Then Exit Sub

 Dim SSOAuth As New SSO.Authentication()

 ' encrypt the password

 Dim passEncrypt As String =

 FormsAuthentication.HashPasswordForStoringInConfigFile( _

  tbPassword.Text.ToLower, "md5")

 ' add the user to the database

 Result = AddNewUser(tbUserName.Text, passEncrypt, _

    ddApplications.SelectedItem.Text)

 ' redirect the user to the login page

 Response.Redirect("../login.aspx" & _

                   QueryStringFromSession())

End Sub

Figure 3: The button save click event.

The users must add a new account for themselves to the specific application before they can log in. The application dropdown list is pulled from the application table and bound to the dropdown list. Add a Cancel button and wire it up with the following code:

Private Sub btnCancel_Click(ByVal sender As System.Object, _

                           ByVal e As System.EventArgs)

                           Handles btnCancel.Click

 Dim querystr As String = QueryStringFromSession()

 If querystr = "" Then

     Response.Redirect("../login.aspx")

 Else

     Response.Redirect("../login.aspx" & querystr)

 End If

End Sub

Note the QueryStringFromSession call. This will be discussed shortly. Add some validator controls to check for required fields and password comparison. Finish it with a validation summary control (see Figure 4).


Figure 4: Create a new account.

On the Login.aspx page we need the ability for a user name, password, and a New Account button (see Figure 5).


Figure 5: The Login.aspx page.

Wire up the New Account button to redirect to the new account page. When the user clicks the Login button, verify: 1) that the user exists; 2) what the user s permissions are; and 3) redirect them to the requested site. When the login page loads, check for a query string. If one is missing, build a query string to the default page url and append it to a redirect back to the login page. Next, store the query string in a session variable. This needs to be done to capture the redirect query string and prevent the user from overtyping the query string and refreshing the page. It also allows restoration of the string to the url if the user visits the new user page. This is where the QueryStringFromSession call from earlier is set, and why it is called in the cancel event. Finally, add some JavaScript to set the focus of the login textbox control.

To wire up the Login button click event, perform a call to the database to validate the user. After the request has been validated, an authentication ticket and token are needed. First, create a login token in the AppTokens table using a stored procedure call. To get the IP address, call a ReadOnly property that returns the string of the requestor s IP address by looping through all of the HostIP s address lists and building a single string of the addresses (see Figure 6).

Private Shared ReadOnly Property GetIPKey() As String

 Get

   Dim IPAddr As IPAddress

   Dim IPKey As String

   ' pull the hostname

   Dim Host As String = Dns.GetHostName

   Try

   ' pull the ip entries

   Dim HostIP As IPHostEntry = _

   Dns.GetHostByName(Dns.GetHostName)

   ' loop through the hostip list as can be wrapped

     For Each IPAddr In HostIP.AddressList

   ' create unique key string

     IPKey += Replace(IPAddr.ToString, ".", "")

   Next

 Catch

   Return "nokey"

 End Try

   Return IPKey

 End Get

End Property

Figure 6: Call a ReadOnly property that builds a single string of the addresses.

The IPKey and application ID are used to validate the user s request against the database. Next, create a FormsAuthenticationTicket object and attach it to the response:

Context.Response.Cookies(cookieName).Value =

 FormsAuthentication.Encrypt(ticket).

Note that the code to create the tickets is located in the HTTPModule, which will be discussed later. Once the ticket is added to the response, redirect the request to its specified url using the FormsAuthentication.RedirectFromLoginPage method. Specify false for the create persistent cookie parameter.

If the user requested to log in to SSO itself, they will be directed to the Default page. This is a simple datalist of applications that acts as a launcher page for all applications. Links to those applications and a visible indicator of the user s access to those applications are displayed below each application image (see Figure 7).


Figure 7: A visible indicator of the user s access to those applications are displayed below each application image.

Finally, the defaultError page is simply the friendly redirect page when errors occur. There are two user controls to simplify the code: an application list control (AppList.ascx) and a header control (Header.ascx). The AppList control displays a list of applications and a tool tip of the descriptions on the login and new user pages. The header control handles the look and feel and provides information about the site requested and site location.

Part III: The HTTP Module

If you have never created an HTTP module, after this section I am sure you will come up with many uses for them. HTTP modules are very powerful and elegant taps into the HTTP pipeline. See Using Application object events (ASP.NET) by Dr. Nitin Paranjape referenced at the end of this article for a good, easy-to-understand explanation of the events that can be trapped. In our case, we want to tap into two events: OnBeginRequest and OnPreRequestHandlerExecute. Let s start with OnBeginRequest.

The biggest problem with an SSO model is determining which application and which user is requesting resources, and if that user has permissions to access those resources. By tapping into the HTTP pipeline, we can push into and pull from the request the information needed to make those determinations. Create a new class project and call it AuthenticationHttpModule. Add the following code block:

Public NotInheritable Class AuthenticationHttpModule : _

 Implements IHttpModule

 Public Sub Init(ByVal application As HttpApplication) _

 Implements IHttpModule

 End Sub

 Public Sub Dispose() Implements IHttpModule.Dispose

 End Sub

End Class

Guess what? You ve just created a skeleton class for an Http module! The following code will trap for our event:

Private Sub OnBeginRequest(ByVal source As Object, _

 ByVal e As EventArgs)

End Sub

Next, let s add our handler to the Init subroutine so it will look like this:

Public Sub Init(ByVal application As HttpApplication) _

 Implements IHttpModule

 AddHandler httpApp.BeginRequest, _

   AddressOf Me.OnBeginRequest

End Sub

We now have an HTTP module and an event handler for the OnBeginRequest event. For the code to know which application is requesting information, we will attach a simple cookie to the request. We want the application ID, the application name, and the requestor s IP address. This information will be read out of the requesting application s web.config file. Our subroutine should now look like Figure 8.

Private Sub OnBeginRequest(ByVal source As Object, _

                          ByVal e As EventArgs)

 httpApp = DirectCast(source, HttpApplication)

 ' get the web config settings

 configs = CType(ConfigurationSettings.GetConfig("WebUIApp"), _

  Specialized.NameValueCollection)

 ' create a new cookie

 Dim cookie As New HttpCookie("AppInfo")

 ' add the applicationid and name to the cookie from the

 ' apps web config

 cookie.Values.Add("AppID", configs.Get("Web.AppID"))

 cookie.Values.Add("AppName", configs.Get("Web.AppName"))

 cookie.Values.Add("IPKey", Me.GetIPKey)

 ' add the cookie to the current request

 httpApp.Request.Cookies.Add(cookie)

End Sub

Figure 8: Get the application ID, the application name, and the requestor s IP address.

GetIPKey is the property, explained earlier, in the Login button click event. The main event we are going to use is the OnPreRequestHandlerExecute. Listing One shows the event handler code.

This event allows us to tap into the requesting application s session and acquire the session ID. The session ID is the unique indicator for the token. The event fires before the default HTTPHandler processes the request, so it is the perfect place to determine if the user is authenticated and what permissions he/she has. Now we need to perform a few checks:

  • If the request path ends with login.aspx and it is a GET request, then we want to sign the user out of any sessions and exit. This forces a login to the SSO site.
  • If the application requested is SSO itself, we exit the routine; if the request type is a POST, we exit out. We only want to authenticate GET requests, not SSO, because there are no permissions that apply directly to the SSO site. Every user has access to the SSO site.
  • If the user is not authenticated or the database token is not valid, then the session will be abandoned and the request will be redirected to the login page.
  • The final check is a validation of the SessionActive database flag in the AppTokensDetail table. If that is not active, then the requested application is different from the previous request and we need to change permissions and the active flag for the new application requested.

If a different application is making the request, we don need another token. Add a new row to the token details and set it to active for the application. However, permissions need to be changed. Changing permissions is important because a user may be an administrator in one application and have read-only access to the other. If you are using a single session object with the same key name, then you must reload the permissions. Changing permissions is accomplished when the application token flag does not match the request s application ID. This model goes under the assumption that the user permissions are stored in a session object. The call to SSOAuth.ClearSessionItems in the OnPreRequestHandlerExecute method loops through all the items in the session object of the request s application. It searches for a session key, such as perm , and will clear out that session. The session will then be filled with the new permission data for the requested application.

The handler assembly is separated into four classes:

1) A token class handles all aspects of the tokens and the interaction with the database.

2) A serializable AppToken class simply holds the token information for the requested application.

3) An authentication class takes care of signing in and out of sessions, authentication tickets, and validation routines.

4) Finally, there is an authentication module class that processes all the requests and implements the IHttpModule interface.

At this point, the user is logging in to SSO and the code creates an authentication ticket. Using forms authentication we redirect them to the requested site. If the user switches to another site, the request then goes into our OnBeginRequest method and we attach the application-specific cookie info from the requestor. Once session state is acquired, it determines if the request is authenticated and has a valid token. We then set up the user s permissions in the session and allow the request to continue to the application, bypassing the login because the authentication ticket is still attached. The last piece is to capture the logoff event. The logoff is handled by the OnPreRequestHandlerExecute (again, see Listing One). The call to SSOAuth.SignOutSession resets all of the active flags for the token detail table, and sets an end session time in the token table.

Part IV: The web.config Settings

The web.config settings will need to be added to any application s config file that will be using this SSO model. The HTTP module uses these settings in the application s web.config file to determine which application is requested. Create a new Web application and call it App1. Set up the forms authentication to point to the SSO site:

 

  loginUrl="/SSO/login.aspx?AppID=1"

  protection="All" timeout="60" />

Notice the loginUrl contains the application ID in the query string; it points to the SSO site and not the application s site. The loginUrl tells forms authentication where to go for the login page. The HTTP module is done; however, we need to tell the applications to use it. So add the httpModules section to the applications config file:

 

  name="AuthenticationHttpModule"/>

We need a section for the module to get the SSO settings from, so add the following:

 

  value="/Login.aspx" />

 

  value="SSOSystem" />

 

 

These settings allow for flexibility within the SSO site. If you use a different page name for the login, launch page, virtual root, or cookie name, then you can adjust these settings. The module reads the connection string settings for the common database out of the request s config file, so add a data access section for those settings:

 

  value="server=(local);Trusted_Connection=true;

  database=common" />

The last set of settings are specific to the application. Add the application name and the application ID. The application name is used to determine if the request is for the SSO site or another site, and the application ID is used after a user has logged into the SSO system for storing and comparing tokens:

 

 

Simply add the configuration settings to the application s web.config file and drop the HTTPHandler assembly into the bin directory on the server. That s all there is to it! The application is now a part of the SSO model. If you want to turn off SSO, then comment out the forms authentication attribute and the httpModules section.

Conclusion

Let s recap. We created simple users, applications, and permissions structures in SQL Server. We created a Web application to create and sign in users. We created an HTTP module to intercept requests from applications and validate the user against a database. Finally, we intercepted the session object of the application and re-populated the permissions for a new request! All this is using the .NET Framework s built-in forms authentication model. The key is to point all sites to a central ticket and trap the incoming requests using the HTTP pipeline.

There are many things you can add to this model. The SSO site could easily be turned into a portal site. The database stored procedures can be turned into a Web service. An encryption class can be added to encrypt and decrypt the cookie data and connection strings as it passes over the wire. The config settings could be stored in the machine.config. The HTTPHandler assembly can be signed and installed in the GAC. A custom authentication scheme using IPrincipal and IIDentity interfaces for users would fit into the HTTPHandler class. A variety of statistics can be captured and reported based on the sessions, users, and IP addresses that are captured with this model.

There is one caveat to the initial statement that no code changes need to be made. To accurately capture session end events, a FormsAuthentication.SignOut call needs to be added to the session end event in the global.asax. The HTTP module cannot pick up the session end event and write to the database if the sign out method is not called. The accompanying sample download contains the database script, module, sample application site, and sample SSO site. Thanks for reading!

References

The sample code referenced in this article is available for download.

Russell Lum is a consultant for Ajilon Consulting in Towson, MD. He has 10 years of experience in software development. He has been architecting, developing, and designing applications with .NET since Beta 1. He has taught classes on ASP.NET and holds an MCSD and MCAD. You can reach him at mailto:[email protected].

Begin Listing One

Private Sub OnPreRequestHandlerExecute(ByVal source As _

 Object, ByVal e As EventArgs)

 Try

   Dim signOut As Boolean

   ' set the local http variable

   Me.httpApp = DirectCast(source, HttpApplication)

   ' pull the appinfo cookie

   Me.appInfo = httpApp.Request.Cookies("AppInfo")

   ' get the data access config settings for

   ' the connection string

   Dim datConfigs As NameValueCollection = _

     CType(ConfigurationSettings.GetConfig("DatAccess"),

     Specialized.NameValueCollection)

   ' signout of session for database tracking if the

   ' login page is requested and it is get request

   If httpApp.Request.Path.ToLower.EndsWith("login.aspx") _

     AndAlso httpApp.Request.RequestType = "GET" Then

       Me.SSOAuth = New Authentication()

   ' sign out of the session in the database

       SSOAuth.SignOutSession(Me.appInfo("AppID"), _

         httpApp.Session.SessionID, _ datConfigs.Get(

         "DataAccess.ConnStringCommon"))

       datConfigs = Nothing

       Exit Sub

    End If

    ' validate that we are not in sso app

    If Me.appInfo("AppName") = "SSO" Then Exit Sub

   ' only validate get requests

     If httpApp.Request.RequestType = "POST" Then Exit Sub

   ' validate that the app is in the db and is active

     Me.SSOAuth = New Authentication(

      httpApp.User.Identity.Name, httpApp.Session,

      appInfo.Item("AppID"), datConfigs.Get(

      "DataAccess.ConnStringCommon"))

     datConfigs = Nothing

   ' if user is not authenticated then exit out

       If Not Me.IsAuthenticated(httpApp.Request) Then

   ' user is not authenticated for this app

       Me.NotAuthenticated()

          Exit Sub

      End If

   ' pull the token

      Dim myToken As AppToken = SSOAuth.ValidateTokenDB(

        appInfo.Item("AppID"), CStr(

        httpApp.User.Identity.Name), httpApp.Session,

        httpApp.Context, 1)

   ' if token is not valid then force logon

       If Not myToken.IsValid Then

   ' user is not authenticated for this app

       Me.NotAuthenticated()

         Exit Sub

   End If

   ' session is not active so activate it

     If myToken.SessionActive = False Then

   ' activate the session - deactivate any others

      SSOAuth.ActivateAppDB(appInfo.Item("AppID"),

        httpApp.Session.SessionID, True)

   ' clear out the session objects

   ' get the new user permissions

   ' replace the session with the new permissions

        SSOAuth.GetUserPermissions(myToken.UserName,

          SSOAuth.ClearSessionItems())

 End If

 Catch

   ' do nothing

 Finally

   SSOAuth = Nothing

 End Try

End Sub

End Listing One

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish