SslHelper - Get help running a partial SSL website in ASP.NET

Security Briefs

Syndication

Over the last couple of years, I've worked on websites that support both HTTP and HTTPS, and it's always tricky to find a balance between security and usability. Dominick wrote an excellent article about this awhile back, suggesting that allowing ASP.NET to make the choice between HTTP and HTTPS makes a website more friendly. He wrote a little class called SslTools that did the heavy lifting.

It's generally safer and much less tricky to get security right when you run your entire website over SSL. But if you do choose to go the mixed route and allow HTTP vs. HTTPS access depending on the page (or section of the site), I strongly recommend that you take a few minutes to read Dominick's original article, Partial SSL Secured Web Apps with ASP.NET. He shows how you can ensure that login cookies are only sent over SSL (prevents eavesdroppers from hijacking logon sessions).

So naturally when I wanted to implement this, I grabbed Dominick's SslTools class and started using it. Since Dominick made his original post, I've rewritten and improved his class substantially, so much that I have created a new version of it called SslHelper. The major features that I wanted to add were:

* Preserves your HTTP port (helps when running in VS.NET debug environment on alternate HTTP port)

* Better support for relative URIs

* Support for App-relative URIs (e.g., ~/default.aspx)

* Support for unit testing outside of ASP.NET

* Make it easy to turn off SSL for the entire website (for debugging)

* Uses System.Uri in the interface (FxCop friendly)

I told Dominick that I was going to publish this class once I had a chance to extensively test it, and it seemed there for awhile that every month I would either find a bug or a feature that I wanted to add, so I've waited until now to publish it. It's been rock solid for me and is being used in live websites for quite some time now.

There's a couple of places you'll want to use this class: early in the ASP.NET pipeline to ensure the use of HTTP or HTTPS on particular pages, and anywhere else you want to get an absolute URL that uses a particular scheme (HTTP or HTTPS). The latter is particularly useful when you want to include a link to a web page in an email sent by your website.

Here's an example of how you might use this class in global.asax to redirect users to the appropriate scheme (HTTP or HTTPS) depending on the section of your website:

SslHelper sslHelper = new SslHelper();
if (Request.Path.StartsWith(Request.ApplicationPath
+ "/internal/",
StringComparison.InvariantCultureIgnoreCase))
sslHelper.EnsureHTTPS();
else sslHelper.EnsureHTTP();

Once you have this code in place, you can easily turn off SSL for debugging without changing your code. Just add an appSetting called "EnableSSL", with a value of "false", and SslHelper will notice this and will use HTTP for everything.

Here's an example that gives you a URI that you can use in an email message, or as a navigate URL. Note that you can use tilde syntax for convenience:

Uri target = new Uri("~/login/UserHome.aspx", UriKind.Relative);
Uri sslTarget = new SslHelper().GetAbsoluteUri(target, ProtocolOptions.Https);

Over the last year, I've extensively used, debugged, and improved this class, so it should be immediately useful. Feel free to use it in your own projects without worry about licensing. If you'd attribute the original work to Dominick and myself as in the comments below, I would appreciate it. The usual caveats apply - use at your own risk!

So without further ado, here's SslHelper!

// This code was originally written by
// Dominick Baier (www.leastprivilege.com)
// His class was called SslTools.
// I made several improvements and renamed it SslHelper.
// - Keith Brown, http://www.pluralsight.com/keith/
namespace Pluralsight.Utilities
{
public class SslHelper
{
public interface IApplication
{
void Redirect(Uri uri);
Uri CurrentRequestUri { get; }
bool IsCurrentRequestUsingSsl { get; }
Uri ResolvePossibleAppRelativeUri(Uri uri);
}

// default behavior uses HttpContext.Current
// implement your own if you want to test
// outside of ASP.NET
public class DefaultApplication : IApplication
{
public void Redirect(Uri uri) {
HttpContext.Current
.Response.Redirect(
uri.ToString());
}
public Uri CurrentRequestUri { get {
return HttpContext.Current
.Request.Url;
} }
public bool IsCurrentRequestUsingSsl { get {
return HttpContext.Current
.Request.IsSecureConnection;
} }
public Uri ResolvePossibleAppRelativeUri(
Uri possibleAppRelativeUri)
{
// can't be app relative if it's an absolute URI
if (possibleAppRelativeUri.IsAbsoluteUri)
return possibleAppRelativeUri;

string queryString;
string urlWithoutQueryString =
SslHelper.StripQueryStringFromRelativeUrl(
possibleAppRelativeUri, out queryString);

if (VirtualPathUtility.IsAppRelative(
urlWithoutQueryString)) {
string absolutePath =
VirtualPathUtility.ToAbsolute(
urlWithoutQueryString);
Uri currentRequest =
HttpContext.Current.Request.Url;
UriBuilder builder = new UriBuilder(
currentRequest.Scheme,
currentRequest.Host,
currentRequest.Port,
absolutePath);

// using the builder to build the entire URI
// is problematic when the query string contains
// URL encoded chars like & because the builder
// unescapes them. So the following doesn't work:
// builder.Query = queryString;

if (string.IsNullOrEmpty(queryString))
return builder.Uri;
else
{
string uriWithoutQuery =
builder.Uri.AbsoluteUri;
string absoluteUri = string.Format(
"{0}?{1}", uriWithoutQuery,
queryString);

return new Uri(absoluteUri,
UriKind.Absolute);
}
}
else return possibleAppRelativeUri;
}

}

IApplication application;
IApplication Application { get { return application; } }

public SslHelper(IApplication application) {
this.application = application;
}

public SslHelper() : this(new DefaultApplication()) { }

public void Redirect(Uri uri) {
Redirect(uri, RedirectOptions.Relative);
}

public void Redirect(Uri uri, RedirectOptions options) {
if (options == RedirectOptions.Relative) {
Application.Redirect(uri);
return;
}

Uri absolutePath = null;
if (options == RedirectOptions.AbsoluteHttp) {
absolutePath = GetAbsoluteUri(uri,
ProtocolOptions.Http);
}

if (options == RedirectOptions.AbsoluteHttps) {
absolutePath = GetAbsoluteUri(uri,
ProtocolOptions.Https);
}

Application.Redirect(absolutePath);
}

public Uri GetAbsoluteUri(
Uri uriFromCaller,
ProtocolOptions protocol) {
// you can turn off SSL when testing
if (ProtocolOptions.Https == protocol &&
!SslIsEnabledInConfig())
protocol = ProtocolOptions.Http;

// deal with ~ (ASP.NET "AppRelative" paths)
if (!uriFromCaller.IsAbsoluteUri)
uriFromCaller = Application
.ResolvePossibleAppRelativeUri(uriFromCaller);

if (uriFromCaller.IsAbsoluteUri) {
UriBuilder builder = new UriBuilder(
SchemeFor(protocol), uriFromCaller.Host);
PreservePortIfPossible(builder, uriFromCaller);
builder.Path = uriFromCaller.GetComponents(
UriComponents.Path, UriFormat.Unescaped);

// UriBuilder.Query unescapes escaped query strings,
// which hoses me for stuff like ReturnUrl
// so I'm doing that part manually
// builder.Query = uriFromCaller.GetComponents(
// UriComponents.Query, UriFormat.UriEscaped);

string query = uriFromCaller.GetComponents(
UriComponents.Query, UriFormat.UriEscaped);
if (query.Length > 0) {
string uriWithoutQuery = builder.Uri.AbsoluteUri;
string absoluteUri = string.Format("{0}?{1}",
uriWithoutQuery, query);
return new Uri(absoluteUri, UriKind.Absolute);
}
else return builder.Uri;
}
else { // relative URI
Uri currentRequestUri = Application.CurrentRequestUri;

UriBuilder builder = new UriBuilder(
SchemeFor(protocol), currentRequestUri.Host);
PreservePortIfPossible(builder, currentRequestUri);
builder.Path = currentRequestUri.GetComponents(
UriComponents.Path, UriFormat.Unescaped);
return new Uri(builder.Uri, uriFromCaller);
}
}

private static void PreservePortIfPossible(
UriBuilder builder, Uri originalUri) {
// if scheme is unchanging, preserve the port,
// otherwise use default port
// this is helpful if you're running this in
// Visual Studio under some random port with SSL disabled
if (originalUri.Scheme.Equals(builder.Scheme))
builder.Port = originalUri.Port;
}

private static string SchemeFor(ProtocolOptions protocol) {
return ProtocolOptions.Https == protocol ?
Uri.UriSchemeHttps : Uri.UriSchemeHttp;
}

public void SwitchToSsl() {
if (SslIsEnabledInConfig())
Redirect(new Uri(
Application.CurrentRequestUri.PathAndQuery,
UriKind.Relative),
RedirectOptions.AbsoluteHttps);
}

public void SwitchToClearText() {
Redirect(new Uri(Application.CurrentRequestUri.PathAndQuery,
UriKind.Relative),
RedirectOptions.AbsoluteHttp);
}

public void EnsureHTTPS() {
if (SslIsEnabledInConfig()) {
if (!Application.IsCurrentRequestUsingSsl)
SwitchToSsl();
}
}

public void EnsureHTTP() {
if (Application.IsCurrentRequestUsingSsl)
SwitchToClearText();
}

public static bool SslIsEnabledInConfig() {
// SSL is enabled by default,
// unless you add an AppSetting for EnableSSL
// and set it to "false"
return !false.ToString().Equals(
ConfigurationManager.AppSettings["EnableSSL"],
StringComparison.InvariantCultureIgnoreCase);
}

public static string StripQueryStringFromRelativeUrl(
Uri possibleAppRelativeUri, out string queryString) {
// Uri class isn't very friendly to relative URIs
// have to work with strings here as far as I can tell
// so I supplied this helper to make it easier
// to implement IApplication.ResolvePossibleAppRelativeUri
string appRelativeUrl = possibleAppRelativeUri.ToString();
int queryIndex = appRelativeUrl.IndexOf('?');
if (-1 == queryIndex) {
queryString = "";
return appRelativeUrl;
}
else {
queryString = appRelativeUrl.Substring(queryIndex + 1);
return appRelativeUrl.Substring(0, queryIndex);
}
}
}

public enum RedirectOptions
{
Relative,
AbsoluteHttp,
AbsoluteHttps,
}

public enum ProtocolOptions
{
Http,
Https
}
}

