Custom Single Sign-On Scenario in SharePoint 2010

 

Here’s the scenario: a user navigates to a site whose homepage includes a link to the SharePoint 2010 site. Goal is, user gets into SharePoint 2010 site with no credentials prompt. Both source and destination sites use SQL as the membership repository.

Easy, right? First implementation was with an http module, getting the SQL user from the cookie user brings when coming from the source site, and calling SetAuthCookie(that username). With some tricks, like figuring out that username has to be on the form of "0#.f|membershipprovider-name|username", user got into the site.

But later on, some problems surfaced,

  • user could not logout correctly,
  • authorization issues i.e., users not being able to access some resources they were supposed to.

So I set out to investigate, and first thing I found  is that cookie generated by SetAuthCookie() (.ASPXAUTH) was different that of the default one you get when logged into SharePoint 2010 out of the box (FedAuth). Luckily, found the reason of this in this great article from the great Steve. That is

  • In SharePoint 2010, FBA users are claims users, therefore
  • SetAuthCookie() doesn’t do anymore.

Let’s now outline the solution. We need to pieces here to make this work

  • a http module that, if it sees an cookie that shows user is authenticated, sets cookie for the SharePoint 2010 site and
  • a custom membership provider that, if a cookie value comes in place of a password, then validates the user

(BIG warning here – we are passing to the custom membership provider, in the http module, as password the cookie value. This means, mitigations should be put in place in order to prevent users to, for example, put a long cookie value in a custom login page. For example, we could hash the cookie in the http module and verify that hash in the custom membership provider.)

First of all, we need to set the authentication cookie the right way – that is, leveraging SharePoint claims API’s. I chose to do that from an http module’s AuthenticateRequest:

public void Init(HttpApplication context)
{
    context.AuthenticateRequest += new EventHandler(OnAuthenticateRequest);
}

void OnAuthenticateRequest(object sender, EventArgs e)
{
    HttpContext context = HttpContext.Current;
    HttpRequest request = context.Request;
    HttpCookie cookieVal = request.Cookies[Constants.SAPCookieName];
    if (request.IsAuthenticated)
    {
        return;
    }

    if (cookieVal != null && !String.IsNullOrEmpty(cookieVal.Value))
    {
        string username = GetUsernameFromCookie(cookieVal);
        if (!String.IsNullOrEmpty(username))
        {
            SetFBACookie(request, username, cookieVal.Value);
        }
    }
}

private static void SetFBACookie(HttpRequest r, string username, string cookieValue)
{
    using (SPSite s = new SPSite(r.Url.AbsoluteUri))
    {
        SPIisSettings iis = s.WebApplication.IisSettings[SPUrlZone.Default];
        SPFormsAuthenticationProvider fap = iis.FormsClaimsAuthenticationProvider;
        SecurityToken token = SPSecurityContext.SecurityTokenForFormsAuthentication(new Uri(r.Url.GetComponents(UriComponents.Scheme | UriComponents.HostAndPort, UriFormat.Unescaped), UriKind.Absolute), fap.MembershipProvider, fap.RoleProvider, username, cookieValue);
        SPFederationAuthenticationModule fam = SPFederationAuthenticationModule.Current;
        fam.SetPrincipalAndWriteSessionToken(token);
    }
}

Logic here is pretty much simple and, other than replacing SetAuthCookie() by SetFBACookie(), nothing has changed in the http module from 2007 to 2010. Basically, it looks from a cookie and, if a user can be inferred from it, then sets cookie based on it.

Let’s go to the membership provider part. When SecurityTokenForFormsAuthentication(uri, membershipProv, roleProv, user, password) is called, our custom membership provider’s ValidateUser(user, password) will be called. In there, we take the password as the cookie value. If we can infer a user from it, then we validate that user. If not, validation is delegated to whatever base class we choose to inherit from (LdapMembershipProvider, SqlMembershipProvider, etc), or could have chosen a better design by inject it at runtime.

Here’s the main part of the membership provider:

public override bool ValidateUser(string username, string password)
{
    // check parameters
    if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
    {
        return (false);
    }

    // validate we can get user from cookie and equals the user passed in
    string cookieValue = password;
    string userFromCookie = null;
    userFromCookie = GetUsernameFromCookie(cookieValue);
    if (!string.IsNullOrEmpty(userFromCookie) && userFromCookie.Equals(username, StringComparison.OrdinalIgnoreCase))
    {
        return (true);
    }

    return base.ValidateUser(username, password);
}

Now that we have the components, we need to deploy them.

  • in the SharePoint 2010 web application’s web.config we are trying to access we need to configure
    • the http module
    • the membership provider
  • in the SecurityTokenServiceApplication IIS site’s web.config we need to configure
    • the membership provider

The http module entry looks like this:

<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
...
<add name="MyHttpModule" type="Contoso.Providers.CustomHttpModule, Contoso, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456701234567" … />
</modules>

and the membership provider entry looks like this:

<system.web>
<membership ...>
<providers ...>
<add name="MyMembershipProvider” type="Contoso.Providers.CustomMembershipProvider, Contoso, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456701234567" … />