Saturday, July 2, 2011

Http to Https Flipper for Asp.Net Web Forms

This article shows how to automatically make Asp.Net Web Form sites open certain pages on the site as https by using a 301 moved permanently redirect.  It works with standard web forms .aspx pages and web forms using .Net Routing.

This example will cover the following scenarios:

1. If a user navigates to http://www.mydomain.com/default.aspx then we can automatically have the URL change to be https://www.mydomain.com/default.aspx for the default.aspx page only, other pages will remain non-secure.

2. You want all your sites web pages to always go secure (https).

3. Allow you to control which pages go secure by using a ‘RequireSSL’ attribute (much like mvc now does).

4. Allow you to control which pages go secure by listing them in the web.config.

5. If you have non standard ports for http and https e.g. http://www.mydomain.com:8054/default.aspx can change the URL to https://www.mydomain.com:4054/default.aspx.

Note the 8054 = http and 4054 = https.

6. You have non standard ports as point 5 above mentions, but in your live environment the ports are masked from the user by the firewall (or some other clever piece of hardware).  But internally the ports are still being used by the firewall to route traffic to your site.

e.g. Your web site on IIS is setup to use ports 8054(http) & 4054(https) but the user in their browser will see http://www.mydomain.com/default.aspx and https://www.mydomain.com/default.aspx.  In this scenario you would want to remove the port when you flip from http to https.

 

Lets see some code

First create a new project, lets call it ‘JL.Web.Security’. 

SolutionStructure

As you add the classes below the structure of the project should end up looking like the above.

Add the following 2 classes (RequireSSLAttribute.cs and RequireSSLModule.cs).

RequireSSLAttribute.cs

namespace JL.Web.Security
{
    /// <summary>
    /// Attribute to place on classes that you want to go secure (SSL / HTTPS)
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    sealed public class RequireSSL : Attribute
    {
    }
}

With the ‘RequireSSLAttribute’' you can now place this attribute on any web form code behind page and that page will always go secure e.g.

namespace WebApplication
{
    [RequireSSL]
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }
}

The ‘RequireSSLModule.cs’ class below is the one that does the real work.

But in summary all this class really does is create a new IHttpModule that listens for page requests. Then checks if every page should be secure and if we are not already secure then go secure (https).  Otherwise check if the RequireSSL attribute is on the page if it is go secure (if we are not secure already), if the attribute is not in use then check the web.config file to see if the page is listed there, if it is go secure (if we are not secure already). Otherwise go non secure (if we are currently secure).

RequireSSLModule.cs

namespace JL.Web.Security
{
    /// <summary>
    /// HttpModule for switching between HTTP and HTTPS (HTTP <=> HTTPS)
    /// </summary>
    public class RequireSSLModule : IHttpModule
    {
        SecureWebPageSettings settings = null;

        public void Init(HttpApplication context)
        {
            settings = WebConfigurationManager.GetSection("secureWebPageSettings") as SecureWebPageSettings;
            if (settings != null && settings.Mode != SecureWebPageMode.Off)
            {
                // The PreRequestHandlerExecute event occurs just before ASP.NET begins executing
                // a handler such as a Page.
                // In here we can acquire a reference to the currently executing ASPX Page
                context.PreRequestHandlerExecute += new EventHandler(OnPreRequestHandlerExecute);
            }
        }

        /// <summary>
        /// Empty, but necessary when implementing IHttpModule
        /// </summary>
        public void Dispose() { }