Posted Jan 17 2009, 02:43 AM by keith-brown

Comments

DotNetShoutout wrote SslHelper - Get help running a partial SSL website in ASP.NET
on 01-18-2009 10:28 PM

Thank you for submitting this cool story - Trackback from DotNetShoutout

infocyde wrote re: SslHelper - Get help running a partial SSL website in ASP.NET
on 01-19-2009 7:39 AM

Thanks for sharing this code, I hope to try it out soon, it looks very useful.

Pablo M. Cibraro (aka Cibrax) wrote Running a partial SSL website with ASP.NET MVC
on 01-19-2009 9:49 AM

Keith Brown has just released a helper class (Based on an original implementation made by Dominick Baier

Christopher Steen wrote Link Listing - January 26, 2009
on 01-26-2009 11:47 PM

Link Listing - January 26, 2009

Christopher Steen wrote Link Listing - January 26, 2009
on 01-26-2009 11:50 PM

Sharepoint New Release of the SmartTools for SharePoint Project [Via: Jan Tielens ] Creating SharePoint...

Tim Abell wrote re: SslHelper - Get help running a partial SSL website in ASP.NET
on 02-05-2009 5:02 AM

Uploaded to github to aid collaboration and updates.

github.com/.../sslhelper

Thanks for your collective hard work.

Jason Morton wrote re: SslHelper - Get help running a partial SSL website in ASP.NET
on 03-11-2009 12:36 PM

Thanks alot to both of you.