Background

On the previous article/note regarding SAML, I noted that we can use HTTP module and handler to implement SAML SSO.

I was tasked on leading the migration of our current SSO setup of all internal applications that we handled on our company and we decided to use HTTP modules and HTTP handlers. This way we will just consume the managed module in our web.config and we will don’t have any changes needed in our app.

HTTP modules and HTTP handlers

As noted in microsoft document, modules and handlers are integral part of the ASP.NET architecture. While a request is being processed each request is processed by multiple HTTP modules and is then processed by a single HTTP handler. After the handler has processed the request flows back through the HTTP modules.

Implementation

Create a module that implements IHttpModule

public class MySsoModule : IHttpModule
{
    ...

    public void Init(HttpApplication application)
    {
        application.AuthenticateRequest += (new EventHandler(this.Application_AuthenticateRequest));
    }

    private void Application_AuthenticateRequest(Object source, EventArgs e)
    {
        HttpApplication application = (HttpApplication)source;
        HttpContext context = application.Context;
        
        //httpmodule is executed first before httphandler, without this we will experience infinite loop
        //if request is a .saml, let it pass so that it will be handled by MyIdHttpHandler
        if (context.Request.Url.AbsoluteUri.Contains("_SAMLAssertionConsumer.axd")) { return; }

        if (context.Request.Cookies != null && context.Request.Cookies["MySsoCookie"] != null)
        {
            try
            {
                var ticket = FormsAuthentication.Decrypt(context.Request.Cookies["MySsoCookie"].Value.ToString());
                var deserializer = new JavaScriptSerializer();
                var user = deserializer.Deserialize<UserIdentity>(ticket.UserData);

                //DO WHATEVER YOU WANT WITH YOUR UserIdentity
                return;
            }
            catch(FormatException fEx)
            {
                //invalid/different format cookieValue
                //do whatever you want with fEx error
            }
            catch (ArgumentException aEx)
            {
                //cookieValue is empty string/null or > 4096 chars
                //do whatever you want with aEx
            }
            catch(Exception ex)
            {
                //throw or whatever your want with general ex;
            }

            
            //destroy MySsoCookie before re-auth to avoid infinite loop (not sure if this will work on Chrome, chrome has a different/weird cookie caching)
            context.Response.Cookies["MySsoCookie"].Expires = DateTime.Now.AddDays(-1);
            AuthenticateMySso(context);
            return;
        }
        else
        {
            AuthenticateMySso(context);
        }
    }

    private void AuthenticateMySso(HttpContext context)
    {
        try
        {
            string samlEndpoint = ConfigurationManager.AppSettings["mySsoEndpoint"].ToString();

            var request = new AuthRequest(
                "http://localhost", //value doesn't matter for IdP Initiated
                context.Request.Url.AbsoluteUri
            );

            //generate the provider URL
            string url = request.GetRedirectUrl(samlEndpoint);

            //then redirect your user to the above "url" var
            context.Response.Redirect(url);
        }
        catch (Exception ex)
        {
            throw new HttpException(500, ex.Message);
        }
    }
}

Basically what is happening here is that all requests that will go to our applcation will through this method => Application_AuthenticateRequest, we check first if the current request is a SAML response using

if (context.Request.Url.AbsoluteUri.Contains("_SAMLAssertionConsumer.axd")) { return; }

If it is a saml response we will just return the module so that our HTTP handler will take care of this. If the MySsoCookie existed, we will validate the cookie if it came from us, if valid, we will get user’s data using the cookie and we will redirect the user as authenticated.

If request is not a samlresponse and MySsoCookie is not yet created, means that the request is not authenticated, so we will just redirect the user to our SSO provider using AuthenticateMySso method in our HttpModule.

Once user enters credential, that request will go to our HttpHandler. Here’s the sample HttpHandler

public class MySsoHttpHandler : IHttpHandler
{
    ...

    public void ProcessRequest(HttpContext context)
    {
        try
        {
            if (context.Request.Form == null || context.Request.Form["SAMLResponse"] == null)
            {
                throw new HttpException(403, "Access denied");
            }

            string samlCertificate = System.IO.File.ReadAllText(context.Server.MapPath("~/App_Data/MySso.crt"));

            Response samlResponse = new Response(samlCertificate);
            samlResponse.LoadXmlFromBase64(context.Request.Form["SAMLResponse"]);

            if (samlResponse.IsValid())
            {
                var user = new UserIdentity
                {
                    Id = samlResponse.GetId(),
                    FirstName = samlResponse.GetFirstName(),
                    LastName = samlResponse.GetLastName(),
                    Email = samlResponse.GetEmail()
                };
                
                var serializer = new JavaScriptSerializer();
                string userData = serializer.Serialize(user);

                var ticket = new FormsAuthenticationTicket(1, //version
                                        user.Username, //username
                                        DateTime.Now, //createDate
                                        DateTime.Now.AddMinutes(60), //expiration 
                                        true, userData); //store json data
                var encryptedTicket = FormsAuthentication.Encrypt(ticket);

                var mySsoCookie = new HttpCookie("MySsoCookie", encryptedTicket) { Expires = ticket.Expiration };
                context.Response.Cookies.Add(myIdCookie);
            }
            else
            {
                throw new HttpException(403, "Access denied");
            }
        }
        catch(ThreadAbortException)
        {
            //ThreadAbortException is thrown when Response.Redirect
            Trace.TraceInformation("Redirected to: {0}", context.Request.Form["RelayState"].ToString());
        }
        catch (Exception ex)
        {
            throw new HttpException(403, "Access denied");
        }
    }
}

On our next article, I will detail how we are going to deploy this to IIS.