Introduction
"Yet another Monday morning at your office. Just started to wonder how fast the weekend just passed away, and how hard are the days going to be in this week. You got a mail! Oh no, not a lucrative job offer. It's just another requirement from your new client. Your client has a number of ASP.NET of sites, and all he wants is to enable his users to log onto all sites just by logging into a single site. And, obviously, log out of all sites by logging out from a single site.
OK, so your client wants a "Single Sign On" implementation. You thought, oh, it's not that hard. The ASP.NET forms authentication is the solution. It allows to share the same cookie across sites under the same domain using the same configuration key using the <machineKey>
element. A quick Google search gives you some pretty fine examples of Single Sign On implementation using <machineKey>
in ASP.NET applications. OK, so life is not that hard as a programmer after all.
Hold on. Something just caught your eyes. The requirement says, "your client has a number of sites, but they are not necessarily under the same domain". You just missed this important point, and, the very first day at your office just started to appear harder to you. It's not easy to implement a Single Sign On for sites under different domains, for the very fundamental reason that, the cookie of a particular domain cannot be shared with another domain. Who doesn't know that it's the cookie that is used to maintain authentication information across different page requests?"
I just depicted a scenario that is pretty common these days. This is an era of web 2.0 and social networking, as they say. Standalone and "island" like systems are very rare these days. You do a tweet from Twitter and update your status at LinkedIn and Facebook at the same time without doing anything else. You write an article on CodeProject and share it on hundreds of sites within seconds. So, it's pretty natural that you would expect to log onto a site and jump to another related one without having to re-login again, doesn't matter to you what domain these sites are deployed under.
So I thought, how about developing something that allows to easily implement a Single Sign On (of course, for ASP.NET sites) for cross-domain sites? People may have tried to implement this in many different ways, and commercial solutions are also available upon purchase. But, what if I try to develop something which is simple, free, and most importantly, works.
How authentication works in ASP.NET
Well, this may not be something new to you. But while we try to solve the hardest problems on earth, we often need to go back to the basics, to try to understand how things really work. So, it wouldn't be bad to revisit the ABS's of the ASP.NET forms authentication mechanism.
Here is how ASP.NET forms authentication works:
Forms authentication in ASP.NET
Sequence of operations:
- You hit a page in an ASP.NET application which is accessible only to the authenticated users (logged in users) of the application.
- The ASP.NET runtime looks for a cookie (forms authentication cookie) in the request, and if the cookie is not found, it redirects the current request to the login page (the login page location is configured in the web.config).
- You provide the login credentials and hit the "Login" button. Assuming that the credentials are correct, your system validates the credentials successfully against the data storage, sets the user name to the
Thread.CurrentPrincipal.Identity.Name
property, writes a cookie in the Response (along with setting the cookie value with the user information and setting some properties like cookie name, expiration date/time etc.), and redirects to the originally requested page.
- You hit another page in the application (or, click a link that navigates you to another page), and your browser sends the authentication cookie (with any other cookie that were written by the same site earlier), this time because the browser has already got the authentication cookie in the last response.
- As usual, the ASP.NET runtime looks for the authentication cookie in the request, and this time it finds the cookie. It checks the cookie properties (like expiration date/time, path etc.), and if not expired already, reads the cookie value, retrieves the user information from the cookie, sets the user name into the
Thread.CurrentPrincipal.Identity.Name
property, checks if the user has permission to access the requested page, and if he/she has permission, executes and serves the page to the browser.
Pretty simple, right?
How authentication works in multiple ASP.NET sites under the same domain
As depicted already, the ASP.NET forms authentication is fully dependent on Cookies. So, if two different sites can share the same authentication cookie, it is possible to make the same user to log onto both sites just by logging onto a single site.
Now, the HTTP protocol says, two different sites can share a cookie if and only if both sites are deployed under the same domain (or, sub-domain). Internally, your browser stores the cookies locally (either in disk or in memory) against the web site's URL. When you hit subsequent requests to any site, the browser reads those cookies which have matching domain or sub domain names comparing to the currently requested URL and sends those cookies with the request.
So, let's assume you have two sites as follows:
- www.mydomain.com/site1
- www.mydomain.com/site2
These two sites share the same host address (same domain mydomain.dom and sub-domain www), and both sites have been configured to use forms authentication for user authentication and authorization. Suppose you just logged onto the www.mydomain.com/site1 site, and as described above, your browser now has a forms authentication cookie originated from www.mydomain.com/site1.
Now, if you click/hit any subsequent URL of www.mydomain.com/site1, the forms authentication cookie will be sent with the request. Why? Because the cookie originally belongs to this site? Yes, but not 100% correct. To be exact, the cookie will be sent because the request URL www.mydomain.com/site1 has the same matching sub domain and domain name (host address) www.mydomain.com.
So, after you logged onto www.mydomain.com/site1, if you hit a URL of www.mydomain.com/site2, the forms authentication cookie (or, any other matching cookie) will be sent with the request for the very same reason that www.mydomain.com/site2 has the same matching host address (www.mydomain.com) even if this is a different application (site2). So, it's obvious that as the same forms authentication cookie can be shared across two different web applications under the same host address, it's possible to log onto both web applications at the same time just by logging onto a single web application (hence, Single Sign On).
However, ASP.NET does not allow you to automatically implement Single Sign On just by implementing forms authentication for multiple web applications under the same host address. Why? Because, each different ASP.NET web application uses its own encryption keys to encrypt and decrypt cookie data (and other data such as ViewState) for ensuring security. So, unless you specify a single encryption key for each ASP.NET application, cookies will be sent from the browser, but an application won't be able to read the value of an authentication cookie that is originated from another application.
Specifying a single authentication key is the solution. For each ASP.NET application, you have to use the same <machinekey>
element in the web.config, as follows:
<machineKey
validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75D"
decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719F"
validation="SHA1"
decryption="AES"/>
If the same machinekey
(with validationKey
and decryptionKey
) is used across all applications under the same domain, each will be able to read cookie values that originate from other applications.
What if two sites have the same domain and different sub domains?
Suppose your sites are deployed under the following host addresses:
- site1.mydomain.com
- site2.mydomain.com
These two sites share the same domain (same second level domain mydomain.com), but has differ in their third level domain (different sub domains site1 and site2).
By default, the browser sends cookies for matching host addresses only (matching domain and sub domain). So, the site site1.mydomain.com won't get any cookie that is originated form site2.mydomain.com (because they don't have the same host address, their sub domain is different), and hence even if you configure the same machineKey
for these two sites, one site won't be able to get cookies originated from another.
So, in addition to implementing the same machineKey
across all sites, you also need to instruct the ASP.NET runtime to define the domain of an authentication cookie so that the cookie is sent from the browser with any request that has a matching domain name. You need to configure the forms authentication cookie as follows:
<forms name="name" loginUrl="URL" defaultUrl="URL" domain="mydomain.com"/>
So, how to share the authentication cookie across multiple domains?
Well, there is absolutely no way to do that. The fundamental barrier of the HTTP protocol prevents you from sharing a cookie across different domains, primarily for security reasons.
Say, you have the following two sites under two different domains:
- www.domain1.com
- www.domain2.com
Now, if you log onto the www.domain1.com site using forms authentication, and hit a URL of www.domain2.com, the browser wouldn't simply send the authentication cookie of domain1.com to domain2.com. There is no built-in mechanism in ASP.NET to implement a Single Sign On across these two different sites.
In order to implement a Single Sign On across these two sites under different domains, there has to be some "workaround" or some implementation model that lets these two sites to access a single cookie via some "indirect" mechanism.
A very basic Cross Domain SSO implementation model
Suppose we have to implement SSO in these sites:
- www.domain1.com
- www.domain2.com
- www.domain3.com
In order to implement SSO across these sites, we need to set an authentication cookie to the client's browser for all three sites when the user is authenticated in any one of the two sites.
If user1 is authenticated into www.domain1.com, the browser will add an authentication cookie in the response before returning a response to www.domain1.com. But as we need to be able to log onto www.domain2.com and www.domain3.com, we need to set the authentication cookie in the same client's browser for www.domain2.com and www.domain3.com at the same time. So, before returning the response to the browser, www.domain1.com will have to redirect the browser to www.domain2.com and www.domain3.com for setting the authentication cookie.
The following diagram depicts the basic idea (arrows denote the redirections):
Basic SSO model overview
And the following sequence diagram depicts the detailed idea:
Sequence diagram of login operation in the basic SSO model
Sequence of operations:
- User hits the URL of an authenticated page of www.domain1.com in the URL.
[Status: browser has no authentication cookie]
Browser sends a request to www.domain1.com with no authentication cookie (because it has got no cookie that belongs to www.domain1.com).
[Status: browser has no authentication cookie]
www.domain1.com sees there is no authentication cookie in the request. So, it redirects the request to the login page of www.domain1.com.
[Status: browser has no authentication cookie]
User provides login credentials and hits the "Login" button. Browser sends a POST request to www.domain1.com.
[Status: browser has no authentication cookie]
www.domain1.com accepts the login credentials, verifies the credentials against a data storage, and upon success, marks the user as logged in, creates an authentication cookie with the logged in user's information, and adds to the response.
[Status : browser has no authentication cookie]
Instead of returning the response to the browser, www.domain1.com redirects the request to a page of www.domain2.com, along with a ReturnUrl
set to the original URL location of www.domain1.com. As the authentication cookie is already set in the response, the cookie is sent to the browser.
[Status : browser has no authentication cookie]
Browser accepts the response that contains the authentication cookie and a redirect command to www.domain2.com. The browser stores the authentication cookie for www.domain2.com and sends a request to www.domain2.com along with setting the authentication cookie in the request.
[Status : browser has an authentication cookie for www.domain2.com]
www.domain2.com immediately redirects to the ReturnUrl
address with setting an authentication cookie for www.domain1.com by reading the cookie value from the request. Eventually, the authentication cookie is also sent with the response with the redirect command.
[Status : browser has an authentication cookie for www.domain2.com]
Browser accepts the response that contains the authentication cookie and a redirect command to www.domain1.com. The browser stores the authentication cookie now for www.domain1.com and sends a request to www.domain1.com, along with setting the authentication cookie in the request.
[Status : browser has authentication cookie
for both www.domain1.com and www.domain2.com].
www.domain1.com sees that the authentication cookie is already set in the request, and hence instead of returning the login page, it serves the page that is requested.
[Status : browser has authentication cookie
for both www.domain1.com and www.domain2.com].
So, if the user hits an authenticated page of www.domain2.com in the browser now, as the authentication cookie is already stored in the browser for www.domain2.com, it is sent with the request, and www.domain2.com retrieves the user information from the authentication cookie, logs in the user, and serves the page output that is requested by the user.
As the browser now has authentication cookie for both www.domain2.com and www.domain3.com, the user is considered to be logged onto both sites, and hence, Single Sign On implemented in both sites :)
How about "Single Sign Out"?
If we only had to manage logging onto the sites for users, we would have been done so far. But as part of the Single Sign On, we also have to manage the "Single Sign Out". That is, when a user signs out of a single site, the user should be marked as signed out of all the sites.
Internally, when the user signs out of a single site (say, www.domain1.com), the authentication cookie of all the other sites needs to be removed from the browser (in this case, www.domain2.com). A cookie can be removed from the browser by removing the cookie from the response before sending the response to the browser.
To remove all authentication cookies for all sites, the same flow of request-redirect-response described above should be followed, and instead of setting the authentication cookie, this time, the authentication cookie should be removed from the response.
Problem with this SSO model
This model should work fine for two sites. For logging into a site and for logging out of a site, the corresponding site internally should follow a request-redirect-response flow with all other sites (that are under the SSO). Once the user is logged onto a single site, for all other subsequent requests onto any site, there is no request-redirect-response loop because each site has its own authentication cookie stored in the browser.
But, if the number of sites is more than 2, the complexity increases. That is, if the user logs onto www.domain1.com, this site will internally redirect to www.domain2.com and www.domain3.com for setting the authentication cookie in the browser, and finally www.domain3.com redirects to www.domain1.com and serves the originally requested page output to the browser. This will make the login and logout functionality of each site costly and complex. What if there are more than 3 sites? What if we have 20+ sites to integrate under a Single Sign On model which are deployed under different domains? The model is obviously not going to fit.
This model also requires each site to have knowledge about all the other sites which are configured under the same SSO umbrella (because, each site has to redirect to all the other sites for setting the authentication cookie). Also, this would require the user authentication and authorization logic to be implemented on each site.
It's clearly understandable, as the number of sites increases, this SSO model will become messy, and will lose its acceptance, and this model can't really be considered a "generic" SSO model for cross-domain applications. So, we need a better model that should be independent of the number of sites to integrate under a Single Sign On umbrella.
A proposed model for Cross-Domain SSO
The previous model was kind of a "mesh-up" where each site had to be redirected to N-1 other sites for setting and removing authentication cookie for signing in and signing out. Each site had to be configured with knowledge of the other N-1 sites, and some complex and expensive login and logout logic had to be implemented.
What if we maintained a single authentication cookie for tracking user authentication for all sites? What about introducing a new dedicated site for authenticating users and setting the authentication cookie? Sounds lucrative.
In order to implement SSO for multiple sites, the user database should be a unified one, and hence, it makes good sense to implement the user authentication and authorization functionality within a dedicated site where each site can access the functionality via a web or WCF service. This allows the sites to get rid of redundant user authentication/authorization functionality. But the most important question is how this dedicated site is going to help implement Single Sign On.
Well, in this model, the browser will not store authentication cookies for each site. Rather, it will store an authentication cookie for only the dedicated site which will be used by the other sites to implement Single Sign On. Let's call this site www.sso.com.
In this model, each and every request to any 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 an authentication cookie. If the cookie is found, the authenticated pages are served to the browser, and if not, the user is redirected to the login page of the corresponding site.
The following diagram depicts the basic idea:
A dedicated site (www.sso.com) for managing the authentication cookie in the proposed SSO model
For understanding the model in detail, let's imagine we are trying to apply this model to implement SSO to the following two sites:
- www.domain1.com
- www.domain2.com
We now have a dedicated site for managing the authentication cookie: www.sso.com.
Here is how this model works:
Sequence diagram of an authenticated page hit without login
Sequence of operations:
- User hits a URL of an authenticated page of www.domain1.com.
- www.domain1.com redirects the request to www.sso.com, adding a
ReturnUrl
query string parameter set to the originally requested URL.
- www.sso.com checks if there is any authentication cookie, or if there is any user
Token
in the request. There isn't any. So, it redirects to the site www.domain1.com with an indication in the URL that the user needs to log in. It also appends the ReturnUrl
parameter value with the query string.
- www.domain1.com checks the query string parameters that is just being redirected from www.sso.com. The query string has indication that the user authentication cookie is not found, and hence, it redirects the current request to the www.domain1.com's login page, along with an indication that this request should not be redirected to www.sso.com.
Sequence diagram of login
- User provides credentials and hits the "Login" button. No postback request is redirected to the SSO site in this model. This time, www.domain1.com invokes the web/WCF service of www.sso.com to check the provided user credentials, and upon success, returns the user object with a
Token
property that is generated each time a user logs in (say, a GUID
).
- www.domain1.com marks the user as logged in (say, stores the user object in the session), prepares a URL with the user
Token
, and redirects the current request to www.sso.com to set the authentication cookie, with the ReturnUrl
parameter value being set to the original request URL.
- www.sso.com checks the incoming request URL, and finds a user
Token
, with no authentication cookie being available yet. That indicates the user has been authenticated into the client site (www.domain1.com), and the authentication cookie now needs to be set in the browser for www.sso.com. So, it retrieves the user from the cache using the Token
, prepares an authentication cookie with the user info and sets the other cookie properties (expiration date/time etc.), adds the cookie to the response, and finally redirects to the originally requested URL found in the ReturnUrl
query string value (a URL of www.domain1.com), along with adding the Token
value in the query string.
- Browser gets a Redirect command now with
ReturnUrl
to www.domain1.com, and also gets an authentication cookie from www.sso.com. So it stores the authentication cookie for www.sso.com and hits a request to the URL at www.domain1.com.
- www.domain1.com now sees that a user
Token
is present in the query string. It validates the user token via a web/WCF service call at www.sso.com, and upon validation, executes the originally requested page of www.domain1.com and writes the output in the response.
Sequence diagram of browsing an authenticated page after login
- User hits an authenticated page URL of www.domain2.com now.
- www.domain2.com redirects the request to www.sso.com in the client's browser, setting the
ReturnUrl
property to the originally requested URL of www.domain2.com.
- The browser gets a redirect command to www.sso.com. It already has an authentication cookie stored for this site, and hence adds this cookie to the request before sending the request to www.sso.com.
- www.sso.com checks the incoming request and looks for any authentication cookie in the request. If finds an authentication cookie this time, and checks whether the cookie has expired. If not, it retrieves the user
Token
from the cookie and redirects the request to the originally requested page URL at www.domain2.com, setting the user Token
in the query string.
- www.domain2.com sees that there is a user
Token
presented in the query string. So it validates the user token via a web/WCF service call at www.sso.com, and upon validation, executes the originally requested page of www.domain2.com and writes the output in the response.
To summarize
Wow, sounds a lot of things are happening! Let me summarize those here:
Initially, the browser doesn't have any authentication cookie for www.sso.com. So, hitting any authenticated page in the browser for www.domain1.com or www.domain2.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, 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 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.
Traffic
Following are the summary of traffic for each different scenario:
Scenario1: A public page hit
A trip from the browser to the client site + a trip from the client site to the browser
Summary: 1 Request + 1 Response
Scenario2: An authenticated page hit without login
A trip from the browser to the client site (page hit) + a redirect command to the browser and a trip from the browser to the SSO site (redirect to SSO for cookie check) + a redirect command to the browser and a trip from the browser to the client site (redirect to client site without cookie) + a trip from the client site to the browser (login page)
Summary: 1 Request + 2 Redirects + 1 Response
Scenario3: Login
A trip from the browser to the client site (postback) + an invocation of the authentication Web Service (user authentication) + a redirect command to the browser and and a trip from the browser to the SSO site (redirect to SSO with Token) + a redirect command to the browser and a trip from the browser to the client site (setting the authentication cookie) + an invocation of a Web Service method (Token validation) + a trip from the client site to the browser (serve the authenticated page)
Summary: 1 Request + 2 Web Service calls at the server + 2 Redirects + 1 Response
Scenario4: Browse an authenticated page while logged in
A trip from the browser to the client site (URL hit) + a redirect command to the browser and a trip from the browser to the SSO site (redirect to SSO with the authentication cookie) + a redirect command to the browser and a trip from the browser to the client site (checking the authentication cookie) + an invocation of a Web Service method (Token validation) + a trip from the client site to the browser (serve the page)
Summary: 1 Request + 2 Redirect + 1 Web Service call at the server + 1 Response
Scenario5: Logout
A trip from the browser to the client site (click logout link) + a redirect command to the browser and a trip from the browser to the SSO site (instruction to log out) + a redirect command to the browser and a trip from the browser to the client site (after removing the authentication cookie) + a trip from the client site to the browser (serve the login page)
Summary: 1 Request + 2 Redirects + 1 Response
Trade offs
Comparing the basic SSO model and the proposed one, it seems the basic model suits well for 2, or, at most 3 sites. In the case of the basic SSO model, the login and logout functionality will be complex in terms of implementation and execution time, but the subsequent pages would be served in a normal request-response cycle (1 request and 1 response).
However, extending the SSO is difficult (addition of new sites), configuration management is maximum (each site has to manage their own cookie and each has dependency on other sites), and redundant user authentication functionality has to be implemented on multiple sites.
On the other hand, the proposed model does not have any dependency on the number of sites to integrate (you can add as many sites as you want and the SSO model won't change). The configuration management is minimum for the participating site (no site has to know about the other sites, they need only know about the SSO site), the authentication and cookie management is handed over to a dedicated site (www.sso.com), the SSO related functionality can be easily integrated with the site if implemented smartly, and could be extended easily (new sites could be added easily).
However, there is a small performance penalty. Unlike the basic model, each authenticated page access requires 3 requests from the browser to the SSO site and the client site (the two additional requests are due to the two internal redirects). Though, the additional two requests take minimum execution time (a blank redirect request, just for setting and checking the cookie), and hence could be considered acceptable considering today's high Internet bandwidth availablilty.
Implementation of the proposed SSO model
Enough theories, and we should be prepared well enough now to get our hands dirty with some code. Good that I already have done it and have shared it in the next article. The following article would present an actual implementation of the proposed SSO model for Cross-Domain ASP.NET sites, and would point out some implementation challenges and their workarounds: