SSO解决方案大全 Single Sign-On for everyone

      前段时间为我们的系统做SSO(单点登录)参考了很多资料,其中包括博客园二级域名的登录.翻译本文是由于作者的一句话:思想都是一样的,只不过实现起来需要创造性思维. 


 Single Sign-On (SSO)是近来的热门话题. 很多和我交往的客户中都有不止一个运行在.Net框架中的Web应用程序或者若干子域名.而他们甚至希望在不同的域名中也可以只登陆一次就可以畅游所有站点.今天我们关注的是如何在各种不同的应用场景中实现 SSO. 我们由简到繁,逐一攻破.

  1. 虚拟目录的主应用和子应用间实现SSO
  2. 使用不同验证机制实现SSO  (username mapping)
  3. 同一域名中,子域名下的应用程序间实现SSO
  4. 运行在不同版本.NET下的应用程序间实现SSO
  5. 两个不同域名下的Web应用程序间实现SSO
  6. 混合身份验证方式模式 (Forms and Windows)下实现SSO

 

1. 虚拟目录的主应用和子应用之间实现SSO

       假设有两个.NetWeb应用程序-FooBar,Bar运行在Foo虚拟目录的子目录(http://foo.com/bar).二者都实现了Forms认证.实现Forms认证需要我们重写Application_AuthenticateRequest,在这个时机我们完成认证一旦通过验证就调用一下FormsAuthentication.RedirectFromLoginPage.这个方法接收的参数是用户名或者其它的一些身份信息.Asp.net中登录用户的状态是持久化存储在客户端的cookie.当你调用RedirectFromLoginPage时就会创建一个包含加密令牌FormsAuthenticationTicketcookie,cookie名就是登录用户的用户名.下面的配置节在Web.config定义了这种cookie如何创建:

<authentication mode="Forms">

   <forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" />

</authentication>

 <authentication mode="Forms">

   <forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" />

</authentication>

比较重要的两个属性是 name protection. 按照下面的配置就可以让FooBar两个程序在同样的保护级别下读写Cookie,这就实现了SSO的效果:

<authentication mode="Forms">

   <forms name=".SSOAuth" protection="All" timeout="60" loginUrl="login.aspx" />

</authentication>

protection属性设置为 "All",通过Hash值进行加密和验证数据都存放在Cookie.默认的验证和加密使用的Key都存储在machine.config文件,我们可以在应用程序的Web.Config文件覆盖这些值.默认值如下:

<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey=" AutoGenerate,IsolateApps" validation="SHA1" />

IsolateApps表示为每个应用程序生成不同的Key.我们不能使用这个.为了能在多个应用程序中使用相同的Key来加密解密cookie,我们可以移除IsolateApps 选项或者更好的方法是在所有需要实现SSO的应用程序的Web.Config中设置一个具体的Key:  

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" />

如果你使用同样的存储方式,实现SSO只是改动一下Web.config而已.

 

2.使用不同认证机制实现SSO (username mapping)

     要是FOO站点使用database来做认证,Bar站点使用Membership API或者其它方式做认证呢?这种情景中FOO站点创建的cookieBar站点毫无用处,因为cookie中的用户名对Bar没有什么意义.

   要想cookie起作用,你就需要再为Bar站点创建一个认证所需的cookie.这里你需要为两个站点的用户做一下映射.假如有一个Foo站点的用户"John Doe"Bar站点需要识别成"johnd".Foo站带你你需要下面的代码:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");

HttpCookie cookie = new HttpCookie(".BarAuth");

cookie.Value = FormsAuthentication.Encrypt(fat);

cookie.Expires = fat.Expiration;

HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("John Doe");

为了演示用户名硬编码了.这个代码片段为Bar站点创建了令牌FormsAuthenticationTicket ,这时令牌里的用户名在Bar站点的上下文中就是有意义的了. 这时再调用 RedirectFromLoginPage创建正确的认证cookie.上面的例子你统一了了Forms 认证的cookie名字,而这里你要确保他们不同--因为我们不需要两个站点共享相同的cookie:

<authentication mode="Forms">

   <forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>

</authentication>

 <authentication mode="Forms">

   <forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>

</authentication>

现在当用户在Foo站点登录,他就会被映射到到Bar站点的用户并同时创建了FooBar两个站点的认证令牌.如果你想在Bar站点登录在Foo站点通行,那么代码就会是这样:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");

HttpCookie cookie = new HttpCookie(".FooAuth");

cookie.Value = FormsAuthentication.Encrypt(fat);

cookie.Expires = fat.Expiration;

HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("johnd");

同样要保证两个站点的Web.config<machineKey>配置节有相同的加密和解密的Key!

 

3. 同一域名中,各子域名下应用程序间实现SSO

        要是这样的情况又将如何:Foo Bar两个站点运行在不同的域名下: http://foo.com and http://bar.foo.com. 上面的代码又不起作用了:因为cookie会存储在不同的文件中,各自的cookie对其它网站不可见.为了能让它起作用我们需要创建域级cookie,因为域级cookie对子域名都是可见的!这里我们也不能再使用 RedirectFromLoginPage 方法了,因为它不能灵活的创建域级cookie我们需要手工完成这个过程!

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");

HttpCookie cookie = new HttpCookie(".BarAuth");

cookie.Value = FormsAuthentication.Encrypt(fat);

cookie.Expires = fat.Expiration;

cookie.Domain = ".foo.com";

HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");

HttpCookie cookie = new HttpCookie(".FooAuth");

cookie.Value = FormsAuthentication.Encrypt(fat);

cookie.Expires = fat.Expiration;

cookie.Domain = ".foo.com";

HttpContext.Current.Response.Cookies.Add(cookie);

注意cookie.Domain = ".foo.com";注意这一行.这里明确指定了cookie的域名为".foo.com",这样我们就保证了cookie http://foo.com http://bar.foo.com 以及其它子域名都是可见的.(译者注:cookie的域名匹配规则是从右到左) .你可以通过设置Bar站点的认证cookie的域名为"bar.foo.com".这样对于其它子域名的站点它的cookie也是不可见的,这样安全了.注意 RFC 2109 要求cookie前面有两个周期所以我们添加了一个过期时间.(cookie值实际上是一个字符串,各参数用逗号隔开).

再次提醒,这里还是需要统一一下各个站点的Web.config<machineKey>配置节的Key. 这种解决方案只有一种异常的情况,且看下节详解.

 

4. 运行在不同版本.Net下应用程序间实现SSO

       要是FooBar站点运行在不同的.Net环境中上面的例子都行不通.这是由于Asp.net 2.0使用了不同于1.1的加密算法:1.1版本使用的是3DES,2.0AES.万幸,Asp.net2.0中有一个属性可以兼容1.1:

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" decryption="3DES" />

设置decryption="3DES"就会让 ASP.NET 2.0使用旧版本的加密算法使cookie能够正常使用.不要企图在Asp.net1.1Web.config文件中添加这个属性,那会报错.

 

5. 两个不同域名下的应用程序实现SSO

        我们已经成功的创建了可以共享的认证Cookie,但是如果Foo站点和Bar站点在不同域名下呢,例如: http://foo.com http://bar.com? 他们不能共享cookie也不能为对方在创建一个可读的cookie.这种情况下每个站点需要创有各自的cookie,调用其它站点的页面来验证用户是否登录.其中一种实现方式就是使用一系列的重定向.

      为了实现上述目标,我们需要在每个站点都创建一个特殊的页面(比如:sso.aspx).这个页面的作用就是来检查该域名下的cookie是否存在并返回已经登录用户的用户名.这样其它站点也可以为这个用户创建一个cookie.下面是Bar.comsso.aspx:

Bar.com:

<%@ Page Language="C#" %>

<script language="C#" runat="server">

void Page_Load()

{

   // this is our caller, we will need to redirect back to it eventually

   UriBuilder uri = new UriBuilder(Request.UrlReferrer);

   HttpCookie c = HttpContext.Current.Request.Cookies[".BarAuth"];

   if (c != null && c.HasKeys) // the cookie exists!

   {

      try

      {

         string cookie = HttpContext.Current.Server.UrlDecode(c.Value);

         FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);         

         uri.Query = uri.Query + "&ssoauth=" + fat.Name; // add logged-in user name to the query

      }

      catch

      {

      }

   }

   Response.Redirect(uri.ToString()); // redirect back to the caller

}

