Saturday, September 30, 2006

Single Sign On across multiple ASP.NET applications

Machine Key

The machineKey element of the web.config file is used to specify keys for encryption and decryption of forms authentication cookies and view state data, and also when dealing with out of process session state.
The default configuration uses auto generated decryption key and validation key, and SHA1 encryption type.
Using the default configuration, different web applications have different decryption and validation key, since they are randomly generated. To force 2 applications to use the same keys, they must be explicitly defined in the web.config file
A complete description of the machineKey element is available here

Single Sign On

Single Sign On (SSO) is a term used to indicate when a pool of applications need a centralized authentication, so that users login once and access to any application.

Implementing a single sign on is quite simple, and can be done by configuring the applications using the web.config file.

 

A default configuration for forms authentication is defined as follows:

<configuration> 
  ... 
  <system.web> 
    ... 
    <authentication mode="Forms"> 
      <forms name=".cookiename" 
             loginUrl="~/Login.aspx" 
             timeout="30" 
             path="/" /> 
    </authentication> 
    ... 
  </system.web> 
  ... 
</configuration> 

Form Authentication

Forms Authentication uses an authentication ticket stored in either a cookie or embedded in the url.

When used in cookie mode, the c
ookie contains authentication data, encrypted so that data can be read by the application who has created the cookie.

Cookies are associated to 2nd level domains (example.com), and can be accessed from any 3rd level domain (www.example.com, test.example.com, and so on).

where .cookiename, by default, is .ASPXFORMSAUTH.

In order for authentication data to be recognized across multiple applications, each application must be configured to use the same values for cookie name, protection and path attributes. But this isn't enough - in fact, they must also have the same machine key values (see the side box for more info about the machineKey element). These information are used to encrypt the forms authentication cookie, as mentioned in the "Forms Authentication" side box.

Below a sample web.config excerpt which must be added to each application we want single sign on enabled. In this sample, the validationKey and encryptionKey attributes must be replaced with unique values you have to generate for your applications pool.

<configuration> 
  ... 
  <system.web> 
    ... 
    <authentication mode="Forms"> 
      <forms name=".cookiename" 
             loginUrl="~/Login.aspx" 
             timeout="30" 
             path="/" 
      /> 
    </authentication> 
    ... 
    <machineKey 
       validationKey= 
         "F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" 
      encryptionKey= 
         "F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" 
      validation="SHA1" 
    /> 
    ... 
  </system.web> 
  ... 
</configuration> 

Second and third level domains

If the cooperating applications are installed under the same 2nd and 3rd level domain, but on different virtual folders, then no additional code is required.

If applications are installed on different second level domains (www.domain1.com and www.domain2.com), the SSO method described in this article won't work, since cookies cannot be read by applications under different second level domains. For example, if application A in domain1.com issues a cookie, the cookie may be read by A itself and any application hosted under www.domain1.com and any other 3rd level domain (test.domain1.com, beta.domain1.com, and so on). Application B in domain2.com isn't able to read the cookie, since it is hosted under a different second level domain.

If cooperating applications are installed on different third level domains, then we need to add some code in order to make SSO work. The code simply has to add the domain name to the authentication cookie, as outlined below

protected void Login (string strUserName, ...) 
{ 
  ... 
  System.Web.HttpCookie cookie; 

  cookie = FormsAuthentication.GetAuthCookie(strUserName, False); 
  cookie.Domain = "domain1.com"; 
  cookie.Expires = DateTime.Now.AddDays (-1); 
  Response.AppendCookie (cookie); 
  ... 
} 

Cookie Expiration

If different applications set different cookie expirations, the actual expiration value is the one set by the application which issued it. So if application A is configured to set an expiration of 1 hour and application B 2 hours, and the user signs in using application B, then the cookie expiration is set to 2 hours.

Logging out

Usually, in order to log out a user, a call to the Authentication.SignOut() method is used - this isn't enough when using SSO. In order to perform a single sign out, the quickest way is to set the cookie expiration to a past date - this ensures that the cookie won't be used by any application for authentication.

