Single sign on (SSO) for ASP.NET apps in different subdomains

I struggled for many hours to get this working. The linchpin is maintaining sessions across subdomains, which (for Microsoft developers) can only be done if sessions are kept in a database. For this example, I’ll use SQL Server.

Step 0. Prepare your Sql Server database

Make your ASPState database

Step 1. Add new class library to both solutions that you want to link, call it SharedSession.

Inside the library, add a single module called SharedSessionModule.cs:

using System;
using System.Web;
using System.Reflection;
using System.Configuration;

namespace SharedSession
{

	public class SharedSessionModule : IHttpModule
	{
		// Cache settings on memory. 
		protected static string applicationName = ConfigurationManager.AppSettings["ApplicationName"];
		protected static string rootDomain = ConfigurationManager.AppSettings["RootDomain"];

		#region IHttpModule Members
		///  
		/// Initializes a module and prepares it to handle requests. 
		///  
		///  
		/// An System.Web.HttpApplication 
		/// that provides access to the methods, 
		/// properties, and events common to all application objects within  
		/// an ASP.NET application. 
		///  
		public void Init(HttpApplication context)
		{
			// This module requires both Application Name and Root Domain to work. 
			if (string.IsNullOrEmpty(applicationName) ||
				string.IsNullOrEmpty(rootDomain))
			{
				return;
			}

			// Change the Application Name in runtime. 
			FieldInfo runtimeInfo = typeof(HttpRuntime).GetField("_theRuntime",
				BindingFlags.Static | BindingFlags.NonPublic);
			HttpRuntime theRuntime = (HttpRuntime)runtimeInfo.GetValue(null);
			FieldInfo appNameInfo = typeof(HttpRuntime).GetField("_appDomainAppId",
				BindingFlags.Instance | BindingFlags.NonPublic);

			appNameInfo.SetValue(theRuntime, applicationName);

			// Subscribe Events. 
			context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
		}

		///  
		/// Disposes of the resources (other than memory) used by the module 
		/// that implements. 
		///  
		public void Dispose()
		{
		}
		#endregion

		///  
		/// Before sending response content to client, change the Cookie to Root Domain 
		/// and store current Session Id. 
		///  
		///  
		/// An instance of System.Web.HttpApplication that provides access to 
		/// the methods, properties, and events common to all application 
		/// objects within an ASP.NET application. 
		///  
		void context_PostRequestHandlerExecute(object sender, EventArgs e)
		{
			HttpApplication context = (HttpApplication)sender;

			try
			{
				if (context.Session != null &&
					//if (context.Session != null &&
					!string.IsNullOrEmpty(context.Session.SessionID))
				{
					// ASP.NET store a Session Id in cookie to specify current Session. 
					// This is a response, so if cookie doesn't exist, it is created!

					HttpCookie cookie = context.Response.Cookies["ASP.NET_SessionId"];
					// Need to store current Session Id during every request. 
					if (cookie != null)
					{
						cookie.Value = context.Session.SessionID;
						// All Applications use one root domain to store this Cookie 
						// So that it can be shared. 
						if (rootDomain != "localhost")
						{
							cookie.Domain = rootDomain;
						}

						// All Virtual Applications and Folders share this Cookie too. 
						cookie.Path = "/";
					}
				}
			}
			catch (Exception ex)
			{
			}
		}
	}


}

Step 2. Make the following modifications to BOTH web.configs

For older versions of IIS:

For newer versions of IIS:

Step 3. There is no Step 3. That’s it!

Extending $log to log to database in AngularJS

$log is a very useful built-in service that is part of the AngularJS (angular 1) distribution. It is even more useful if you could write logs to a database table rather than just to console.

Here’s how you can do it:

Assumes you have a RESTful back-end that responds to the url in the log() function of LoggingService below,


var app = angular.module('app');