        /// <summary>
        /// Switch between HTTP and HTTPS as and when necessary.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnPreRequestHandlerExecute(object sender, EventArgs e)
        {
            bool requireSSL = false;

            // obtain a reference to the ASPX Page
            System.Web.UI.Page Page = HttpContext.Current.Handler as System.Web.UI.Page;

            // if a valid Page was not found, exit
            if (Page == null)
            {
                return;
            }

            if (settings.MakeAllPagesSecure)    // All pages must be secure
                requireSSL = true;
            else
            {
                // Check if the Page is decorated with the RequireSSL attribute
                requireSSL = (Page.GetType().GetCustomAttributes(typeof(RequireSSL), true).Length > 0);
                if (requireSSL == false)
                {
                    // Check the pages in the web.config to see if it is mentioned here as must be secure
                    var pageName = Page.GetType().BaseType.Name;
                    if (settings.Files.IndexOf(pageName) >= 0)
                        requireSSL = true;
                }
            }


            // check if the Page is currently using the HTTPS scheme
            bool isSecureConnection = HttpContext.Current.ApplicationInstance.Request.IsSecureConnection;

            // acquire the URI (e.g. http://localhost/default.aspx)
            Uri baseUri = HttpContext.Current.ApplicationInstance.Request.Url;

            String portToUse = String.Empty;


            if (requireSSL && !isSecureConnection)
            {
                // The page requires SSL and it is currently using the HTTP scheme.
                // Switch the HTTP scheme to the HTTPS scheme
                string url = baseUri.ToString().Replace(baseUri.Scheme, Uri.UriSchemeHttps);

                ChangePort(baseUri, true, ref url);

                // perform a 301 redirect to the secure url
                PermanentRedirect(url);
            }
            else if (!requireSSL && isSecureConnection)
            {
                // The page does not require SSL and it is currently using the HTTPS scheme.
                // Switch the HTTPS scheme to the HTTP scheme
                string url = baseUri.ToString().Replace(baseUri.Scheme, Uri.UriSchemeHttp);

                ChangePort(baseUri, false, ref url);

                // perform a 301 redirect to the non-secure url
                PermanentRedirect(url);
            }
        }

        /// <summary>
        /// Peforms a 301 redirect to the given url. This is the most search engine friendly
        /// way of redirecting to another Page. The 301 status code means that a Page has
        /// permanently moved to a new location.
        /// </summary>
        /// <param name="url"></param>
        private void PermanentRedirect(string url)
        {
            //throw new Exception("url:" + url);
            HttpContext.Current.Response.Status = "301 Moved Permanently";
            HttpContext.Current.Response.AddHeader("Location", url);
        }


        /// <summary>
        ///  Change or remove the port from the url
        /// </summary>
        private void ChangePort(Uri baseUri, Boolean useSecurePort, ref String url)
        {
            String portToUse = String.Empty;

            // Get the current port
            String portToFind = String.Format(":{0}", baseUri.Port.ToString());

            if (settings != null && settings.SecurityRemovePort)
            {
                // Config setting says to remove the port from the url so lets do that  
                url = url.Replace(portToFind, "");
            }
            else
            {
                // Security setting says to leave the port in the url so we need to change it
                if (useSecurePort)
                {
                    // If we are not on the standard port 80 we need to lookup the secure port from iis
                    if (baseUri.Port != 80)
                    {
                        portToUse = LookupSecurePortFromIIS();
                        String portToRepaceWith = String.Format(":{0}", portToUse);
                        url = url.Replace(portToFind, portToRepaceWith);
                    }
                }
                else
                {
                    // If we are not on the standard secure port 443 we need to lookup the non secure port from iis
                    if (baseUri.Port != 443)
                    {
                        portToUse = LookupNonSecurePortFromIIS();
                        String portToRepaceWith = String.Format(":{0}", portToUse);
                        url = url.Replace(portToFind, portToRepaceWith);
                    }
                }
            }
        }

        private String LookupSecurePortFromIIS()
        {
            return LookupPortFromIIS(true);
        }

        private String LookupNonSecurePortFromIIS()
        {
            return LookupPortFromIIS(false);
        }

        /// <summary>
        /// Using DirectoryServices look up in IIS the port that should be used
        /// when swapping from http to https or https to http
        /// </summary>
        private String LookupPortFromIIS(bool findHttpsPort)
        {
            String port = String.Empty;
            System.DirectoryServices.PropertyValueCollection serverBindings;

            // Get the current website server instance
            String websiteInstanceID = HttpContext.Current.ApplicationInstance.Request["INSTANCE_ID"];  // e.g. 1646904159

            // Connect to the IIS directory service for the website instance id
            System.DirectoryServices.DirectoryEntry path = null;
            if (!String.IsNullOrEmpty(settings.IISServerUsername) && !String.IsNullOrEmpty(settings.IISServerPassword))
                path = new System.DirectoryServices.DirectoryEntry("IIS://localhost/W3SVC/" + websiteInstanceID, settings.IISServerUsername, settings.IISServerPassword);
            else
                path = new System.DirectoryServices.DirectoryEntry("IIS://localhost/W3SVC/" + websiteInstanceID);

            // Find the port number
            if (findHttpsPort)  // Get https port
                serverBindings = path.Properties["SecureBindings"];
            else                // Get http port
                serverBindings = path.Properties["serverbindings"];

            if (serverBindings.Value != null)
            {
                String bindingValue = serverBindings.Value.ToString();

                // Just get the port number (remove everything else)
                bindingValue = bindingValue.Substring(bindingValue.IndexOf(":") + 1);

                port = bindingValue.Replace(":", "");
            }

            return port;
        }
    }
}

The LookupPortFromIIS method is the one that needs the most explaining.  In the case when you are using non standard ports e.g. not 80 for http and not 443 for https, then in order to flip from http://mydomain.com:8054 to https://mydomain.com:4054 this method looks up from the IIS server what port number should be used.  So if you were going from http on port 8054 we would look up in IIS that we need port 4054 in order to go secure.  And if we were already secure we would look up that we need port 8054 to go non-secure.  To do this lookup into IIS we use DirectoryServices which has been tested on IIS6 and IIS7.

 

And finally to support controlling which pages go secure via the web.config we need to add some configuration code. Add to this project a ‘Configuration’ directory.  Under the configuration directory add the following 3 classes (enums.cs, SecureWebPageFileSetting.cs and SecureWebPageSettings.cs).

enums.cs

namespace JL.Web.Security.Configuration
{
    public enum SecureWebPageMode
    {
        /// <summary>
        /// Web page security is on
        /// </summary>
        On,

        /// <summary>
        /// Web page security is off
        /// </summary>
        Off
    }
}


SecureWebPageFileSetting.cs

namespace JL.Web.Security.Configuration
{
    /// <summary>
    /// Represents an file entry in the <secureWebPageSettings>
    /// configuration section.
    /// </summary>
    public class SecureWebPageFileSetting : ConfigurationElement
    {
        #region Constructors
        /// <summary>
        /// Creates an instance of SecureWebPageFileSetting.
        /// </summary>
        public SecureWebPageFileSetting()
            : base()
        {
        }

        /// <summary>
        /// Creates an instance with an initial page name.
        /// </summary>
        /// <param name="pagename">The page class name e.g. login (not login.aspx).</param>
        public SecureWebPageFileSetting(string pagename)
            : this()
        {
            Pagename = pagename;
        }
        #endregion

        /// <summary>
        /// Gets or sets the path of this file setting.
        /// </summary>
        [ConfigurationProperty("pagename", IsRequired = true, IsKey = true)] 
        public string Pagename
        {
            get { return (string)this["pagename"]; }
            set { this["pagename"] = value; }
        }
    }

    /// <summary>
    /// Represents a collection of SecureWebPageFileSetting objects.
    /// </summary>
    public class SecureWebPageFileSettingCollection : ConfigurationElementCollection
    {
        /// <summary>
        /// Gets the element name for this collection.
        /// </summary>
        protected override string ElementName
        {
            get { return "files"; }
        }

        /// <summary>
        /// Gets a flag indicating an exception should be thrown if a duplicate element
        /// is added to the collection.
        /// </summary>
        protected override bool ThrowOnDuplicate
        {
            get { return true; }
        }

        /// <summary>
        /// Gets the element at the specified index.
        /// </summary>
        /// <param name="index">The index to retrieve the element from.</param>
        /// <returns>The SecureWebPageFileSetting located at the specified index.</returns>
        public SecureWebPageFileSetting this[int index]
        {
            get { return (SecureWebPageFileSetting)BaseGet(index); }
        }