</script>

这个页面总是重定向回调用的站点.如果Bar.com存在认证cookie,它就解密出来用户名放在ssoauth参数中.

另外一端(Foo.com),我们需要在HTTP Rquest处理的管道中添加一些的代码.可以是Web应用程序的 Application_BeginRequest 事件或者是自定义的HttpHandlerHttpModule.基本思想就是在所有Foo.com的页面请求之前做拦截,尽早的检查验证cookie是否存在:

1. 如果Foo.com的认证cookie已经存在,就继续处理请求,用户在Foo.com登录过

2. 如果认证Cookie不存在就重定向到Bar.com/sso.aspx.

3. 如果现在的请求是从Bar.com/sso.aspx重定向回来的,分析一下ssoauth参数如果需要就创建认证cookie.

路子很简单,但是又两个地方要注意死循环:

// see if the user is logged in

HttpCookie c = HttpContext.Current.Request.Cookies[".FooAuth"];

if (c != null && c.HasKeys) // the cookie exists!

{

   try

   {

      string cookie = HttpContext.Current.Server.UrlDecode(c.Value);

      FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);

      return; // cookie decrypts successfully, continue processing the page

   }

   catch

   {

   }

}

// the authentication cookie doesn't exist - ask Bar.com if the user is logged in there

UriBuilder uri = new UriBuilder(Request.UrlReferrer);

if (uri.Host != "bar.com" || uri.Path != "/sso.aspx") // prevent infinite loop

{

   Response.Redirect(http://bar.com/sso.aspx);

}

else

{

   // we are here because the request we are processing is actually a response from bar.com

   if (Request.QueryString["ssoauth"] == null)

   {

      // Bar.com also didn't have the authentication cookie

      return; // continue normally, this user is not logged-in 

   } else

   {

      // user is logged in to Bar.com and we got his name!

      string userName = (string)Request.QueryString["ssoauth"];

   

      // let's create a cookie with the same name

      FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddYears(1), true, "");

      HttpCookie cookie = new HttpCookie(".FooAuth");

      cookie.Value = FormsAuthentication.Encrypt(fat);

      cookie.Expires = fat.Expiration;

      HttpContext.Current.Response.Cookies.Add(cookie);

   }

}

          同样的代码两个站点都要有,确保你使用了正确的cookie名字(.FooAuth vs. .BarAuth) . 因为cookie并不是真正意义上的共享,因为Web应用程序的有不同的<machineKey>配置节. 这里没有必要统一加密和解密的Key.

         有些人把在url里面把用户名当作参数传递视为畏途.实际上有两件事情可以做来保护:首先我们可以检查引用页参数不接受bar.com/sso.aspx (or foo.com/ssp.aspx)以外的站点.其次,用户名可以可以通过相同的Key做一下加密.如果FooBar使用不同的认证机制,额外的用户信息(比如email地址)同样也可以传递过去.

 

6. 混合身份验证模式下 (Forms and Windows)实现SSO

       上面我们都是处理的Forms认证.要是我们这样设计认证过程呢:先做Forms认证,如果没有通过就检查Intranet用户是否已经在NT域上登录过了.这个思路我们需要检查下面的参数来看和请求关联的Windows logo信息:

Request.ServerVariables["LOGON_USER"]

      但是除非我们的站点都是禁用匿名登录的,否则这个值总是空的.我们可以在IIS的控制面板禁用匿名登录并为我们的站点启用Windows集成认证.这样LOGON_USER 值就包含了NT域登录用户的名字.但是所有Internet用户的都会遇到用户名和密码的难题,这就不好了,我们要让Internet用户使用Forms认证要是这种方式失败了再使用Windows域认证.

       这个问题的解决方法之一就是为Intranet用户设置一个特殊的入口页面:Windows集成认证方式可用,验证域用户,创建Forms cookie重定向到主站点.我们甚至可以隐藏这样一个事实:由于Server.TransferIntranet用户实际上访问了不同的页面.

     也有一个简单的解决方法.这个方法的基础是IIS掌控认证处理.如果站点对匿名用户可用,IIS就把请求传递给Asp.net运行时.并试图进行认证要是失败了就引发一个401错误.IIS会试图寻找另外该站点的其它认证方式 .你要设置匿名访问和集成认证可用并在Forms认证失败之后执行下面的代码:

if (System.Web.HttpContext.Current.Request.ServerVariables["LOGON_USER"] == "") { 

   System.Web.HttpContext.Current.Response.StatusCode = 401; 

   System.Web.HttpContext.Current.Response.End();

}

else

{

   // Request.ServerVariables["LOGON_USER"] has a valid domain user now!

}

这段代码执行时,它会检查域用户并取得一个空的初始值.这回终止当前请求并返回认证的401错误到IIS.这就让IIS自动选择另外的认证机制,Windows集成认证方式就是候选方式.如果用户可以登录到域,请求就可以继续,并附加上了NT域用户的信息. 如果用户没有在域中登录会有三次输入用户名密码的机会.如果三次失败他就会得到一个403错误(AccessDenied).

