My Resume

  • My Resume (MS Word) My Resume (PDF)


Affiliations

  • Microsoft Most Valuable Professional
  • INETA Community Champion
  • Leader, NJDOTNET: Central New Jersey .NET User Group

Wednesday, October 24, 2007

Custom Authentication with Community Server

You may have scrolled down to the bottom of one of our blog posts over at Infragistics at some point and noticed the little Community Server logo in the page footer.  We've been using Community Server to host our blogs for some time now, and it's such a great platform that we're looking to leverage it a bit more.  To do what we're thinking of, you guys will need to be able to log in...  and what kind of user experience is it for you to have two IG.com accounts?  Enter Single Sign On (SSO), and lucky for us, Telligent sells a custom module that does just that.  Setting it up was a cinch: just drop the assembly in, tell Community Server which cookie to use, and make sure that your main site (the authenticating site) is setting the cookie correctly.  And, it really was that simple!  Well, almost...

The out-of-the-box solution did work great for almost all scenarios.  There were, however, a few problems we had:

Cookie Timeouts

Unfortunately, it doesn't seem like the custom cookie authentication module can be configured to provide any type of sliding expiration behavior to the cookie.  That means that after a user logs in on the main site and the cookie is created, they only have as long as the cookie lives to be "singlely signed on" and once it expires, they'd be redirected back to the main site to re-authenticate themselves.  The most obvious and easiest fix would be to just set no expiration date on the cookie.  Sure - that'd work, but I prefer the security of having users automatically signed out after a period of inactivity, at least by default. 

So, I decided to write a module to check for the cookie on each request, and update the expiration date accordingly.  BAM - sliding expiration.  Below is the code I used to do it:

Cookie Manager

public class CookieManager : IHttpModule
{
    // Lifetime of sliding expiration (in minutes)
    private const int COOKIE_LIFETIME = 20;

    #region CookieDomain
    private static string _CookieDomain =
        ConfigurationManager.AppSettings["CookieDomain"];

    /// <summary>
    /// Gets or sets the cookie domain.
    /// </summary>
    /// <value>The cookie domain.</value>
    public static string CookieDomain
    {
        get
        {
            if (String.IsNullOrEmpty(_CookieDomain))
            {
                try
                {
                    string cookieDomain = null;
                    // If no domain name was specified, try to guess one
                    string hostname = HttpContext.Current.Request.Url.Host;
                    // Try to get the domain name
                    string[] hostnameParts = hostname.Split('.');
                    if (hostnameParts.Length >= 2)
                    {
                        cookieDomain = String.Format(
                            "{0}.{1}",
                            hostnameParts[hostnameParts.Length - 2],
                            hostnameParts[hostnameParts.Length - 1]);
                    }
                    _CookieDomain = cookieDomain ?? hostname;
                }
                catch { /* We don't really care if this doesn't work... */ }
            }

            // Don't allow an empty domain
            if (String.IsNullOrEmpty(_CookieDomain)) _CookieDomain = "localhost";

            return _CookieDomain;
        }

        set { _CookieDomain = value; }
    }
    #endregion

    #region SSOCookieName
    private static string _SSOCookieName;
    /// <summary>
    /// Gets the name of the SSO cookie that the 
    /// Custom Authentication module is using.
    /// </summary>
    /// <value>The name of the SSO cookie.</value>
    public static string SSOCookieName
    {
        get
        {
            if (_SSOCookieName == null)
            {
                Telligent.Components.Provider authProvider =
                    CSConfiguration.GetConfig()
                    .Extensions["CustomAuthentication"] as Telligent.Components.Provider;

                if (authProvider != null)
                    _SSOCookieName = authProvider.Attributes["authenticatedUserCookieName"];
                else
                    throw new ApplicationException(
                        "Could not find Community Server provider 'CustomAuthentication'.");
            }
            return _SSOCookieName;
        }
    }
    #endregion
    private void KeepSSOCookieAlive(HttpContext context)
    {
        // See if we've got an SSO cookie
        HttpCookie cookie = context.Request.Cookies[SSOCookieName];
        if (cookie != null)
        {
            // Make sure the domain name is set
            cookie.Domain = CookieDomain;
            // Update the cookie's expiration (use sliding expiration)
            cookie.Expires = DateTime.Now.AddMinutes(COOKIE_LIFETIME);
            // Send the cookie back
            context.Response.Cookies.Set(cookie);
        }
    }

    #region IHttpModule Members
    /// <summary>
    /// Disposes of the resources (other than memory) used 
    /// by the module that implements <see cref="T:System.Web.IHttpModule"></see>.
    /// </summary> public void Dispose() { }
    /// <summary> /// Inits the specified application.
    /// </summary>
    /// <param name="application">The application.</param>
    public void Init(HttpApplication application)
    {
        application.BeginRequest += new EventHandler(application_BeginRequest);
    }
    void application_BeginRequest(object sender, EventArgs e)
    {
        KeepSSOCookieAlive((sender as HttpApplication).Context);
    }
    #endregion
}

There are a few things in this class I'd like to point out:

The most important method, obviously, is the KeepSSOCookieAlive() method.  It's the heart of the module, but pretty straightforward.
I decided to try to get the CookieDomain programmatically, but didn't really get into too complex of an operation to get the correct domain.  If for whatever reason this didn't work in production, you could easily override the guessing by setting it in the AppSettings.
I'm getting the actual cookie name used by the provider - by actually retrieving the provider itself and asking it which cookie it's looking for - so that there won't be any confusion.

News Gateway Authentication

Ok, great - we've got the SSO for the main Community Server instance working like a charm, so I move on to getting Telligent's News Gateway service into place.  After reading through the docs for a bit, I see a very ominous line saying something to the effect of "Custom (cookie-based) Authentication won't work with the News Gateway."  Ugh!  I mean, it makes sense, but... Ugh! 

Of course, the first thing I do is try it and hope it works.  Unsurprisingly, it doesn't.  The next thing I do is take a step back, look through what's available, and spot Telligent's Form-based membership provider, which (I think) is the default provider in the Gateway.  The Cookie provider works by creating a Community Server account for a user that has logged in to the main site.  The problem with Forms (username/password combination) Authentication is that the Community Server account that is set up knows nothing about your password from the originating site, nor can it ask the originating site - that it knows nothing about - to authenticate you...  unless you help it.

The solution was obvious: override the ValidateUser(string username, string password) method of the Community Server Forms Membership Provider.  I wasn't going to paste the code because it's just a simple override, but here's an example snippet:

Custom Membership Provider

public class CustomMembershipProvider
    : CommunityServer.ASPNet20MemberRole.CSMembershipProvider
{
    public override bool ValidateUser(string username, string password)
    {
        // TODO: Insert your custom validation logic here
        return (username == "superdude" && password = "wickedcool");
    }
}

That's right - just a regular ol' override of the membership provider.  Then you can follow it up by copying the Membership provider section in the configuration file (CommunityServer.NntpServer.Service.exe.config or CommunityServer.NntpServer.Console.exe.config, depending on which one you're using) and replace the CSMembershipProvider with your own.  Then, BAM - you've got username/password authentication against whatever data source you like.

 

I hope this post can help you out if you were coming across some of the same problems we were!

0 comments: