Monday, June 23, 2014

Single sign-on with Forms Authentication

Configuration
The configuration showed on the following figure is a sample of how you can set the Forms Authentication attributes with security in mind. You should follow these hints for SSO Forms Auth. First of all, you should have the same settings (see forms element attributes) that are listed below on every site that you want to adhere to SSO.·Name·Protection·PathThe machineKey element might be configured on the machine.config file or on every web.config application file. In the first scenario, you may have the encryption key set to something like this (this is the default setting, albeit useless for this scenario):
<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey= "AutoGenerate,IsolateApps" validation="SHA1"/>

The "IsolateApps" means that a different key will be AutoGenerated for *each* application. You can either remove the isolateApps option (for apps on the same machine) or insert a specific key value for it to use (for apps on different boxes). This last option is the one that is used on following the config sample.

<configuration>
<system.web>
<authentication mode="Forms">
<forms loginUrl="Secure\login.aspx" protection="All" requireSSL="true" timeout="10" name="FormsAuthCookie" path="/FormsAuth" slidingExpiration="true" />
</authentication> <!-- The virtual directory root folder contains general pages.Unauthenticated users can view them and they do not need to be secured with SSL. --><authorization><allow users="*" /> <!-- Allow all users --></authorization>
<machineKey validationKey="C50B…CABE" decryptionKey= "8A9BE8FD67AF6979E7D20198CFEA50DD3D3799C77AF2B72F" validation="SHA1"/> </system.web> <!-- The restricted folder is for authenticated and SSL access only. All pages on the Secure subfolder will be under SSL access. --><location path="Secure" >
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
</configuration>
Note: Check ouy the path attribute. This should be aligned with the app name. If you want to have SSO on every app, just leave the default value "/".
Principal Creation
After gathering the user credentials you will perform the authentication process and after that you will retrieve the user roles if you want to use the .NET role authorization pattern. This implies the creation of an Identity and a Principal object that will contain this data. So on the login page server side and after the auth process you will get the Forms ticket and save there your roles info and may be any other user profile related data (beware of size constrains, less than 4KB).
// Do auth with your preferred auth methodWindowsIdentity identity = WinAccessHelper.LogonUser( UserId, Password ); // Add rolesstring[] roles = WinAccessHelper.Roles( new WindowsPrincipal( identity ) );HttpCookie cookie = FormsAuthentication.GetAuthCookie( UserId.Text, false );FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value); // Store roles inside the Forms cookie.FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(ticket.Version,ticket.Name,ticket.IssueDate,ticket.Expiration,ticket.IsPersistent,String.Join( "|", roles),ticket.CookiePath); cookie.Value = FormsAuthentication.Encrypt(newticket);Context.Response.Cookies.Set(cookie);Response.Redirect( FormsAuthentication.GetRedirectUrl( newticket.Name, newticket.IsPersistent ) );// For different domains, should use the cookie domain//HttpCookie formsCookie = FormsAuthentication.GetAuthCookie( UserId.Text, false );//formsCookie.Domain = "localhost.com";//Response.AppendCookie( formsCookie );//Response.Redirect( FormsAuthentication.GetRedirectUrl( UserId.Text, false ) );//FormsAuthentication.RedirectFromLoginPage( UserId.Text, false );