结论

      我们考查了在各种场景中在两个Asp.net应用程序间实现SSO.我们也可以在不同系统不同平台间实现SSO,思想都是一样的,只不过实现起来需要创造性思维. 

 

 

Single Sign-On for everyone

Single Sign-On (SSO) is a hot topic these days. Most clients I worked with have more than one web application running under different versions of .NET framework in different subdomains, or even in different domains and they want to let the user login once and stay logged in when switching to a different web site. Today we will implement SSO and see if we can make it work in different scenarios. We will start with a simple case and gradually build upon it:

  1. SSO for parent and child application in the virtual sub-directory
  2. SSO using different authorization credentials (username mapping)
  3. SSO for two applications in two sub-domains of the same domain
  4. SSO when applications run under different versions of .NET
  5. SSO for two applications in different domains.
  6. SSO for mixed-mode authentication (Forms and Windows)

1. SSO for parent and child application in the virtual sub-directory

Lets assume that we have two .NET applications - Foo and Bar, and Bar is running in a virtual sub-directory of Foo (http://foo.com/bar). Both applications implement Forms authentication. Implementation of Forms authentication requires you to override the Application_AuthenticateRequest, where you perform the authentication and upon successful authentication, call FormsAuthentication.RedirectFromLoginPage, passing in the logged-in user name (or any other piece of information that identifies the user in the system) as a parameter. In ASP.NET the logged-in user status is persisted by storing the cookie on the client computer. When you call RedirectFromLoginPage, a cookie is created which contains an encrypted FormsAuthenticationTicket with the name of the logged-in user . There is a section in web.config that defines how the cookie is created:

<authentication mode="Forms">
   <
forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

 <authentication mode="Forms">
   <
forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

 

The important attributes here are name and protection. If you make them match for both Foo and Bar applications, they will both write and read the same cookie using the same protection level, effectively providing SSO:

<authentication mode="Forms">
   <
forms name=".SSOAuth" protection="All" timeout="60" loginUrl="login.aspx" />
</
authentication>

 

When protection attribute is set to "All", both encryption and validation (via hash) is applied to the cookie. The default validation and encryption keys are stored in the machine.config file and can be overridden in the application’s web.config file. The default value is this:

<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey=" AutoGenerate,IsolateApps" validation="SHA1" />

 

IsolateApps means that a different key will be generated for every application. We can’t have that. In order for the cookie to be encrypted and decrypted with the same key in all applications either remove the IsolateApps option or better yet, add the same concrete key to the web.config of all applications using SSO:

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" />

 

If you are authenticating against the same users store, this is all it takes – a few changes to the web.config files.

2. SSO using different authorization credentials (username mapping)

 

But what if the Foo application authenticates against its own database and the Bar application uses Membership API or some other form of authentication? In this case the automatic cookie that is created on the Foo is not going to be any good for the Bar, since it will contain the user name that makes no sense to the Bar.

To make it work, you will need to create the second authentication cookie especially for the Bar application. You will also need a way to map the Foo user to the Bar user. Lets assume that you have a user "John Doe" logging in to the Foo application and you determined that this user is identified as "johnd" in the Bar application. In the Foo authentication method you will add the following code:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".BarAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("John Doe");

 

User names are hard-coded for demonstration purposes only. This code snippet creates the FormsAuthenticationTicket for the Bar application and stuffs it with the user name that makes sense in the context of the Bar application. And then it calls RedirectFromLoginPage to create the correct authentication cookie for the Foo application. If you changed the name of forms authentication cookie to be the same for both applications (see our previous example), make sure that they are different now, since we are not using the same cookie for both sites anymore:

<authentication mode="Forms">
   <
forms name=".FooAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>
</
authentication>

 <authentication mode="Forms">
   <
forms name=".BarAuth" protection="All" timeout="60" loginUrl="login.aspx" slidingExpiration="true"/>
</
authentication>

 

Now when the user is logged in to Foo, he is mapped to the Bar user and the Bar authentication ticket is created along with the Foo authentication ticket. If you want it to work in the reverse direction, add similar code to the Bar application:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".FooAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
HttpContext.Current.Response.Cookies.Add(cookie);

FormsAuthentication.RedirectFromLoginPage("johnd");

 

Also make sure that you have the <machineKey> element in web.config of both applications with matching validation and encryption keys.

3. SSO for two applications in two sub-domains of the same domain

 

Now what if Foo and Bar are configured to run under different domains http://foo.com and http://bar.foo.com. The code above will not work because the cookies will be stored in different files and will not be visible to both applications. In order to make it work, we will need to create domain-level cookies that are visible to all sub-domains. We can’t use RedirectFromLoginPage method anymore, since it doesn’t have the flexibility to create a domain-level cookie. So we do it manually:

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "johnd", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".BarAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
cookie.Domain =
".foo.com";
HttpContext.Current.Response.Cookies.Add(cookie);

 

FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, "John Doe", DateTime.Now, DateTime.Now.AddYears(1), true, "");
HttpCookie cookie = new HttpCookie(".FooAuth");
cookie.Value =
FormsAuthentication.Encrypt(fat);
cookie.Expires = fat.Expiration;
cookie.Domain =
".foo.com";
HttpContext.Current.Response.Cookies.Add(cookie);

 