app.factory("LoggingService", function ($http) {
    var _isEnabled = true;

    var isEnabled = function () {
        return _isEnabled;
    }

    var enable = function (b) {
        _isEnabled = b;
    }

    var log = function () {
        var url = "/Api/Audit/Log";
        if (this.isEnabled()) {
            var type = "UNK"; // placeholder for when no type is passed
            args = [];
            if (typeof arguments === 'object') {
                type = arguments[0];
                for (var i = 1; i < arguments.length; i++) {
                    arg = arguments[i];
                    if (typeof (arg) != 'string') arg = JSON.stringify(arg);
                    args.push(arg);
                }
                return $http.post(url, { "ACTIVITY": type + ': ' + args.join('\n') });
            }
            else {
                return $http.post(url, { "ACTIVITY": type + ': ' + arguments[0] });
            }
        }
    };

    return {
        log: log,
        enable: enable,
        isEnabled: isEnabled
    };
})

.config(function ($routeProvider, $logProvider, $httpProvider, $provide) {

    $logProvider.debugEnabled(1);

    // -----------------------------------------------------------------------------------
    // logging algorithm suggested by:
    // http://stackoverflow.com/questions/32365811/decorate-angulars-log-to-use-a-service-that-uses-http
    // -----------------------------------------------------------------------------------

    $provide.decorator("$log", function ($delegate, $injector) {

        var logFn = $delegate.log;

        // N.B. Don't delegate $log.debug()...that is ALWAYS written just to the console!
        // $log.warn(), $log.error() and $log.info() are written to both console (assuming $logProvider.debugEnabled == 1) and database
        // $log.db() is written ONLY to database!!

        $delegate.warn = function (message) {
            var LoggingService = $injector.get('LoggingService');
            LoggingService.log('WARN', message);
            if ($logProvider.debugEnabled()) logFn.apply(null, arguments);
        };
        $delegate.error = function (message) {
            var LoggingService = $injector.get('LoggingService');
            LoggingService.log('ERROR', message);
            if ($logProvider.debugEnabled()) logFn.apply(null, arguments);
        };
        $delegate.info = function (message) {
            var LoggingService = $injector.get('LoggingService');
            LoggingService.log('INFO', message);
            if ($logProvider.debugEnabled()) logFn.apply(null, arguments);
        };
        $delegate.db = function (message) {
            var LoggingService = $injector.get('LoggingService');
            LoggingService.log('DB', message);
            // N.B. $log.db is not written to console ever!!
        };
        //Return the delegate
        return $delegate;
    });
//   :
//   :
// more config functionality here
//   :
//   :
}) // end of config function

.run();

Tunneling PUT, DELETE and HEAD methods as POSTs

Applies to: Asp.net MVC WebAPI projects

This post is adapted from Hanselman.

Step 1. Add the following MethodOverrideHandler.cs class in the project root:

    public class MethodOverrideHandler : DelegatingHandler
    {
        readonly string[] _methods = { "DELETE", "HEAD", "PUT" };
        const string _header = "X-HTTP-Method-Override";

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // Check for HTTP POST with the X-HTTP-Method-Override header.
            if (request.Method == HttpMethod.Post && request.Headers.Contains(_header))
            {
                // Check if the header value is in our methods list.
                var method = request.Headers.GetValues(_header).FirstOrDefault();
                if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase))
                {
                    // Change the request method.
                    request.Method = new HttpMethod(method);
                }
            }
            return base.SendAsync(request, cancellationToken);
        }
    }

 

Step 2. In the Register method of App_Start/WebApiConfig.cs, insert the following code:

 

    config.MessageHandlers.Add(new MethodOverrideHandler());

 

Step 3. You’ll need to modify any client calls of PUT, DELETE or HEAD that access your WebApi project. For each affected call, add the following header:

'X-HTTP-Method-Override': 'PUT'

or
'X-HTTP-Method-Override': 'DELETE'

or
'X-HTTP-Method-Override': 'HEAD'

…and then make your AJAX call as a POST, adding whatever parameters you need either as part of the URL or as data.

Now your PUT, DELETE or HEAD calls will pass through the firewall as POSTs, be recognized by a message handler for what they truly are and converted to their native method, then passed to your Controllers to be executed as expected.