protected void Logout (string strUserName) 
{ 
  System.Web.HttpCookie cookie; 

  cookie = FormsAuthentication.GetAuthCookie(strUserName, false); 
  cookie.Domain = "domain1.com"; 
  cookie.Expires = DateTime.Now.AddDays (-1); 
  Response.AppendCookie (cookie); 
}

Integrating Web Applications

What said so far is valid if applications use the same database to store user profiles. But what if 2 applications use each one their own database?

In this case, the SSO works, but sooner or later one of the applications will throw exceptions due to missing data in its database. If a user registers in application A, once he signs in he can access to application B - but he never registered in application B, so application B doesn't have this user in its database.

This is the case when, for example, we have to integrate 2 existing applications, which already have their own authentication and registration implemented.

To solve this problem, we have 2 choices:

  • modify both application in order to use a single authentication and registration process, and having a shared user profile repository
  • choose one application as the master application, and remove the authentication and registration process from the other application (the slave application)

We'll focus on the second solution.

This method requires that the database used by slave applications is accessible by the master application. This can be achieved by either:

  • Create a single database which holds both application databases. In this case it would be good to use different prefixes for database entities to avoid naming conflicts - this could happen if both databases have a Users table. If we choose mst and slv as prefixes, we should rename the Users table to mst_Users for the master database and slv_Users for the slave database. This requires that we modify the source code and stored procedures.
  • Use 2 different databases, but the master application must be able to access to the slave's database.

Authentication should be performed in the following way:

  • The user accesses to the master application, and signs in
  • The master application verifies the user's credentials
  • The master application verifies whether the logged user is defined in the slave database - if not, accesses to the slave's database and creates the new user
  • The master application calls (if existing) a slave's stored procedure which performs post-authentication processing (such as setting a "logged in" field, inserting a new row in a history table, and so on)
  • The master application generates the SSO cookie

User profile creation on the slave database requires that:

  • the master application is able to access to the slave's database
  • the slave's database exposes a stored procedure which handles user registration (we may need to write it by ourselves)

The second requirement isn't mandatory, since it could also be achieved by using inline SQL - but I usually prefer the stored procedure solution.

Final touch

There would be a few final things to do on the slave application:

  • Removal of all login links
  • Replacement of logout links with a "Back to the Master application" link
  • Replacement of all "User's profile" links to point to the master application user's profile page

These steps ensure that navigation is consistent with integration - we're supposing that all user's info (credentials, profile, user's preferences) are handled by the master application, so we need to modify the slave application accordingly. It is responsibility of the master application to update user's profile in the slave application.

What if different cookies are used?

There may be cases where we want to keep authentication cookies separated from master and slaves applications. In this case we can't share the authentication cookie among cooperating applications.

In this case the solution is to create an authentication cookie for the slave application from within the master application.

The code below creates an authentication cookie from the slave application:

FormsAuthenticationTicket ticket; 
HttpCookie cookie; 
string cookiestr; 

ticket = new FormsAuthenticationTicket( 
                       1, 
                       userId, 
                       DateTime.Now, 
                       DateTime.Now.AddYears (120), 
                       true, 
                       "User Data", 
                       "cookie_path" 
           ); 

cookiestr = FormsAuthentication.Encrypt(ticket); 
cookie = new HttpCookie("cookie_name", cookiestr); 
cookie.Expires = ticket.Expiration; 
cookie.Path = "cookie_path"; 

Response.Cookies.Add(cookie); 

Forms Authentication Ticket

The FormsAuthenticationTicket class is used to create and read the values of a forms authentication cookie identifying an authenticated user.

Forms authentication tickets must be encrypted using the FormsAuthentication.Encrypt() method before being issued as a cookie.

More information about the  FormsAuthenticationTicket class can be found here.

The FormsAuthenticationTicket, as its name says, is a class used to generate authentication tickets (see the side box for more details). The code sample above shows that the following parameters are used to create the ticket:

  • ticket version number
  • user id
  • date and time at which the ticket was generated
  • ticket expiration
  • whether creating a persistent cookie or limited to the current browser's session
  • user specific data to be stored in the ticket (for example, this could be a user class)
  • cookie path

References

MSDN - machineKey element

MSDN - FormsAuthenticationTicket class

Copyright © 2013. All Rights Reserved.