Principal Retrieving
On each AuthenticateRequest event of every SSO “federated” site you may retrieve your saved user info and create your Principal object and load them onto the User object of the current HttpContext instance. This is accomplished on the following figure.
protected void Application_AuthenticateRequest(Object sender, EventArgs e){if (Context.Request.IsAuthenticated)
{// retrieve user's identity from httpcontext user FormsIdentity ident = (FormsIdentity)Context.User.Identity;
// retrieve roles from the authentication ticket userdata field
string[] arrRoles = ident.Ticket.UserData.Split(new char[] {'|'}); // create principal and attach to user Context.User = new System.Security.Principal.GenericPrincipal(ident, arrRoles);}}

Single Sign-On (Building Real-World Cloud Apps with Windows Azure)

There are many security issues to think about when you’re developing a cloud app, but for this series we'll focus on just one: single sign-on. A question people often ask is this: "I’m primarily building apps for the employees of my company; how do I host these apps in the cloud and still enable them to use the same security model that my employees know and use in the on-premises environment when they’re running apps that are hosted inside the firewall?" One of the ways we enable this scenario is called Windows Azure Active Directory (WAAD). WAAD enables you to make enterprise line-of-business (LOB) apps available over the Internet, and it enables you to make these apps available to business partners as well.

Introduction to WAAD

WAAD provides Active Directory in the cloud. Key features include the following:
  • It integrates with on-premises Active Directory.
  • It enables single sign-on with your apps.
  • It supports open standards such as SAML, WS-Fed, and OAuth 2.0.
  • It supports Enterprise Graph REST API.
Suppose you have an on-premises Windows Server Active Directory environment that you use to enable employees to sign on to Intranet apps:

What WAAD enables you to do is create a directory in the cloud. It’s a free feature and easy to set up.
It can be entirely independent from your on-premises Active Directory; you can put anyone you want in it and authenticate them in Internet apps.
Windows Azure Active Directory
Or you can integrate it with your on-premises AD.
AD and WAAD integration
Now all the employees who can authenticate on-premises can also authenticate over the Internet – without you having to open up a firewall or deploy any new servers in your data center. You can continue to leverage all the existing Active Directory environment that you know and use today to give your internal apps single-sign on capability.
Once you’ve made this connection between AD and WAAD, you can also enable your web apps and your mobile devices to authenticate your employees in the cloud, and you can enable third-party apps, such as Office 365, SalesForce.com, or Google apps, to accept your employees’ credentials. If you're using Office 365, you're already set up with WAAD because Office 365 uses WAAD for authentication and authorization.
3rd party apps
The beauty of this approach is that any time your organization adds or deletes a user, or a user changes a password, you use the same process that you use today in your on-premises environment. All of your on-premises AD changes are automatically propagated to the cloud environment.
If your company is using or moving to Office 365, the good news is that you’ll have WAAD set up automatically because Office 365 uses WAAD for authentication. So you can easily use in your own apps the same authentication that Office 365 uses.

Set up a WAAD tenant

A WAAD directory is called a WAAD tenant, and setting up a tenant is pretty easy. We'll show you how it's done in the Windows Azure Management Portal in order to illustrate the concepts, but of course like the other portal functions you can also do it by using a script or management API.
In the management portal click the Active Directory tab.
WAAD in portal
You automatically have one WAAD tenant for your Windows Azure account, and you can click the Add button at the bottom of the page to create additional directories. You might want one for a test environment and one for production, for example. Think carefully about what you name a new directory. If you use your name for the directory and then you use your name again for one of the users, that can be confusing.
New Directory
The portal has full support for creating, deleting, and managing users within this environment. For example, to add a user go to the Users tab and click the Add Userbutton.
Add User button
Add user dialog
You can create a new user who exists only in this directory, or you can register a Microsoft Account as a user in this directory, or register or a user from another WAAD directory as a user in this directory. (In a real directory, the default domain would be ContosoTest.onmicrosoft.com. You can also use a domain of your own choosing, like contoso.com.)
User types
Add user dialog
You can assign the user to a role.
User profile
And the account is created with a temporary password.
Temporary password
The users you create this way can immediately log in to your web apps using this cloud directory.
What's great for enterprise single sign-on, though, is the Directory Integration tab:
Directory Integration tab
If you enable directory integration, and download a tool, you can sync this cloud directory with your existing on-premises Active Directory that you're already using inside your organization. Then all of the users stored in your directory will show up in this cloud directory. Your cloud apps can now authenticate all of your employees using their existing Active Directory credentials. And all this is free – both the sync tool and WAAD itself.
The tool is a wizard that is easy to use, as you can see from these screen shots. These are not complete instructions, just an example showing you the basic process. For more detailed how-to-do-it information, see the links in the Resources section at the end of the chapter.
WAAD Sync tool configuration wizard
Click Next, and then enter your Windows Azure Active Directory credentials.
WAAD Sync tool configuration wizard
Click Next, and then enter your on-premises AD credentials.
WAAD Sync tool configuration wizard
Click Next, and then indicate if you want to store a hash of your AD passwords in the cloud.
WAAD Sync tool configuration wizard
The password hash that you can store in the cloud is a one-way hash; actual passwords are never stored in WAAD. If you decide against storing hashes in the cloud, you'll have to useActive Directory Federation Services (ADFS). There are alsoother factors to consider when choosing whether or not to use ADFS. The ADFS option requires a few additional configuration steps.
If you choose to store hashes in the cloud, you’re done, and the tool starts synchronizing directories when you click Next.
WAAD Sync tool configuration wizard
And in a few minutes you’re done.
WAAD Sync tool configuration wizard
You only have to run this on one domain controller in the organization, on Windows 2003 or higher. And no need to reboot. When you’re done, all of your users are in the cloud and you can do single sign-on from any web or mobile application, using SAML, OAuth, or WS-Fed.
Sometimes we get asked about how secure this is – does Microsoft use it for their own sensitive business data? And the answer is yes we do. For example, if you go to the internal Microsoft SharePoint site at http://microsoft.sharepoint.com, you get prompted to log in.
Office 365 sign-in
Microsoft has enabled ADFS, so when you enter a Microsoft ID, you get redirected to an ADFS log-in page.
ADFS sign-in
And once you enter credentials stored in an internal Microsoft AD account, you have access to this internal application.
MS SharePoint site
We're using an AD sign-in server mainly because we already had ADFS set up before WAAD became available, but the log-in process is going through a WAAD directory in the cloud. We put our important documents, source control, performance management files, sales reports, and more, in the cloud and are using this exact same solution to secure them.

Create an ASP.NET app that uses WAAD for single sign-on

Visual Studio makes it really easy to create an app that uses WAAD for single sign-on, as you can see from a few screen shots.
When you create a new ASP.NET application, either MVC or Web Forms, the default authentication method is ASP.NET Identity. To change that to WAAD, you click a Change Authentication button.
Change Authentication
Select Organizational Accounts, enter your domain name, and then select Single Sign On.
Configure Authentication dialog
You can also give the app read or read/write permission for directory data. If you do that, it can use the Windows Azure Graph REST APIto look up users’ phone number, find out if they’re in the office, when they last logged on, etc.
That's all you have to do - Visual Studio asks for the credentials for an administrator for your WAAD tenant, and then it configures both your project and your WAAD tenant for the new application.
When you run the project, you'll see a sign-in page, and you can sign in with credentials of a user in your WAAD directory.
Org account sign-in
Logged in
When you deploy the app to Windows Azure, all you have to do is select an Enable Organizational Authentication check box, and once again Visual Studio takes care of all the configuration for you.
Publish Web
These screen shots come from a complete step-by-step tutorial that shows how to build an app that uses WAAD authentication: Developing ASP.NET Apps with Windows Azure Active Directory .

Summary

In this chapter you saw that Windows Azure Active Directory, Visual Studio, and ASP.NET, make it easy to set up single sign-on in Internet applications for your organization's users. Your users can sign on in Internet apps using the same credentials they use to sign on using Active Directory in your internal network.

A Developer's Introduction To Active Directory Federation Services

One of the most important components of Windows Server® 2003 R2 is Active Directory Federation Services (ADFS). ADFS solves a number of problems—one of the most obvious and compelling being business-to-business automation. In this article I'm going to take a look at ADFS from the perspective of a developer who is building a Web application and wants to allow other organizations to use it.

What kind of business-to-business problems am I referring to? Imagine that a bicycle manufacturer called Fabrikam wants to expose a Web application that will allow authorized dealers to purchase bikes and parts at wholesale prices. There are over two hundred dealers, each with several people who need to use the application. Fabrikam is going to need a secure logon mechanism.
An obvious solution would be to create a database containing user names and passwords, but this could become very costly to manage. If someone makes a call to Fabrikam claiming to be an employee of a dealer, how is Fabrikam going to verify this claim? They'll probably want to contact someone they trust at the dealership to verify the employee's status before provisioning a new account. Just consider the maintenance cost of such a user account: people forget user names, passwords, and have other problems. And what happens when the employee is terminated from the dealership? Is anyone going to remember to notify Fabrikam that a user account should be removed (or deprovisioned, in identity lingo)? If not, that user could go home and place false orders on the dealer's behalf.
Passwords themselves pose another problem. As computing power has increased, passwords have become easier and easier to attack, and many organizations now prefer to use stronger authentication techniques like smart cards. But because Fabrikam must work with so many different dealerships, it's going to have a difficult time supporting anything stronger than passwords.
Notice that trust is a factor here as well. Fabrikam trusts each dealership to supply an accurate list of employees who should be allowed to make purchases using Fabrikam's Web application.


A Solution Using ADFS
ADFS helps you establish trust relationships and reduces the need for provisioning user accounts. Why should you bother building a user account database for your application when your dealerships already have software that authenticates their users?
ADFS is the Microsoft implementation of the WS-Federation passive requestor profile protocol (passive indicates that all the client needs is a cookie- and JavaScript-capable Web browser—a passive agent that does not run any special code to help implement the protocol). Here's how this protocol works: when a user at a dealership points her browser at Fabrikam's purchasing application, her browser will eventually be redirected back to a Web site at the dealership where she works, so that the dealership can authenticate her. Then her browser will be redirected back to Fabrikam, and this new request will carry a signed statement by the dealership indicating that this is an actual employee who is authorized by the dealership to use the application. (I'm simplifying this quite a bit.) The point is that Fabrikam trusts the dealership to authenticate its own users, which is key to federation technology.
If a dealership has software that supports the WS-Federation passive profile, Fabrikam doesn't need to provision any user accounts for employees at that dealership. It doesn't need to worry about password reset requests. When a user at the dealership switches roles and no longer works in the purchasing department, as long as her identity information is changed to reflect this, when she tries to use Fabrikam's purchasing application the statement from the dealership will indicate that she's no longer a purchasing agent and she'll be denied access. Or if she quits her job at the dealership and her account is deprovisioned, she'll also no longer be able to use Fabrikam's purchasing application. By federating with the dealership, Fabrikam is virtually guaranteed to receive more relevant, up-to-date information about the user's identity.
ADFS is built on standards like WS-Federation, which was coauthored by Microsoft, IBM, Verisign, BEA, and RSA Security. Different organizations often run very different software. If Fabrikam uses Windows Server 2003 R2 with ADFS, but has dealerships running IBM WebSphere or BEA WebLogic, this really shouldn't be a problem because WebSphere and WebLogic both implement WS-Federation.
End users also get a better deal with federated identity. Instead of having to remember yet another password, a purchasing agent at a dealership can simply point her browser at Fabrikam's application and immediately start working. If the dealership's authentication system supports an integrated logon through a browser, as Windows does with Internet Explorer®, the user won't even be prompted for her credentials; she'll be authenticated silently and the federation service will translate the local knowledge of her identity into the signed statement for Fabrikam that I mentioned earlier. This is the brass ring of Web security: single sign-on across the Internet and across organizational and technological boundaries.


ADFS Architecture
Now I'm going to show you the various parts of ADFS and explain some terminology. In any given federation relationship, one side supplies the users (accounts) and the other side supplies the applications (resources). When you install ADFS, you'll configure its trust policy using the ADFS administration snap-in, which shows up in your Administrative Tools folder, to indicate the list of partners with which you want to federate. Users from the account partner will be accessing ADFS-enabled applications in the resource partner. Figure 1 shows the tree view of ADFS trust policies from either side of a relationship, as seen from the ADFS administration snap-in.

Figure 1 Policy for Two Partners (Click the image for a larger view)
On either side sits what is known as a federation service. Each federation service exposes a Web application, and it's expected that the user's browser will be redirected to these applications in order to establish a logon. The federation service resolves the impedance mismatch between the account and resource partners, so much so that the partners don't have to be using the same identity or operating system technology. Your app doesn't need to worry much about the federation service; the ADFS team provides a Web agent that runs in the ASP.NET pipeline as an HttpModule that will do the heavy lifting for you. All you need to do is configure your application to use the Web agent and supply some configuration settings so it knows where to find your company's federation service. Figure 2 shows the basic structure of ADFS and how the user's browser interacts with the various components. This shows what things would look like if ADFS were on both sides of the wire, but it's easy enough to imagine the account partner, for example, being implemented in WebSphere with an IBM directory service behind it, exposing a Java-language federation service.

Figure 2 ADFS Architecture


Federated Logon Walkthrough
When Alice, an authorized user from one of Fabrikam's dealerships, points her browser at Fabrikam's purchasing application for the first time, the Web agent notices that she doesn't have an ADFS cookie. She's not logged in. So the Web agent redirects her browser to Fabrikam's federation service before the purchasing application even sees the request. Fabrikam's federation service then redirects the browser to the dealership's federation service, adding to the request a unique identifier for Fabrikam's federation service so the dealership will know which partner is requesting a logon. When Alice's browser ends up back at her own federation service, she's asked to authenticate. Because her dealership is using integrated Windows authentication, Alice's browser automatically responds, establishing a logon with her federation service. Once that logon is successful, her federation service issues a Security Assertion Markup Language (SAML) token describing Alice, and redirects Alice's browser back to Fabrikam's federation service, sending the SAML token along with the request as part of the POST body (the page that is returned has JavaScript that auto-submits the form containing hidden input elements). Fabrikam reads the contents of the token, and issues a different SAML token that contains the set of claims that the application will ultimately see (I'll explain why two separate SAML tokens are used in the section on claims transformation). This second token is sent using the POST method and written out to a cookie from fabrikam.com, allowing Alice to use the purchasing application until her cookie expires, which by default is in 10 hours.
Remember the Web agent in the purchasing application that was looking for a logon cookie? That's what started this whole sequence of events. Now that the cookie exists, the Web agent peels it apart and reads the claims in the SAML token that was issued by Fabrikam's federation service. The Web agent allows the Web page in the purchasing application to execute. If the application needs to know the logged-on user's name, it needs only to look in the usual place: HttpContext.User.Identity.Name.
The implementation of IIdentity used to convey this information is of type SingleSignOnIdentity, which also exposes a number of other useful properties, including the authentication method used by the account partner to authenticate the user in her home realm. From this class the application can also discover the entire set of claims sent by its federation service.
You should note that the partner federation services never directly talk to one another. It's only through browser redirections and associated query strings and POST bodies that the services are able to communicate.


Securing the Federated Logon
Now let's dig into some of the details to show you how the logon is secured. One attack vector would be to try to read the SAML tokens off the wire and then replay them at your convenience so you could impersonate a legitimate user. To prevent this, all of the communication occurs over HTTPS. This is critical; if you try to install ADFS without already having an SSL certificate installed for the default Web site in IIS, the ADFS installer will warn you and quit before it even gets started.
What about the logon cookies that contain claims? If the user wanted to elevate her privileges to an application she could simply add claims to the SAML token in her cookie! But this type of tampering can be detected because each SAML token is signed with a private key known only by the issuing federation service. This has implications for deploying ADFS. If you're acting as a resource partner, each of your account partners must supply certificates for their account federation services. Your resource federation service will use the account partner's certificate to verify signatures on the SAML tokens that it issues. The Web agent must do something similar, verifying that each SAML token it receives via a logon cookie was signed by the resource federation service.
Cookies issued by ADFS are always marked with the Secure bit, which indicates that the browser should only send the cookie over HTTPS, not HTTP. So if any part of your application runs over HTTP, the logon cookie won't be present and you won't have access to the user's identity information. It'll feel as though the user is anonymous. I'd recommend running the entire application over SSL to simplify your life and reduce the chances of leaking sensitive data to an eavesdropper.
Cross-site scripting (XSS) bugs in an ADFS-enabled Web application are devastating, as they would be in any system that uses cookies to represent logon sessions. Cookies can easily be stolen from an application that has this type of vulnerability, and if you steal the cookie, you've stolen the logon. As a defense-in-depth measure, ADFS logon cookies time out after a work day by default, and you can adjust these timeout values in the ADFS trust policy.
Where Do You Come From, User?
Fabrikam has many dealerships that act as federated account partners. When one uses the purchasing application, how does Fabrikam's federation server know to which partner to redirect the user's browser? It doesn't. With multiple partners, Fabrikam's federation server must pause the logon process to display a list of partners to the user so she can select one to authenticate her. This is known as home realm discovery. If the client lies about her home realm, she's only going to inconvenience herself, as she won't have credentials to authenticate with any of the other dealerships.
One of the goals of building a great ADFS application is to avoid bothering the user with these sorts of details. The Northwind bike shop can eliminate this step by having its users access Fabrikam's Web site through a link with a magical query string parameter, whr. (I like to think of this as "which home realm?") The Web agent looks for this query string argument, and if it finds it, strips it out of the request during preprocessing. The value of this parameter is the URI of the partner federation server (each federation server names itself with a URI). So employees at Northwind shop could be presented with a link that looked something like this:
https://fabrikam.com/purchasing.aspx/?whr=urn:federation:
Northwindbikeshop
This gives ADFS enough of a hint to avoid having to display an interactive home realm discovery page.


Claims and Transformation
I've been talking a lot about claims so far in this article. A new, increasingly important security concept on the Windows platform, claims are a general way of making statements about an entity such as a user. Consider a Kerberos ticket, which contains user and domain group identifiers for the logged-on user. This is really just a set of claims signed by the domain controller that issued the ticket. The user identifier is an identity claim. But while this may be useful for auditing or personalization, it's not typically used for access control. The groups in the ticket are more likely to be used for performing access checks. For example, if you're a member of the Managers group, you may be allowed to approve high-value purchases.
ADFS supports three types of claims: identity claims, group claims, and custom claims. An identity claim takes the form of a user principal name (UPN), an e-mail address, or an arbitrary string called a common name. A group claim is Boolean (either a member or not a member), and it doesn't necessarily have to represent a Windows group: it might simply represent a logical role that your application understands. A custom claim contains a string value. Age, gender, or perhaps even ManagerName would be examples of custom claims that you can configure in ADFS. These types of custom claims should make you start thinking about privacy issues.
Claim transformation is how ADFS addresses both privacy issues and other practical concerns such as the fact that not every company uses the same terminology, and that not every resource partner needs to know a user's e-mail address or age. This is why ADFS trust policy allows you to configure each partner differently; you should only send the claims that partner needs.
Each company defines a set of organizational claims; this is like determining the vocabulary that it understands. For example, Fabrikam might define a group claim called "Owner" that it uses to represent the owner of a bike dealership. But the Northwind bike shop may represent this role with a claim called "Manager." That's OK as long as the meaning is the same, because the owner of Northwind can use his resource federation trust policy to map his outgoing "Manager" claim into an "Owner" claim that Fabrikam will understand. Administrators can set up these sorts of mappings on either side of a federation relationship. Figure 3 shows an example of a claim mapping in a resource partner's trust policy.

Figure 3 Claim Mapping (Click the image for a larger view)
Some claim mappings are too dynamic to be represented by a static mapping in trust policy. Suppose a resource partner needs a claim called IsOfLegalVotingAge to certify that someone is 18 years or older, but all you have is the user's date of birth as a custom claim from Active Directory®. What you need is a claims transformation module, which is an assembly with a class that implements a managed class called IClaimTransform. You can wire this up to your trust policy via the trust policy property sheet shown in Figure 4. This module is called twice: once before the normal trust policy mapping occurs and once afterward, so you can perform pre- or post-processing. Every ADFS trust policy allows a module like this to be installed, which means you can do dynamic claims transformation on both the account and resource side of a federation.

Figure 4 Claims Transformation Module


Where Do Claims Come From?
Unless you're writing a claims transformation module that generates claims dynamically, all of your claims will originate from an account store, which can be either Active Directory or Active Directory Application Mode (ADAM), a lightweight directory server based on the Active Directory code base. For Active Directory account stores, you can map group claims onto one or more groups in Active Directory; with an ADAM store, you must supply the name of an attribute on the user object and a value to look for to indicate whether the group claim should be sent for a given user. Each custom/identity claim is mapped directly onto a single attribute of the user object in the directory.
The beauty is that an administrator can take advantage of existing identity data in the company directory to integrate with partners and, in many cases, there's no need to write any code.


Anonymity
One thing that's really interesting about claims-based systems is that there's often no need to send the user's name at all. Perhaps all the partner cares about is whether or not the user is authorized to perform the request she's making. But it's often convenient for the partner to at least have some identifying handle from which he can hang personalization data or other state for the user.
To support this anonymous mode of operation, there's a special built-in transformation for identity claims called enhanced identity privacy. If you're an account partner and would like to ensure that your users remain anonymous with a particular resource partner, just enable this feature for that partner in your trust policy, and instead of seeing
alice@northwindbikes.com
the partner will instead see something like this:
tQZPfFuodGysa4t40oj+kM2vBIU=@northwindbikes.com

To generate this handle, the federation service hashes a combination of the actual identity claim, the resource partner's URI, and a salt value that only your federation service knows (this is called the "privacy key" in ADFS terminology). Including the partner's URI ensures that handles differ for each resource partner, preventing them from colluding to build a dossier of data on a user. The privacy key is included to make dictionary attacks difficult.
Despite protecting the identity from the resource partner, this feature still maintains traceability. In the event that an identity needs to be determined, the hashed identity can be found in the resource logs and provided to the account partner administrator, who can use the account event logs to determine which user received that identity.


The Federation Service Proxy
When you install the ADFS federation service, you'll see a new Web application in IIS manager called "adfs." Underneath this, you'll find subdirectories called "fs" and "ls," which stand for Federation Service and Logon Service. The Logon Service is really just the browser-based front end of ADFS; if you poke around under the ls directory you'll see a set of ASPX pages (by the way, you can customize these pages if you like, so it's worthwhile to explore them). This is where the browser is redirected during a federated logon. I've not found the term "Logon Service" anywhere in the published documentation for ADFS, but it's apparent by looking at the code that this is what it's called internally. Under the fs directory you'll find a file called FederationServerService.asmx, the Web service which is the back end of ADFS, technically called a Security Token Service (STS).
The reason these are split up is so that the front end (the Logon Service) can be hosted in a perimeter network (often referred to as a DMZ) and can communicate with the back-end of the federation service via the ASMX Web service, which lives on the internal network. In this configuration the front-end is known as the federation service proxy.
Whether or not your administrator splits up the federation service by using the proxy on a separate machine has little impact on how you write your application, but it's good to know that ADFS was designed to be DMZ-friendly.


ADFS-Enabling Your Web Application
If you've been tasked with building a Web application that supports ADFS logons, you'll need to do a few things. You'll need to know how to set up your web.config file to load and configure the ADFS Web agent. Figure 5 shows the web.config file I used for the sample application for this article.


<configuration>

  <configSections>
    <sectionGroup name="system.web">
      <section name="websso" type=

        "System.Web.Security.SingleSignOn.WebSsoConfigurationHandler, 
         System.Web.Security.SingleSignOn, Version=1.0.0.0, 
         Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null"/>
    </sectionGroup>
  </configSections>

  <system.web>

    <!-- we’re not using any of the standard ASP.NET auth techniques, 
         we’re using ADFS! -->
    <authentication mode="None" />
    <customErrors mode="Off"/>
    <sessionState mode="Off" />

    <!-- pull in ADFS assemblies -->
    <compilation debug=‘true’ defaultLanguage=‘c#’>
      <assemblies>
        <add assembly="System.Web.Security.SingleSignOn, Version=1.0.0.0, 
                       Culture=neutral, PublicKeyToken=31bf3856ad364e35, 
                       Custom=null"/>
        <add assembly="System.Web.Security.SingleSignOn.ClaimTransforms, 
                       Version=1.0.0.0, Culture=neutral, 
                       PublicKeyToken=31bf3856ad364e35, Custom=null"/>
      </assemblies>
    </compilation>

    <!-- pull in the module containing the ADFS web agent -->
    <httpModules>
      <add name="ADFS Web Agent" type=
          "System.Web.Security.SingleSignOn.WebSsoAuthenticationModule,
           System.Web.Security.SingleSignOn, Version=1.0.0.0, 
           Culture=neutral, PublicKeyToken=31bf3856ad364e35, 
           Custom=null" />
    </httpModules>

    <!-- the web agent looks here for its configuration -->
    <websso>
      <authenticationrequired />
      <eventloglevel>55</eventloglevel>
      <auditsuccess>2</auditsuccess>
      <urls>
        <returnurl>https://resource.local/web/</returnurl>
      </urls>
      <cookies writecookies="true">
        <path>/web</path>
        <lifetime>240</lifetime>
      </cookies>
      <fs>https://resource.local/adfs/fs/federationserverservice.asmx</fs>
    </websso>
  </system.web>

  <system.diagnostics>
    <switches>
      <!-- enables full debug logging -->
      <add name="WebSsoDebugLevel" value="255" /> 
    </switches>
    <trace autoflush="true" indentsize="3">
      <listeners>
        <!-- either create a c:\logs directory and grant Network Service 
             permission to write to it, or remove this listener -->
        <add name="MyListener" type=
            "System.Web.Security.SingleSignOn.
                 BoundedSizeLogFileTraceListener, 
             System.Web.Security.SingleSignOn, Version=1.0.0.0, 
             Culture=neutral, PublicKeyToken=31bf3856ad364e35, 
             Custom=null"
             initializeData="c:\logs\webagent.log" />
      </listeners>
    </trace>
  </system.diagnostics> 
</configuration>


Once your web.config file is set up, you should be able to accept logins from ADFS account partners. The next step is to start making security decisions based on claims.
There are actually two different ADFS Web agents. One is called the Web Agent for Windows NT® Token-based Applications. This Web agent will generate a real Windows logon session for the remote user. It generates a login from the user principal name via the S4U Kerberos extensions I described in my April 2003 Security Briefs column (a custom authentication package is used if not running in Windows Server 2003 native mode). The benefit is that you can impersonate this logon and pass off authorization duties to existing secure resource managers behind you, such as the file system or SQL Server™. (If these resources are remote, you'll also need to enable protocol transition in Active Directory.) One of the biggest drawbacks is that you're going to have to provision a user account in your domain for each incoming user, which destroys a lot of the benefit of using ADFS in the first place! (However, users will not need to know their resource account password, and most likely they won't even know there is another account provisioned for them.)
If you want to buy into federation, you'll need to build your app to be claims aware, and you won't be able to rely on impersonation because you won't have a Windows domain account representing the users from your account partners. You'll use the Web Agent for Claims-Aware Applications, which doesn't attempt any mapping onto Windows accounts, and hands you the incoming claims via HttpContext.User. This is the approach I took in my sample.
Writing code to check claims is not at all difficult. In fact, if you primarily rely on group claims, you need not think much about claims at all. Just continue to use HttpContext.User.IsInRole to check for the presence or absence of group claims, treating them as application roles. This means you can also use controls like the ASP.NET LoginView, which bases its output on roles discovered via the HttpContext.User property.
If you want to read the value of custom claims, you'll need to write a bit of new code. This sample looks for a claim called Title:
string GetTitle() 
{
    SingleSignOnIdentity id = (SingleSignOnIdentity)User.Identity;
    SecurityPropertyCollection c =
        id.SecurityPropertyCollection.GetCustomProperties("Title");
    return (1 == c.Count) ? c[0].Value : string.Empty;
}

One interesting piece of data that's passed along is the original method used to authenticate the client in her home realm. ADFS actually supports four authentication techniques: Windows integrated, SSL client certificate, Basic authentication, and ASP.NET Forms authentication. You can discover which was used via the AuthenticationMethod property on the SingleSignOnIdentity object. Don't confuse this with the original AuthenticationType property on IIdentity; you'll find that it always returns WebSSO, which basically means ADFS was responsible for the login.
Other interesting properties include AuthenticatingAuthority, the URI of the account partner that authenticated the client. And it's always a good idea to provide a Log Off button, so you'll find the SignOutUrl property useful in this regard.
The sample application for this article dumps out all of the public properties of SingleSignOnIdentity into a table, as shown in Figure 6. It also dumps out the collection of security properties, which basically shows all of the claims that were sent in their raw form. Before you can run this application, though, you'll need to set up ADFS.

Figure 6 Sample Application (Click the image for a larger view)


Getting Started with ADFS in the Lab
If you'd like to experiment with ADFS, you'll need to make time to set up an appropriate environment. I took notes as I built my claims-based B2B setup and came up with a reasonably quick way to put together a set of three Virtual PC images that represent an account partner with a single client machine along with a resource partner.I walk you through setting up domains and using SYSPREP, which may be new to you. You can edit the wiki by double-clicking the page, so feel free to add comments for fellow readers; just try to be brief in the spirit of my original notes.
There is a sample application up on the wiki as well, including the config file you can use to test with. Just copy the default.aspx and web.config files into a virtual directory on the resource partner image; then you should be able to log into the client machine and point your browser at the Web application in the partner application to log into it.

Single Sign On (SSO) for cross-domain ASP.NET applications

Its a sample SSO application based on the proposed model. It's not just another "Hello world", it's a working application that implements SSO across three different sites under three different domains. The hard work is done, and the soft output is, you just need to extend a class for making an ASPX page a "Single Sign On" enabled one in your ASP.NET application. You, of course, have to set up an SSO site and configure your client applications to use the SSO site, that's all (merely a 10 minute work).
The SSO implementation is based on the following high level architecture:
CrossDomainSSOExample/Proposed_SSO_model_overview.png
Figure : The Single Sign On implementation model
There may be unlimited number of client sites (in our example, 3 sites) which could participate under a "Single Sign On" umbrella, with the help of a single "Single Sign On" server (call this the SSO site, www.sso.com). As described in the previous article, the browser will not store an authentication cookie for each different client site in this model. Rather, it will store an authentication cookie for only the SSO site (www.sso.com) which will be used by the other sites to implement Single Sign On.
In this model, each and every request to any client site (which takes part in the SSO model) will internally be redirected to the SSO site (www.sso.com) for setting and checking the existence of the authentication cookie. If the cookie is found, the authenticated pages for the client sites (that are currently requested by the browser) are served to the browser, and if not found, the user is redirected to the login page of the corresponding site.

How it works

Initially, the browser doesn't have any authentication cookie stored for www.sso.com. So, hitting any authenticated page in the browser for www.domain1.com, or www.domain2.com, or www.domain3.com redirects the user to the login page (via an internal redirection to www.sso.com for checking the existence of the authentication cookie). Once a user is logged onto a site, the authentication cookie for www.sso.com is set in the browser with the logged in user information (most importantly, the user Token, which is valid only for the user's login session).
Now, if the user hits any authenticated page URL of www.domain1.com, or www.domain2.com or www.domain3.com, the request is internally redirected to www.sso.com in the user's browser and the browser sends the authentication cookie, which is already set. www.sso.com finds the authentication cookie, extracts the user Token, and redirects to the originally requested URL in the browser with the user token, and the originally requested site validates the Token and serves the page that was originally requested by the user.
Once the user is logged onto any site under this SSO model, hitting any authenticated page on www.domain1.com, or www.domain2.com, or www.domain3.com results in an internal redirection to www.sso.com (for checking the authentication cookie and retrieving the user Token) and then serving the authentication page in the browser output.

OK, show me what you implemented

I have developed a sample Single Sign On application that incorporates three different sites (www.domain1.com, www.domain2.com, and www.domain3.com) and an SSO server site (www.sso.com). The sample SSO implementation code is available for download with this article. You just need to download and set up the sites as instructed in the next section. Once you are done with that, you can test the implementation in different scenarios.
The following section has step by step instructions to test the Single Sign On functionality in different scenarios, and each testing scenario has a Firebug network traffic information that depicts the total number of requests (including the lightweight redirect requests) and their length in size. The number of redirect requests and their length in size are marked within green for easy understandability.

Scenario1: Before authentication

Hit the following three URLs in the browser in three different tabs of the same browser window.
  • www.domain1.com
  • www.domain2.com
  • www.domain3.com
CrossDomainSSOExample/LoginUrlSSO.png
Three different login screens will be presented in each different tabs, for each different site:
CrossDomainSSOExample/Login1.png
CrossDomainSSOExample/Login2.png
CrossDomainSSOExample/Login3.png

Traffic info

For presenting the login screen, in total, four requests are sent to the servers, among which three are redirect requests (marked in green). The redirect request sizes are very small (in terms of bytes), and are negligible even considering network latency.
CrossDomainSSOExample/TrafficLogin.png

Scenario 2: Authentication

Use one of any following credentials in any one of the login screens to log on. Let's log onto www.domain1.com with user1/123.
Available credentials:
  • user1/123
  • user2/123
  • user3/123
After login, the following screen will be provided for user1 onto www.domain1.com.
CrossDomainSSOExample/Home1.png

Traffic info

For login, in total, three requests are sent to the servers, among which two are redirect requests (marked in green). The redirect request sizes are very small (in terms of bytes), and are negligible even considering network latency.
CrossDomainSSOExample/TrafficAuthentication.png

Scenario 3: After authentication

As user1 has logged on to www.domain1.com, he should be logged onto the other remaining sites: www.domain2.com and www.domain3.com at the same time, if those sites are browsed in the same window or in different tabs in the same window. Hitting an authenticated page in www.domain2.com and www.domain3.com should not present a login screen.
Let's just refresh the current page at www.domain2.com and www.domain3.com in their corresponding window (currently, the login screen is being shown in the browser):
CrossDomainSSOExample/Home2.png
CrossDomainSSOExample/Home3.png
You will see that instead of showing the login screen, the authenticated home page is being shown. So, user1 is logged onto all three sites: www.domain2.com, www.domain2.com, and www.domain3.com.
Each home page shows a "Go to Profile Page" link which you can click to navigate to another page. This demonstrates that clicking on hyperlinks and navigating to other pages in the application also works without any problem.

Traffic info

For browsing authenticated pages after login, in total, 3 requests are sent to the servers, among which 2 are redirect requests (marked in green). The redirect request sizes are also very small (in terms of bytes), and are negligible even considering network latency.
CrossDomainSSOExample/TrafficAuthentication.png

Scenario 4: Hit the authenticated page on a different session

As expected, the user's "Sign on" status should only be valid for the current session ID, and any authenticated page URL hit to any one of the three sites will be successful if the URL is hit in the same browser window or in a different tab of the same browser window. But, if a new browser window is opened, and an authenticated URL is hit there, it should not be successful and the request should be redirected to the login page (because that is a different browser session).
To test this, open a new browser window and hit any URL of the three sites that points to an authenticated page (you can copy and paste the existing URL addresses). This time, instead of showing the page output, you will see the request will be redirected to the login page as follows (assuming that you hit a URL of www.domain3.com):
CrossDomainSSOExample/Login3.png

Traffic info

For hitting an authenticated page on a different session, in total, 4 requests are sent to the servers, among which 3 are redirect requests (marked in green). The redirect request sizes are very small (in terms of bytes), and are negligible even considering network latency.
CrossDomainSSOExample/TrafficLogin.png

Log out

To log out of the sites, click on the "Log out" link of the home page of www.domain1.com. The system will log out user1 from the site www.domain1.com and will redirect to the login screen again:
CrossDomainSSOExample/Login1.png

Traffic info

For logging out, in total, 4 requests are sent to the servers, among which 3 are redirect requests (marked in green). The redirect request sizes are very small (in terms of bytes), and are negligible even considering network latency.
CrossDomainSSOExample/TrafficLogout.png

After log out

As user1 is logged out of the site www.domain1.com, he should be logged out from www.domain2.com and www.domain3.com at the same time. So, hitting any authenticated page URL of www.domain2.com and www.domain3.com should now redirect to their corresponding login screens.
To test this, refresh the current page of www.domain2.com and www.domain3.com. Instead of refreshing the page, the system will now redirect the requests to their login pages:
CrossDomainSSOExample/Login2.png
CrossDomainSSOExample/Login3.png

Traffic info

Same as login.

Great... this seems to be working! How do I set up the sites?

The sample SSO implementation has been developed using Visual Studio 2010, .NET 4.0 Framework, and tested in IIS 7 under a Windows Vista machine. However, it doesn't use any 4.0 framework specific technology or class library and hence, it can be converted to use for a lower level framework without much effort, if required.
Follow these steps to set up the example SSO implementation in your machine:
  • Download SSO.zip and extract to any convenient location in your PC. The following folders/files will be extracted within the folder "SSO":

  • Click on "SSO.sln" to open the solution in Visual Studio, to understand "Who is What" in the solution structure:
  • CrossDomainSSOExample/SolutionExplorer.png
    As the names imply:
    • C:\...\www.domain1.com is the website for www.domain1.com
    • C:\...\www.domain2.com is the website for www.domain2.com
    • C:\...\www.domain3.com is the website for www.domain3.com
    • C:\...\www.sso.com is the web site for www.sso.com
    • SSOLib is a Class Library that handles all the Single Sign On related logic along with communicating with the SSO site via a Web Service, on behalf of the client sites.
  • Type "inetmgr" in the Run command to launch the IIS Manager (alternatively, you can navigate to the IIS Manager from Start->Program files) and create a site there named "www.domain1.com":
  • Right click on "Sites" and click on "Add Web Site...":
    CrossDomainSSOExample/AddNewSite.png
    Provide the necessary inputs in the following input form and click "OK":
    CrossDomainSSOExample/CreateSiteDomain1.png
    The site "www.domain1.com" will be created in IIS. After creating the site, the site might be shown with a red cross sign in IIS Explorer, indicating that the site is not started yet (this happens in my IIS in Windows Vista Home Premium). In this case, you need to select the site and click on the Restart icon to make sure it starts (the Restart icon is available in the right-middle portion of the screen in IIS Explorer).
    CrossDomainSSOExample/RestartSite.png
  • Following the same previous steps, create the following three sites pointing to their corresponding correct physical folder location:
    • www.domain2.com
    • www.domain3.com
    • www.sso.com
    CrossDomainSSOExample/CreateAllSites.png
  • Click on the "Application Pool" node in IIS Explorer. The application pool listing will be shown in the right pane, and you will see the application pools for the corresponding sites that you've just created in IIS.
  • Make sure all application pools are running under .NET Framework 4.0 (as the web application has been built in Framework 4.0). To do that, right click on the corresponding application pools (that have the same names as the site names) and select the .NET Framework version in the form:
    CrossDomainSSOExample/ApplicationPool.png
  • Edit the Hosts file (C:\Windows\System32\drivers\etc\hosts) so that the site names are mapped to the localhost loopback address (127.0.0.1):
  • 127.0.0.1       localhost
    127.0.0.1       www.domain1.com
    127.0.0.1       www.domain2.com
    127.0.0.1       www.domain3.com
    127.0.0.1       www.sso.com
If things are correctly done, you should be able to run the sites and test correctly as shown above. Otherwise, please verify if there is any thing missing or misconfigured by reviewing the steps from the start.

OK, how do I implement SSO for my sites?

Good question. The sample SSO implementation works fine. But, as a developer, you would likely be more interested in how to implement SSO in your ASP.NET sites using the things developed. While implementing the SSO model, I tried to make a pluggable component (SSOLib.dll) so that it requires minimum programmatic change and configuration. Assuming that you have some existing ASP.NET applications, you need the following steps to implement "Single Sign On" across them:
  • Add a reference to "SSOLib.dll", or add a reference to the "SSOLib" project to each ASP.NET application.
  • Set up the SSO site (see previous steps).
  • Configure your ASP.NET applications to use the SSO site. To do this, just add the following configurations in the web.confing of each ASP.NET application:
  • <!--Configuration section for SSOLib-->
     <configSections>
        <sectionGroup name="applicationSettings" 
             type="System.Configuration.ApplicationSettingsGroup, System, 
                   Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
         <section name="SSOLib.Properties.Settings" 
            type="System.Configuration.ClientSettingsSection, System, 
                  Version=4.0.0.0, Culture=neutral, 
                  PublicKeyToken=b77a5c561934e089" 
            requirePermission="false" />
        </sectionGroup>
     </configSections>
     <applicationSettings>
        <SSOLib.Properties.Settings>
         <setting name="SSOLib_Service_AuthService" serializeAs="String">
           <value>http://www.sso.com/AuthService.asmx</value>
         </setting>
        </SSOLib.Properties.Settings>
     </applicationSettings>
     <appSettings>
           <add key="SSO_SITE_URL" 
             value="http://www.sso.com/Authenticate.aspx?ReturnUrl={0}" />
           <add key="LOGIN_URL" value="~/Login.aspx" />
           <add key="DEFAULT_URL" value="~/Page.aspx" />
       </appSettings>
    <!--End Configuration section for SSOLib-->
    Note: Modify the configuration values according to the SSO site URL of your setup and your application specific needs.
  • Modify the code-behind classes (*.aspx.cs) of the login page and other private pages (pages which are accessible only to authenticated users) so that instead of extending System.Web.UI.Page, they will extend the SSOLib.PrivatePage class.
  • In addition to existing codes that perform the log out functionality, call the Logout() method that is already available to the code-behind classes from the SSOLib.PrivatePage base class. Also, instead of executing existing login functionality, call the Login() method of SSOLib.PrivatePage and execute other post-login codes if they exist already.
That's it! You should be done with your SSO implementation.

Hold on! My pages already extend a base class

OK, there is a high chance that you already have a base class which is extended by the code-behind classes in your ASP.NET applications. If that is so, integrating the SSOLib.PrivatePage may become even easier for you.
Let's say there is already a BasePage class which is extended by the code-behind classes of the authenticated page (pages which are accessible onto to the authenticated users) in one of your applications. In this case, instead of modifying the code-behind classes of all the ASPX pages, you might just need to modify the BasePage so that it extends SSOLib.PrivatePage, and you are done.
class BasePage : SSOLib.PrivatePage
{
    ...
}
Another alternative is to modify SSOLib.PrivatePage to extend the existing BasePage (you have the source code, you can do it) and modify all the existing aspx.cs classes of the authenticated pages to extend SSOLib.PrivatePage as suggested. That is:
class PrivatePage : BasePage 
{
    ...
}
If there is any conflicting code or method between the existing BasePage class and the SSOLib.PrivatePage class, you might need to modify some code in these two classes. It would be preferable not to change the code of SSOLib.PrivatePage unless any bug is discovered, and it would be better to change the existing BasePage code as required. But, feel free to change the code of SSOLib.PrivatePage if you really need to, it's all yours!

Hmm.. so who does all the SSO things? Where is the magic?

Good question. In the ideal case, using this example SSO model, you won't have to write a single line of Sign On oriented code to implement SSO in your ASP.NET applications (except some configuration and inheritance changes). How is this possible? Who is managing all the dirty SSO stuff?
SSOLib and the SSO site are the two magicians doing all the tricks. SSOLib is a DLL which is used by each client site to carry out the following things:
  • Communicate to SSO site via a Web Service.
  • Redirect to the SSO site or login page or serve the requested page.
The following diagram depicts the role of SSOLib in the SSO model:
CrossDomainSSOExample/SSOLib.png
The most important thing inside SSOLib is the PrivatePage class which is inherited by the code-behind pages of the authenticated classes. This class inherits the System.Web.UI.Page class, and overrides the OnLoad() method, as follows:
public class PrivatePage : Page
{
      protected override void OnLoad(EventArgs e)
      {
           //Set caching preferences
           SetCachingPreferences();
                      
           //Read QueryString parameter values
           LoadParameters();

           if (IsPostBack)
           {
               //If this is a postback, do not redirect to SSO site.
               //Rather, hit a web method to the SSO site 
               //to know user's logged in status
               //and proceed based on the status
               HandlePostbackRequest();
               base.OnLoad(e);
               return;
           }

           //If the current request is marked not 
           //to be redirected to SSO site, do not proceed
           if (SessionAPI.RequestRedirectFlag == false)
           {
               SessionAPI.ClearRedirectFlag();
               base.OnLoad(e);
               return;
           }

           if (string.IsNullOrEmpty(RequestId))
           {
               //Absence of Request Paramter "RequestId" means 
               //current request is not redirected from SSO site.
               //So, redirect to SSO site with ReturnUrl
               RedirectToSSOSite();
               return;
           }
           else
           {
               //Current request is redirected from 
               //the SSO site. So, check user status
               //And redirect to appropriate page
               ValidateUserStatusAndRedirect();
           }

           base.OnLoad(e);
       }
}
Basically, OnLoad() is called whenever a Page object is loaded as a result of a URL hit in the browser, and the core SSO logic is implemented inside this method. All the codes is self descriptive and documented to depict what is going on.
More on the SSOLib functionality in the following sections.

What does www.sso.com do?

The SSO site has the following two important functionalities:
  • User authentication and user retrieval Web Services which are accessed by the client sites via the SSOLib DLL to authenticate the user and to know the user's logged-in status on the SSO site. The following services are available:
  • CrossDomainSSOExample/SSOWebServices.png
  • Setting and retrieving user authentication cookie using an ASPX page (Authenticate.aspx) which is redirected by SSOLib for setting / checking or removing the cookie based upon the type of request.
  • Following is the core functionality that is performed by Authenticate.aspx. The codes is self-descriptive and documented for easy understandability.
    protected void Page_Load(object sender, EventArgs e)
    {
       //Read request paramters and populate variables
       LoadRequestParams();
    
       if (Utility.StringEquals(Action, AppConstants.ParamValues.LOGOUT))
       {
           //A Request paramter value Logout indicates
           //this is a request to log out the current user
           LogoutUser();
           return;
       }
       else
       {
           if (Token != null)
           {
               //Token is present in URL request. That means, 
               //user is authenticated at the client site 
               //using the Login screen and it redirected
               //to the SSO site with the Token in the URL parameter, 
               //so set the Authentication Cookie
               SetAuthCookie();
           }
           else
           {
               //User Token is not available in URL. So, check 
               //whether the authentication Cookie is available in the Request
               HttpCookie AuthCookie = 
                  Request.Cookies[AppConstants.Cookie.AUTH_COOKIE];
               if (AuthCookie != null)
               {
                   //Authentication Cookie is available 
                   //in Request. So, check whether it is expired or not.
                   //and redirect to appropriate location based upon the cookie status
                   CheckCookie(AuthCookie, ReturnUrl);
               }
               else
               {
                   //Authentication Cookie is not available 
                   //in the Request. That means, user is logged out of the system
                   //So, mark user as being logged out
                   MarkUserLoggedOut();
               }
           }
       }
    }

Looks pretty good. Did you have to handle any critical issues?

Another good question. The core SSO logic seems pretty straightforward. That is:
If current request is a PostBack, 
    If this is a PostBack in Login page (For Login)
        Do Nothing
    Else
        Do not redirect to SSO site. Rather, invoke a web service at SSO site 
        to know user's logged in status, using the User *Token. 
    If user is not logged out 
        Proceed the normal PostBack operation.
    Else 
        Redirect to login page
Else
    If current request is not redirected from the SSO Site, 
        Redirect it to SSO site with   setting ReturnUrl with 
        the current Request URL and parameters.
    Else
        Get user's Logged in status on SSO Site 
        by invoking a web service with user *Token

        If user is logged out there, 
            Redirect to Login page
        If current request is a page refresh, 
            Redirect to SSO site with ReturnUrl
        Else 
            Redirect to the originally requested URL
    End If
End If
*User token is a hash code of a GUID that identifies a user's login onto the SSO site uniquely. Each time a user is logged onto the SSO site, the token is generated at the SSO site, and this token is used later to set the authentication cookie and to retrieve the user object by the client sites.
But there are some obvious issues that were needed to be handled to implement the SSO logic. These are marked in bold in the above logic:

Implement "Redirect to login page" and "Redirect to the originally requested URL"
SSOLib.PrivatePage redirects to the SSO site, or redirects to the currently requested page at the client site, based upon the situation. But, there is a problem if SSOLib.PrivatePage redirects to a page of the current site. As each authenticated page extends the SSOLib.PrivatePage class, a redirect to a page in the current site from SSOLib.PrivatePage would redirect to itself again and again, and will cause an infinite redirect loop.
To solve this issue, an easy fix could be to add a request parameter (say, Redirect=false) to indicate that the request should not be redirected any further. But, this would allow the user to see the Request parameter and allow the user to "hack" the system by altering its value. So, instead of using a Request parameter, I used a Session variable to stop further redirection, before redirecting to any URL of the current site from SSOLib.PrivatePage. In OnLoad(), I check the Session variable and reset it and return as follows:
//If the current request is marked not to be redirected to SSO site, do not proceed
if (SessionAPI.RequestRedirectFlag == false)
{
   SessionAPI.ClearRedirectFlag();
   return;
}
Detect whether "Current request is not redirected from the SSO Site", and whether "current request is a page refresh"
SSOLib.PrivatePage redirects to the SSO site for setting or checking the authentication cookie. After the SSO site is done with its work, it redirects back to the calling site using the URL that is set in ReturnUrl.

This also creates a scenario where the client site might again redirect to the SSO site and the SSO site again redirects to client site and creates an infinite redirection loop. Unlike the previous situation, this time, a Session variable could not be used because the redirection is occurring from the SSO site, and the client site and the SSO site have different Session states. So, a Request parameter value should be used to prevent further redirect to the SSO site once the SSO site redirects to the client site.
But again, using a Request parameter to prevent redirection would allow the user to alter it and break the normal functionality. To work-around this, the Request parameter value is set with a hash of a GUID (RequestId=Hash(New GUID)), and this is appended from the SSO site before redirecting back to the client-site URL.
The redirect request executes the OnLoad() method of SSOLib.PrivatePage again, and this time, it finds the RequestId, and this indicates that this request is redirected back from the SSO site and hence this should not be redirected to the SSO site further.
But, what if the user alters the value of the RequestId in the query string and hits the URL, or the user just refreshes the current page?

As each different request is to be redirected to the SSO site (except the postback hits), in this case, this request should be redirected to the SSO site as usual. But, the request URL already contains a RequestId, and despite this, the request should be redirected to the SSO site. So, how should SSOLib.PrivatePage understand this?
There is only one way. A specific RequestId should be valid for each particular redirect from the SSO site only, and once the RequestId is received at the client site from the Request parameter, it should expire instantly so that even if the next URL hit contains the same RequstId, or if the next URL contains an invalid value, it redirects to the SSO site.
The following logic has been used to handle this scenario:
if (string.IsNullOrEmpty(RequestId))
{
   //Absence of Request Paramter RequestId means current 
   //request is not redirected from SSO site.
   //So, redirect to SSO site with ReturnUrl
   RedirectToSSOSite();
   return;
}
else
{
   //Current request is redirected from the SSO site. So, check user status
   //And redirect to appropriate page
   ValidateUserStatusAndRedirect();
}

//And,

UserStatus userStatus = AuthUtil.Instance.GetUserStauts(Token, RequestId);
if (!userStatus.UserLoggedIn)
{
   //User is not logged in at SSO site. So, return the Login page to user
   RedirectToLoginPage();
   return;
}
if (!userStatus.RequestIdValid)
{
   //Current RequestId is not valid. That means, 
   //this is a page refresh and hence, redirect to SSO site
   RedirectToSSOSite();
   return;
}
if (CurrentUser == null || CurrentUser.Token != Token)
{
   //Retrieve the user if the user is not found 
   //in session, or, the current user in session
   //is not the one who is currently logged onto the SSO site
   CurrentUser = AuthUtil.Instance.GetUserByToken(Token);
   if (CurrentUser.Token != Token || CurrentUser == null)
   {
       RedirectToSSOSite();
       return;
   }
}
On the other hand, before redirecting to the client site, the SSO site generates a RequestId, appends it with the query string, and puts it in Application using the RequestId as the key and value. Following is how the SSO site redirects back to the client site:
/// <summary>
/// Append a request ID to the URl and redirect
/// </summary>
/// <param name="Url"></param>
private void Redirect(string Url)
{
    //Generate a new RequestId and append to the Response URL.
    //This is requred so that, the client site can always
    //determine whether the RequestId is originated from the SSO site or not
    string RequestId = Utility.GetGuidHash();
    string redirectUrl = Utility.GetAppendedQueryString(Url, 
              AppConstants.UrlParams.REQUEST_ID, RequestId);
    
    //Save the RequestId in the Application
    Application[RequestId] = RequestId;

    Response.Redirect(redirectUrl);
}
Note that, before redirection, RequestId is stored in the Application scope to mark that this RequestId is valid for this particular response to the client site. Once the client site receives the redirected request, it executes the GetUserStatus() Web Service method, and following is how the GetUserStatus() web method clears the RequestId from the Application scope so that any subsequent requests with the same RequestId or any request with an invalid RequestId can be tracked as an invalid RequestId:
/// <summary>
/// Determines whether the current request is valid or not
/// </summary>
/// <param name="RedirectId"></param>
/// <returns></returns>
[WebMethod]
public UserStatus GetUserStauts(string Token, string RequestId)
{
      UserStatus userStatus = new UserStatus();

      if (!string.IsNullOrEmpty(RequestId))
      {
          if ((string)Application[RequestId] == RequestId)

          {
              Application[RequestId] = null;
              userStatus.RequestIdValid = true;
          }
      }

      userStatus.UserLoggedIn = 
        HttpContext.Current.Application[Token] == null ? false : true;

      return userStatus;
}

Get user's logged in status on the SSO site

The GetUserStauts() Web Service method returns the user's status inside a UserStatus object, which has two properties: UserLoggedIn and RequestIdValid.
Once a user is logged onto the SSO site via the Authenticate Web Service method, it generates a User Token (hash code of a new GUID) and stores the user Token inside an Application variable using the Token as the Key:
/// <summary>
/// Authenticates user by UserName and Password
/// </summary>
/// <param name="UserName"></param>
/// <param name="Password"></param>
/// <returns></returns>
[WebMethod]
public WebUser Authenticate(string UserName, string Password)
{
   WebUser user = UserManager.AuthenticateUser(UserName, Password);
   if (user != null)
   {
       //Store the user object in the Application scope, 
       //to mark the user as logged onto the SSO site
       //Along with the cookie, this is a supportive way 
       //to trak user's logged in status
       //In order to track a user as logged onto the SSO site 
       //user token has to be presented in the cookie as well as
       //he/she has to be presented in teh Application scope
       HttpContext.Current.Application[user.Token] = user;
   }
   return user;
}
When the user logs out of the system from any client site, the authentication cookie is removed, and also the user object is removed from the Application scope (inside Authenticate.aspx.cs in the SSO site):
/// <summary>
/// Logs out current user;
/// </summary>
private void LogoutUser()
{
   //This is a logout request. So, remove the authentication Cookie from the response
   if (Token != null)
   {
       HttpCookie Cookie = Request.Cookies[AppConstants.Cookie.AUTH_COOKIE];

       if (Cookie.Value == Token)
       {
           RemoveCookie(Cookie);
       }
   }
   //Also, mark the user at the application scope as null
   Application[Token] = null;

   //Redirect user to the desired location
   //ReturnUrl = GetAppendedQueryString(ReturnUrl, 
   //   AppConstants.UrlParams.ACTION, AppConstants.ParamValues.LOGOUT);
   Redirect(ReturnUrl);
}
So, without redirecting to the SSO site, it is possible to know the user's logged in status just by checking the user's presence in the Application scope of the SSO site. The client sites invoke the Web Service method of the SSO site, and the SSO site returns the user's logged in status inside the UserStatus object.
This method of knowing the user's logged in status is handy because when a postback occurs, the client sites would not want to redirect to the SSO site (because, if they do that, the postback event methods cannot be executed).
In such cases, they invoke the web method to know the user's logged in status, and if the user is not available at the SSO site, the current request is redirected to the login page. Otherwise, the normal postback event method is executed.

Wait a minute, storing the user in the Application scope should mark the user logged in for all sessions. How do you handle that?

True. Once a user is authenticated, he/she is stored in the Application scope to mark as logged in. But, the Application scope is a global scope irrespective of the site and user sessions. So, there is a risk that the user might also get marked as logged in for all browser sessions.
This sounds risky. But, this is handled with care so that the user object of a particular browser session is not available to other browser sessions. Let us now see how this has been handled.
Once a user logs onto the SSO site, the user is stored in the Applicationscope against the user Token, which is valid only for a particular user Login session.
If some direct request is hit in a new window (hence with a new Session) with the user Token (with or without the RequestId) by copying the URL from the address bar, the system will not let the URL request bypass the login screen. Why? Because the authentication cookie that is set by the SSO site is a "non-persistent" cookie, and hence this cookie is sent by the browser to the SSO site only if subsequent requests are hit in the same browser session (from the same browser window or different tabs in the same window). That means, if a new browser window is opened, it does not have any authentication cookie to send to the SSO site, and naturally, the request is redirected to the login page of the client site. So, even if a user is stored in the Application scope in the SSO site, that user object is stored against a different user Token as a key, that can never be accessed for any new request in the new session, because this request does not know about the existing user Token, and once the user logs onto this new browser session, it gets a new user Token which never matches with the existing ones.

How cookie timeout and sliding expiration is maintained?

The web.config of the SSO site has configuration options for configuring the cookie timeout value and for enabling/disabling the sliding expiration of the cookie.
<appSettings>
    <add key="AUTH_COOKIE_TIMEOUT_IN_MINUTES" value="30"/>
    <add key="SLIDING_EXPIRATION" value="true"/>
</appSettings>
The cookie timeout value can be configured in the web.config of the SSO site and the timeout value applies to all client sites under the SSO. That is, if the cookie timeout value is specified in the web.config as 30 minutes and if user1 logs onto www.domain1.com, the cookie is available for the next 30 minutes in the browser, and hence user1 is signed on the other two sites for this 30 minutes, unless user1 is logged out of the site.
Now, how is this cookie timeout implemented? Simple, by setting the cookie expiration time, of course.
Unfortunately, I couldn't do that. Why? Because, by default, when a cookie is set in the Response, it is created as a non-persistent cookie (the cookie is stored only in the browser's memory for the current session, not in the client's disk). If the expiry date is specified for the cookie, ASP.NET runtime automatically instructs the browser to store the cookie as a persistent cookie.
In our case, we don't want to create a persistent cookie, because this will let the other sessions to also send the authentication cookie to the SSO site and eventually mark the user as logged in. We do not want that to happen.
But, the expiration datetime has to be set somehow. So, I stored the expiration value in the cookie's value, along with appending to the user's Token, as follows:
/// <summary>
/// Set authentication cookie in Response
/// </summary>
private void SetAuthCookie()
{
   HttpCookie AuthCookie = new HttpCookie(AppConstants.Cookie.AUTH_COOKIE);

   //Set the Cookie's value with Expiry time and Token
   int CookieTimeoutInMinutes = Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES;

   AuthCookie.Value = Utility.BuildCookueValue(Token, CookieTimeoutInMinutes);
   //Appens the Token and expiration DateTime to build cookie value

   Response.Cookies.Add(AuthCookie);

   //Redirect to the original site request
   ReturnUrl = Utility.GetAppendedQueryString(ReturnUrl, 
                        AppConstants.UrlParams.TOKEN, Token);
   Redirect(ReturnUrl);
}

/// <summary>
/// Set cookie value using the token and the expiry date
/// </summary>
/// <param name="Value"></param>
/// <param name="Minutes"></param>
/// <returns></returns>
public static string BuildCookueValue(string Value, int Minutes)
{
    return string.Format("{0}|{1}", Value, 
       DateTime.Now.AddMinutes(Minutes).ToString());
}
Eventually, when the cookie is received at the SSO site, its value is retrieved as follows:
/// <summary>
/// Reads cookie value from the cookie
/// </summary>
/// <param name="cookie"></param>
/// <returns></returns>
public static string GetCookieValue(HttpCookie Cookie)
{
   if (string.IsNullOrEmpty(Cookie.Value))
   {
       return Cookie.Value;
   }
   return Cookie.Value.Substring(0, Cookie.Value.IndexOf("|"));
}
And, the expiration date time is retrieved as follows:
/// <summary>
/// Get cookie expiry date that was set in the cookie value
/// </summary>
/// <param name="cookie"></param>
/// <returns></returns>
public static DateTime GetExpirationDate(HttpCookie Cookie)
{
   if (string.IsNullOrEmpty(Cookie.Value))
   {
       return DateTime.MinValue;
   }
   string strDateTime = 
     Cookie.Value.Substring(Cookie.Value.IndexOf("|") + 1);
   return Convert.ToDateTime(strDateTime);
}
If SLIDING_EXPIRATION is set to true in the web.config, the cookie expiration date-time value is increased with each request, with the minute value specified in AUTH_COOKIE_TIMEOUT_IN_MINUTES in the web.config. The following code does that:
/// <summary>
/// Increases Cookie expiry time
/// </summary>
/// <param name="AuthCookie"></param>
/// <returns></returns>
private HttpCookie IncreaseCookieExpiryTime(HttpCookie AuthCookie)
{
   string Token = Utility.GetCookieValue(AuthCookie);
   DateTime Expirytime = Utility.GetExpirationDate(AuthCookie);
   DateTime IncreasedExpirytime = 
     Expirytime.AddMinutes(Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES);

   Response.Cookies.Remove(AuthCookie.Name);

   HttpCookie NewCookie = new HttpCookie(AuthCookie.Name);
   NewCookie.Value = 
     Utility.BuildCookueValue(Token, Config.AUTH_COOKIE_TIMEOUT_IN_MINUTES);

   Response.Cookies.Add(NewCookie);

   return NewCookie;
}

Can this implementation be used for production systems?

Yes! It surely can be used, but before that, some security and other cross-cutting issues have to be addressed. This is just a basic implementation, and I didn't verify the model with a professional Quality Assurance process (though I did some basic acceptance testing myself). Also, this authentication does not offer the full flexibility and powers that Forms authentication provides. Additionally, it does not have the built-in authorization mechanism of Forms authentication, and hence you might need to write some more customization on the current SSO implementation, based upon your specific requirements.
However, I'll try to update the SSO model to enrich it with more features and make it robust so that this could be used in commercial systems without requiring any customization.