Note the highlighted lines. By explicitly setting the cookie domain to ".foo.com" we ensure that this cookie will be visible in both http://foo.com and http://bar.foo.com or any other sub-domain. You can also specifically set the Bar authentication cookie domain to "bar.foo.com". It is more secure, since other sub-domains can’t see it now. Also notice that RFC 2109 requires two periods in the cookie domain value, therefore we add a period in the front – ".foo.com"

Again, make sure that you have the same <machineKey> element in web.config of both applications. There is only one exception to this rule and it is explained in the next secion.

4. SSO when applications run under different versions of .NET

 

It is possible that Foo and Bar applications run under different version of .NET. In this case the above examples will not work. It turns out that ASP.NET 2.0 is using a different encryption method for authorization tickets. In ASP.NET 1.1 it was 3DES, in ASP.NET 2.0 it is AES. Fortunately, a new attribute was introduced in ASP.NET 2.0 for backwards compatibility

<machineKey validationKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902" decryptionKey="F9D1A2D3E1D3E2F7B3D9F90FF3965ABDAC304902F8D923AC" validation="SHA1" decryption="3DES" />

 

Setting decryption="3DES" will make ASP.NET 2.0 use the old encryption method, making the cookies compatible again. Don’t try to add this attribute to the web.config of the ASP.NET 1.1 application. It will cause an error.

5. SSO for two applications in different domains.

 

We’ve been quite successful creating shared authentication cookies so far, but what if Foo and Bar are in different domains – http://foo.com and http://bar.com? They cannot possibly share a cookie or create a second cookie for each other. In this case each site will need to create its own cookies, and call the other site to verify if the user is logged in elsewhere. One way to do it is via a series of redirects.

In order to achieve that, we will create a special page (we’ll call it sso.aspx) on both web sites. The purpose of this page is to check if the cookie exists in its domain and return the logged in user name, so that the other application can create a similar cookie in its own domain. This is the sso.aspx from Bar.com:

<%@ Page Language="C#" %>

 

<script language="C#" runat="server">

 

 

void Page_Load()
{
   
// this is our caller, we will need to redirect back to it eventually
   
UriBuilder uri = new UriBuilder(Request.UrlReferrer);

   HttpCookie c = HttpContext.Current.Request.Cookies[".BarAuth"];

   if (c != null && c.HasKeys) // the cookie exists!
   {
      
try
      
{
         
string cookie = HttpContext.Current.Server.UrlDecode(c.Value);
         
FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);         

         uri.Query = uri.Query + "&ssoauth=" + fat.Name; // add logged-in user name to the query
      
}
      
catch
      
{
      }
   }
   Response.Redirect(uri.ToString());
// redirect back to the caller
}

 

</script>

 

This page always redirects back to the caller. If the authentication cookie exists on Bar.com, it is decrypted and the user name is passed back in the query string parameter ssoauth.

On the other end (Foo.com), we need to insert some code into the http request processing pipeline. It can be in Application_BeginRequest event or in a custom HttpHandler or HttpModule. The idea is to intercept all page requests to Foo.com as early as possible to verify if authentication cookie exists:

1. If authentication cookie exists on Foo.com, continue processing the request. User is logged in on Foo.com
2. If authentication cookie doesn’t exist, redirect to Bar.com/sso.aspx.
3. If the current request is the redirect back from Bar.com/sso.aspx, analyse the ssoauth parameter and create an authentication cookie if necessary.

It looks pretty simple, but we have to watch out for infinite loops:

// see if the user is logged in
HttpCookie c = HttpContext.Current.Request.Cookies[".FooAuth"];

 