        /// <summary>
        /// Gets the element with the specified pagename.
        /// </summary>
        /// <param name="pagename">The pagename of the element to retrieve.</param>
        /// <returns>The SecureWebPageFileSetting with the specified pagename.</returns>
        public new SecureWebPageFileSetting this[string pagename]
        {
            get
            {
                if (pagename == null)
                    throw new ArgumentNullException("pagename");
                else
                    return (SecureWebPageFileSetting)BaseGet(pagename.ToLower(CultureInfo.InvariantCulture));
            }
        }

        #region Collection Methods
        /// <summary>
        /// Adds a SecureWebPageFileSetting to the collection.
        /// </summary>
        /// <param name="fileSetting">An initialized SecureWebPageFileSetting instance.</param>
        public void Add(SecureWebPageFileSetting fileSetting)
        {
            BaseAdd(fileSetting);
        }

        /// <summary>
        /// Clears all file entries from the collection.
        /// </summary>
        public void Clear()
        {
            BaseClear();
        }

        /// <summary>
        /// Removes a SecureWebPageFileSetting from the collection with a matching pagename as specified.
        /// </summary>
        /// <param name="pagename">The pagename of a SecureWebPageFileSetting to remove.</param>
        public void Remove(string pagename)
        {
            int Index = IndexOf(pagename);
            if (Index >= 0)
                BaseRemoveAt(Index);
        }

        /// <summary>
        /// Removes the SecureWebPageFileSetting from the collection at the specified index.
        /// </summary>
        /// <param name="index">The index of the SecureWebPageFileSetting to remove.</param>
        public void RemoveAt(int index)
        {
            BaseRemoveAt(index);
        }
        #endregion

        /// <summary>
        /// Creates a new element for this collection.
        /// </summary>
        /// <returns>A new instance of SecureWebPageFileSetting.</returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new SecureWebPageFileSetting();
        }

        /// <summary>
        /// Gets the key for the specified element.
        /// </summary>
        /// <param name="element">An element to get a key for.</param>
        /// <returns>A string containing the pagename of the SecureWebPageFileSetting.</returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            if (element != null)
                return ((SecureWebPageFileSetting)element).Pagename.ToLower(CultureInfo.InvariantCulture);
            else
                return null;
        }

        /// <summary>
        /// Returns the index of an item with the specified pagename in the collection.
        /// </summary>
        /// <param name="pagename">The pagename of the item to find.</param>
        /// <returns>Returns the index of the item with the pagename.</returns>
        public int IndexOf(string pagename)
        {
            if (pagename == null)
                throw new ArgumentNullException("pagename");
            else
                return this.IndexOf((SecureWebPageFileSetting)BaseGet(pagename.ToLower(CultureInfo.InvariantCulture)));
        }

        public int IndexOf(SecureWebPageFileSetting item)
        {
            if (item != null)
                return BaseIndexOf(item);
            else
                return -1;
        }
    }
}


SecureWebPageSettings.cs

namespace JL.Web.Security.Configuration
{
    /// <summary>
    /// Contains the settings of a secureWebPageSettings configuration section.
    /// </summary>
    public class SecureWebPageSettings : ConfigurationSection
    {
        /// <summary>
        /// Creates an instance of SecureWebPageSettings.
        /// </summary>
        public SecureWebPageSettings()
            : base()
        {
        }

        #region Properties
        /// <summary>
        /// Gets or sets the mode indicating how the secure web page settings handled.
        /// </summary>
        [ConfigurationProperty("mode", DefaultValue = SecureWebPageMode.On)]
        public SecureWebPageMode Mode
        {
            get { return (SecureWebPageMode)this["mode"]; }
            set { this["mode"] = value; }
        }

        /// <summary>
        /// Gets the collection of file settings read from the configuration section.
        /// </summary>
        [ConfigurationProperty("files")]
        public SecureWebPageFileSettingCollection Files
        {
            get { return (SecureWebPageFileSettingCollection)this["files"]; }
        }

        /// <summary>
        /// Gets or sets the remove port mode.  This should be set to true when SSL is on a non standard port
        /// </summary>
        [ConfigurationProperty("securityRemovePort", DefaultValue = false)]
        public bool SecurityRemovePort
        {
            get { return (bool)this["securityRemovePort"]; }
            set { this["securityRemovePort"] = value; }
        }

