Fixing SharePoint, Claims Authentication, and Sign Out

One of my clients has a project that includes claims based authentication with a custom identity provider (IP-STS). This part of the project has been helped by the fact that the documentation gets better each month – sometimes it seems as if the question we have was documented a few days before we needed to ask it!

There is a lot of documentation in the Windows Identity Foundation SDK and in the Identity Developer Training Kit. But in my opinion, the best reference material to date is in the book A Guide to Claims–based Identity and Access Control from Microsoft Patterns and Practices. The companion code for this book is the most complete and well documented example of many scenarios I’ve seen. In particular, the first sample for Single Sign On was most recently helpful to understand what should happen when a user signs out via claims based SSO – something that SharePoint 2010 does incorrectly at the moment! This post is about what should happen and the approach I took to make this work seamlessly with both our custom sites and stock sites that use the SSO.

The Problem

If you use the Sign Out or Sign in as a Different User links in the welcome menu you will most likely be redirected back to the sites home page instead of signing out! This happens based on the following flow.

  1. You sign in to the IP-STS which writes a persisted authentication cookie for the STS on your machine.
  2. You are redirected to the SharePoint site which (by default) writes a persisted authentication cookie for the SharePoint site on your machine.
  3. You click Sign Out whereupon SharePoint deletes the authentication cookie for the SharePoint site and redirects you to the STS (via the SharePoint Sign On page).
  4. The STS uses the authentication cookie it stored on the machine to determine you are already logged in and sends you back to the SharePoint site.

The problem is that SharePoint should redirect you to the identity provider with the query string parameter wa=wsignout1.0. Upon receiving this parameter, the STS will delete its cookie and optionally reply to the relying party via an HTML <img> tag whose src attribute is the relying party with query string parameter wa=wsignoutcleanup1.0. When the RP, in this case SharePoint receives a GET for the URL with this parameter it should delete whatever cookies it must to clean up the session. In the SSO sample that comes from patterns and practices the Sign Out page looks like this:

The check mark is the result of the img tag! <img title="Signout request: http://sp2010:8088/_trust/default.aspx?wa=wsignoutcleanup1.0" src="http://sp2010:8088/_trust/default.aspx?wa=wsignoutcleanup1.0"/> (obvious right?)

A Solution

You can write your own sign out control, but we require the traditional welcome links to work. We need SharePoint sites without our customizations to work as well. One option is to replace the welcome.ascx control, but this is a bad solution – you shouldn’t replace built-in files with your own versions of the same.

The first solution I tried was based on HideCustomAction and CustomAction features to change out the links, but although you can add links to the welcome links menu, the control itself sets the visibility of the stock links in its code-behind and ignores any HideCustomActions! One bug was keeping me from addressing the other!

The actual behavior for Sign Out is in _layouts/signout.aspx. For Sign In as a Different user the page is _layouts/CloseConnection.aspx. Since I could not easily affect the links in the UI I decided to create a simple HttpModule whose salient code is shown below. This code simply redirects to a new page, ClaimsSignOut.aspx if either of the original pages is requested.

void context_BeginRequest(object sender, EventArgs e)
{
var context = (HttpApplication)sender;
string url = context.Request.Url.ToString().ToLowerInvariant();
if(url.Contains(@"/signout.aspx"))
{
context.Response.AddHeader("Location",
url.Replace("/signout.aspx", "/ClaimsSignOut/ClaimsSignOut.aspx"));
context.Response.StatusCode = 301;
context.Response.End();
}
if(url.Contains(@"/closeconnection.aspx"))
{
context.Response.AddHeader("Location",
url.Replace("/closeconnection.aspx", "/ClaimsSignOut/ClaimsSignOut.aspx"));
context.Response.StatusCode = 301;
context.Response.End();
}
}

            

The implementation of ClaimsSignOut.aspx was created by copying SignOut.aspx and using IlSpy to decompile the code behind which I subsequently modified to fix the bug. The fix consists of a simple function to get the IP’s address from the current principal:

private string GetIPUrl()
{
IClaimsPrincipal user = Page.User as IClaimsPrincipal;
if(user == null) return string.Empty;

string ipUrl = string.Empty;
try
{
string providerName = (user.Identity.Name).Split('|')[1];
ipUrl = SPSecurityTokenServiceManager.Local.TrustedLoginProviders[providerName].ProviderUri.AbsoluteUri;
}
catch
{
//TODO:Log it
}
return ipUrl;
}

            

And the following modification to the page’s RemoveCookiesAndRedirect method.

if (iisSettingsWithFallback.UseClaimsAuthentication)
{
string ipUrl = GetIPUrl();
if(ipUrl != string.Empty)
{
string replyUrl = HttpUtility.UrlEncode(SPContext.Current.Site.RootWeb.Url);
string redirect = ipUrl + "?wa=wsignout1.0&wreply=" + replyUrl;

//This next line shouldn't be neccesary if the IP-STS is correct, but clears
//cookies just in case the STS doesn't call back to cleanup the session.
FederatedAuthentication.SessionAuthenticationModule.SignOut();

if (Context.Session != null) Context.Session.Abandon();

Context.Response.Redirect(redirect);
}
else
{
//Original code per reflection – DTW
FederatedAuthentication.SessionAuthenticationModule.SignOut();
int num = 0;

            

Hopefully there will be a hotfix for this at some point and this solution will no longer be needed! In the meantime, you can download the solution from here.

If you see any issues or can make any improvements, please let me know via the comments section!

–Doug

Author: Doug Ware