if (c != null && c.HasKeys) // the cookie exists!
{
   try
   
{
      string cookie = HttpContext.Current.Server.UrlDecode(c.Value);
      FormsAuthenticationTicket fat = FormsAuthentication.Decrypt(cookie);
      return; // cookie decrypts successfully, continue processing the page
   
}
   
catch
   
{
   
}
}

 

// the authentication cookie doesn't exist - ask Bar.com if the user is logged in there
UriBuilder uri = new UriBuilder(Request.UrlReferrer);

 

if (uri.Host != "bar.com" || uri.Path != "/sso.aspx") // prevent infinite loop
{
   Response.Redirect(
http://bar.com/sso.aspx);
}
else
{
   // we are here because the request we are processing is actually a response from bar.com

 

   if (Request.QueryString["ssoauth"] == null)
   {
      // Bar.com also didn't have the authentication cookie
      
return; // continue normally, this user is not logged-in 
   
} else
   
{

      // user is logged in to Bar.com and we got his name!
      
string userName = (string)Request.QueryString["ssoauth"];
   
      
// let's create a cookie with the same name
      
FormsAuthenticationTicket fat = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddYears(1), true, "");
      
HttpCookie cookie = new HttpCookie(".FooAuth");
      cookie.Value =
FormsAuthentication.Encrypt(fat);
      cookie.Expires = fat.Expiration;
      
HttpContext.Current.Response.Cookies.Add(cookie);
   
}
}

 

The same code should be placed on both sites, just make sure you are using correct cookie names (.FooAuth vs. .BarAuth) on each site. Since the cookie is not actually shared, the applications can have different <machineKey> elements. It is not necessary to synchronize the encryption and validation keys.

Some of us may cringe at the security implications of passing the user name in the query string. A couple of things can be done to protect it. First of all we are checking the referrer and we will not accept the ssoauth parameter from any source other then bar.com/sso.aspx (or foo.com/ssp.aspx). Secondly, the name can easily be encrypted with a shared key. If Foo and Bar are using different authentication mechanisms, additional user information (e.g. e-mail address) can be passed along similarly.

6. SSO for mixed-mode authentication (Forms and Windows)

 

So far we have dealt with Forms authentication only. But what if we want to authenticate Internet users via Forms authentication first and if it fails, check if the Intranet user is authenticated on the NT domain? In theory we can check the following parameter to see if there is a Windows logon associated with the request:

Request.ServerVariables["LOGON_USER"]

 

However, unless our site has Anonymous Access disabled, this value is always empty. We can disable Anonymous Access and enable Integrate Windows Authentication for our site in the IIS control panel. Now the LOGON_USER value contains the NT domain name of the logged in Intranet user. But all Internet users get challenged for the Windows login name and password. Not cool. We need to be able to let the Internet users login via Forms authentication and if it fails, check their Windows domain credentials.

One way to solve this problem is to have a special entry page for Intranet users that has Integrate Windows Authentication enabled, validates the domain user, creates a Forms cookie and redirects to the main web site. We can even conceal the fact that Intranet users are hitting a different page by making a Server.Transfer.

There is also an easier solution. It works because of the way IIS handles the authentication process. If anonymous access is enabled for a web site, IIS is passing requests right through to the ASP.NET runtime. It doesn’t attempt to perform any kind of authentication. However, if the request results in an authentication error (401), IIS will attempt an alternative authentication method specified for this site. You need to enable both Anonymous Access and Integrated Windows Authentication and execute the following code if Forms authentication fails:

if (System.Web.HttpContext.Current.Request.ServerVariables["LOGON_USER"] == "") { 
   System.Web.HttpContext.Current.Response.StatusCode = 401; 
   System.Web.HttpContext.Current.Response.End();
}
else
{
   
// Request.ServerVariables["LOGON_USER"] has a valid domain user now!
}

 

When this code executes, it will check the domain user and will get an empty string initially. It will then terminate the current request and return the authentication error (401) to IIS. This will make the IIS use the alternative authentication mechanism, which in our case is Integrated Windows Authentication. If the user is already logged in to the domain, the request will be repeated, now with the NT domain user information filled-in. If the user is not logged in to the domain, he will be challenged for the Windows name/password up to 3 times. If the user cannot login after the third attempt, he will get the 403 error (access denied).

Conclusion

 

We have examined various scenarios of Single Sign-On for two ASP.NET applications. It is also quite possible to implement SSO for heterogeneous systems spawning across different platforms. Ideas remain the same, but the implementation may require some creative thinking.

posted @ 2008-09-28 11:06  失恋的混蛋  阅读(469)  评论(0编辑  收藏  举报