Friday, 24 May 2013

http:// to Https:// Redirecting between SSL Secured and Non-secured Pages in ASP.NET


Redirecting between SSL Secured and Non-secured Pages in ASP.NET


Introduction

This is a simple JavaScript solution to prevent 403.4 errors during redirects in ASP.NET applications where some pages are secured with SSL.

Background

While I was working with SSL encryption on a recent project, I realized the need to have a dynamic and reliable way to redirect the browser to the next page regardless of whether the current page uses SSL and the next page doesn't, or visa versa. Server solutions didn't work. I read several forum posts suggesting that the programmer use code-behind on the server like this, placed in the Page_Load event:
Dim strURL As String = Request.Url.ToString()
If Request.IsSecureConnection Then
    If strURL.IndexOf("http:") > -1 Then
    strURL = strURL.Replace("http:", "https:")
    Response.Redirect(strURL)
    End If
Else
    If strURL.IndexOf("https:") > -1 Then
    strURL = strURL.Replace("https:", "http:")
    Response.Redirect(strURL)
    End If
End If
This method uses Request.IsSecureConnection to test whether or not SSL is required by IIS for the current page, then Response.redirect to send the browser back to the same requested URL, replacing the http: with https: for the second request. This idea seemed to make sense until I realized that IIS 7 (and maybe other versions of IIS) doesn't pass the request to the web application if a page that is secured with SSL is requested without the https: prefix. Instead, an error is raised in IIS and the user gets an ugly 403.4 error page which, despite Microsoft's best efforts to help users solve their own problems, reads like instructions on how to program a VCR from 1988.
To solve this problem, other coders suggested creating a custom error page for the 403.4 error and using JavaScript on that page to correct the location.href property to the https: prefix and redirect to the corrected URL. The idea is that when IIS receives a bad SSL request (a user requests a secured page with the http: prefix), it redirects the user to a custom error page at the server root, something like "sslredirect.html," which then uses JavaScript to resolve the original bad URL and redirect again. This solution falls a little short because it still allows the actual http: / https: error to occur in IIS and then handles it with an HTML page, a method which causes two extra server requests. (The first request occurs when IIS sends the user to the custom 403.4 error page "sslredirect.html," the second request occurs when sslredirect.html goes ALL the way to the user's browser, then JavaScript in that HTML page changes the page location back to the requested URL, this time with an https: prefix, which then redirects back to the server as a new page request). Web-programming best practices tells us that the fewer roundtrips and requests you make to your web server, the better. Another short-coming of this solution is that it doesn't help redirect the user to a non-secured page (with the correct http: prefix) when they leave a page with the https: prefix. Basically with this method, once a user browses to an SSL secured page with the https: prefix, all the rest of the pages they visit on the same site will use the https: prefix. Plus, my web host charges $10 extra to have a custom 403.4 redirect page setting installed in IIS!
I thought about it for a while and realized that maybe the idea of trying to catch a 403.4 error and handle it was looking at the problem in the wrong way. Instead of waiting for the 403.4 error, the coder needs a way to prevent the SSL redirect problem from ever happening in the first place. In aspx pages and their code behind, the coder generally wants to use relative URLs instead of typing out the FULL web address for every <a> (anchor) tag. ...I recommend using <a> anchor tags over <asp:hyperlinks> whenever possible, BTW... the hyperlink tag in ASP.NET has no real benefit over a simple <a>, except that the hyperlink's properties are available in the code-behind page, should you need to change them dynamically. ALSO,<asp:hyperlink> tags must be rendered down to <a> tags by the app server before the page is sent to the client, so you might as well save the server CPU some work by using <a> links instead.
Back to relative URLs... in an aspx page, a relative URL might look like this, "../myapp/homepage.aspx" or this "/users/login.aspx," but when the page is rendered in the browser, the / (root) or ../ (up one level) part of the relative URL is actually converted to the FULL URL. Think about it - when the page arrives at the browser, the <a> tag has no idea what the "../" is relative to, so the URL it renders in the page MUST be complete. The problem this causes for a website with SSL and non-SSL pages is that ALL the <a> tags using relative URLs in an aspx page have their prefix written the same way as the current page's URL. For example, the https://samplesite/users/login.aspx page will render an <a> tag with href="../home.aspx" as "https://samplesite/home.aspx" even though the "home.aspx" page doesn't need SSL. Likewise, the pagehttp://samplesite/home.aspx will render an <a> tag with href="users/login.aspx" as "http://samplesite/users/login.aspx" which will cause IIS to throw a 403.4 error because of the missing https: prefix in the URL of a page that requires SSL. This is where my JavaScript solution came into play.

Using the Code

Two small bits of JavaScript solved this problem for me. One goes in a master page (I highly recommend using master pages) and another goes in any page secured with SSL. Here's the master page JavaScript, placed at the BOTTOM of the page, just before the </body> closing tag:
<script type="text/JavaScript">//<![CDATA[
var currHref;
if (location.href.indexOf('http:')>-1)
{var SSLlinks = document.getElementsByName('SSL');
for (var i = 0;i<SSLlinks.length;i++)
{currHref = SSLlinks[i].href;
currHref = currHref.replace('http:','https:');
SSLlinks[i].href = currHref;}}
//]]></script>
This bit of code tests to see if the current page is unsecured with location.href.indexOf('http:')>-1, then makes a collection of all objects on the page with the name "SSL." Those objects are anchors which redirect to SSL secured pages, marked with the same name attribute:
<a href="mysecurepage.aspx" name="SSL">Goto Secure Page</a>
Or, for asp:HyperLink users:
<asp:HyperLink id="link" NavigateUrl="mysecurepage.aspx" name="SSL" runat="server">
Goto Secure Page</asp:HyperLink> 
should work the same way, but I don't know for sure as I avoid using <asp:HyperLink>s like the plague.
If you don't have the same SSL cert setup in your development environment and don't want https: redirects happening in your local solution, you can change the 3rd line in the above JavaScript to this:
if (location.href.indexOf('http:')>-1 && location.href.indexOf('localhost')<0) 
This replacement line checks to see if the page is being run from "localhost" and ignores the rest of the code if it's running in a local development environment. I recommend removing this "localhost test" code before you deploy.
The above JavaScript function loops all objects with the "SSL" name in the rendered page (after it reaches the client) and changes their href to the https: prefix. At this point, no anchor pointing to a secure page will incorrectly send the user to an http: prefixed page, so long as the anchor or hyperlink has a name="SSL."
But this code only solves half the problem... a second bit of JavaScript is needed to send the user from an SSL secured page back to a non-secured page. This script is placed at the bottom of the page, just before the </body> closing tag in any aspx page secured in IIS with SSL:
<script type="text/JavaScript">//<![CDATA[
var currHref;
var links = document.getElementsByName('noSSL');
for (var i=0;i<links.length;i++){
currHref = links[i].href;
currHref = currHref.replace('https:','http:');
links[i].href = currHref;}
//]]></script>
This function gets a collection of all objects with the name "noSSL", then loops those objects to replace the href prefix "https:" with "http:" (Without this code, all relative URLs on an SSL secured page will be rendered in the browser with an https: prefix, even if they redirect to a non-secured page). Now when a user leaves a secure page for a non-secured one, the prefix will always be corrected to http: long before they click on the link. Make sure all <a> tags on the secured page which redirect to an unsecured page have the "noSSL" name:
<a href="../unsecuredpage.aspx" name="noSSL">Goto Unsecured Area</a> 
Or,
<asp:HyperLink id="link" NavigateUrl="../unsecuredpage.aspx" name="noSSL" 
 runat="server">Goto Unsecured Area</asp:HyperLink> 
So this is the simplest solution I have seen to redirecting between an SSL secured page and a non-secured page in an ASP.NET solution. No extra server requests or round-trips, no IIS 403.4 errors and JavaScript redirect HTML pages. All of the work is done on the client, saving server processor time, and the programmer can still use relative URLs in their <a> tags and <asp:hyperlinks> knowing the https: and http: prefixes are properly rendered in the browser BEFORE the user ever clicks on them.
This solution does require that JavaScript is enabled in the browser, which is a small limitation, but if your users aren't using JavaScript, well, they're luddites.

No comments:

Post a Comment