        /// <summary>
        /// Makes all pages on the site secure, regardless of what is specified in the files section.
        /// </summary>
        [ConfigurationProperty("makeAllPagesSecure", DefaultValue = false)]
        public bool MakeAllPagesSecure
        {
            get { return (bool)this["makeAllPagesSecure"]; }
            set { this["makeAllPagesSecure"] = value; }
        }

        /// <summary>
        /// IIS Server user name use to login to directory services
        /// </summary>
        [ConfigurationProperty("iisServerUsername", DefaultValue = "")]
        public String IISServerUsername
        {
            get { return (String)this["iisServerUsername"]; }
            set { this["iisServerUsername"] = value; }
        }

        /// <summary>
        /// IIS Server password use to login to directory services
        /// </summary>
        [ConfigurationProperty("iisServerPassword", DefaultValue = "")]
        public String IISServerPassword
        {
            get { return (String)this["iisServerPassword"]; }
            set { this["iisServerPassword"] = value; }
        }
        #endregion
    }
}

 

In our web site we configure the Http to Https flipper:

Add the following line to the web.config configSections.

<section name="secureWebPageSettings" type="JL.Web.Security.Configuration.SecureWebPageSettings, JL.Web.Security" />

Add the following line to the web.config modules section under system.webServer.

<add name="JL.Web.Security.RequireSSLModule" type="JL.Web.Security.RequireSSLModule" preCondition="managedHandler" />

 

You can now use the ‘RequireSSLAttribute’' by placing this attribute on any web form code behind page and that page will always go secure e.g.

namespace WebApplication
{
    [RequireSSL]
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }
}

And in the web.config add the following to control the http to https flipper settings:

<!-- SSL Configuration -->
    <secureWebPageSettings mode="On" securityRemovePort="False" makeAllPagesSecure="False">
        <files>
            <add pagename="WebForm1" />
        </files>
    </secureWebPageSettings>

secureWebPageSettings mode=”Off”   - Disables the http to https flipper, which you will want to do if you are using Visual Studio’s built in web server as that does not support https.

secureWebPageSettings mode =”On”   - Enable the flipper, use this when the code is running on an IIS server.

secureWebPageSettings securityRemovePort = “True”   - This will remove the port from the URL (to be used in scenario 6 I mentioned at the top of this article).

secureWebPageSettings makeAllPagesSecure = “True”   - All pages on your site will go secure.

secureWebPageSettings makeAllPagesSecure = “False”   - Only pages using the RequireSSL attribute or those listed in the <files> section in the web.config will go secure.  All other pages will flip back to non-secure.

In the add files section you should add a line for each page that you want to go secure.  The pagename is the class name of the page (as in the code behind page e.g. WebForm1)

namespace WebApplication
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }
}

To test the code you will need an SSL, you can create your own on IIS6 using the SelfSSL tool that comes with the IIS6 Resource Kit that can be downloaded here: http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=17275.

Or on IIS7 here is a good post on setting up a self certificate: http://weblogs.asp.net/scottgu/archive/2007/04/06/tip-trick-enabling-ssl-on-iis7-using-self-signed-certificates.aspx

And finally on IIS7 when the app pool is running in Managed Pipeline Mode = Integrated you will need to add the JL.Web.Security.RequireSSLModule to the ‘Modules’ section in IIS Manager for the website.

Modules

AddModule

Here is the source code

5 comments:

  1. Interesting Jon. Great work indeed.

    ReplyDelete
  2. Very good and simple tool... Works great.

    But one little detail, I'm hosted on Godaddy Servers and when switching to HTPPS (or back to HTTP) the URL starts showing the subdirectories where the page resides while it is was displayed before.

    Any idea?

    ReplyDelete
  3. Nice article, very similar to the asp.net security switcher project though.

    ReplyDelete
  4. Interesting stuff... but I've been unable to identify what license you make this code available under. Would you please provide licensing info so this code can be used appropriately?

    ReplyDelete