Site Meter
 
 

Adding HTTPS/SSL support to ASP.NET MVC routing

ASP.NET MVC is introducing a number of brilliant enhancements that make .NET web development much neater than ever before. One of them is the new routing system, which makes it dead easy to handle “clean” URLs, even automatically generating outbound URLs from the same schema.

Unfortunately, as of Preview 4, routing has a missing feature (oh noes!): it’s got no support for absolute URLs. Everything it does works in terms of application-relative “virtual paths“, so you simply can’t generate links to other subdomains, port numbers, or even switch from HTTP to HTTPS (or vice-versa). As they say, the design is never perfect first time.

In many web applications, you do need some way of getting visitors into/out of SSL mode, so routing’s limitation does actually bite you. What I’d like to be doing is marking certain route entries as being “secure”, so they always generate URLs containing https:// (unless the visitor is already in SSL mode, in which case they generate a relative URL, and the non-secure route entries then generate absolute URLs containing http://). That deals with getting visitors in to SSL, and later back out of it automatically.

One way it could work

Perhaps you could configure it like this:

routes.Add(new Route("Account/Login", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Account", action = "Login" }),
    DataTokens = new RouteValueDictionary(new { scheme = "https" })
});
routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });

With this configuration (notice the DataTokens value), you’d be saying that the Login() action on AccountController must be referenced over HTTPS, whereas all other actions get referenced over HTTP. Which means that:

  • Html.ActionLink(“Click me”, “Login”, “Account”) would render:
    <a href=”https://yoursite:port/virtualDirectory/Account/Login“>Click me</a>
  • Whereas Html.ActionLink(“Click me”, “About”, “Home”) would render:
    <a href=”/virtualDirectory/Home/About“>Click me</a>

… assuming the current request is over HTTP. If the current request was over HTTPS, then it would be the other way round (the first link would be relative, and the second would be absolute) – to get them out of HTTPS.

And what’s so difficult about that?

Firstly, let’s get back to the limitation in System.Web.Routing‘s design. It only works in terms of virtual paths. So how can it ever be possible to generate absolute URLs? One option is to create a custom Route subclass, and simply return absolute URLs when you need to (even though you’re only supposed to return virtual paths). It’s a hack, but it might just work. Sadly, life never is that easy.

Problem 1

Some gremlin in the routing underwurlde fiddles with the URL after you’ve generated it, prepending the path to your virtual directory (and also normalizing the URL, e.g. to remove double slashes). What a pain! You’ll generate http://www.google.com, and it gets converted to /http:/www.google.com (notice the leading slash, and the loss of the double slash). That totally wrecks absolute URLs.

Problem 2

How do you make a normal routing configuration aware of a DataTokens entry called scheme, and make it respect it, generating absolute URLs when it needs to? The normal Route class doesn’t have any notion of this. And I don’t want to have to change my routing config in any weird way.

Possible solutions

If RouteCollection was smart enough to spot absolute URLs and not fiddle with them, you’d have solved problem 1. (This is a hint to any MS developers reading :) )

Then, if you used the pseudo-route entry trick I described in my previous post, you could intercept URLs after they’re generated, converting them to absolute URLs when needed. That would solve problem 2.

I’m getting bored. Show me the solution.

Dear lucky reader, here’s an implementation:

1. Start by downloading AbsoluteRouting.zip. It’s a C# class library project. Put it in your solution and compile it yourself. (And obviously, put a project reference from your main MVC project to the AbsoluteRouting project.)

2. In your web.config file, replace UrlRoutingModule with AbsoluteUrlRoutingModule, i.e. put:

<system.web>
    <httpModules>
        <add name="UrlRoutingModule" type="AbsoluteRouting.AbsoluteUrlRoutingModule, AbsoluteRouting"/>
    </httpModules>
</system.web>

3. At the top of your routing configuration, drop an EnableAbsoluteRouting entry, i.e. put:

public class GlobalApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
	// Here it is!
        routes.Add(new EnableAbsoluteRouting());
 
	// Rest of routing config goes here
	routes.MapRoute(...)
    }
}

UPDATE: If you want to use nonstandard ports (i.e. not ports 80 and 443), you can configure it like this:

routes.Add(new EnableAbsoluteRouting()
                 .SetPort("http", 81)
                 .SetPort("https", 450));

4. For any route entries that you want to force into SSL, add a DataTokens entry:

routes.Add(new Route("Account/Login", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Account", action = "Login" }),
    DataTokens = new RouteValueDictionary(new { scheme = "https" })
});

Note that if you don’t specify any scheme value, it will be assumed to demand http (thereby switching visitors back out of SSL mode for links to non-secured routes).

AbsoluteUrlRoutingModule does some nasty weirdness to squish the routing gremlin that destroys absolute URLs, and EnableAbsoluteRouting intercepts the outbound URL generation process to turn relative URLs into absolute ones if it needs to force the link on to a different scheme to the current request. Hopefully, it doesn’t screw anything else up in the process.

Parting comments

I don’t think this is great. It’s a hack; it abuses routing; you’re just not supposed to generate absolute URLs. Also, the code isn’t pretty (read it yourself), and it hasn’t been tested in the real world. It’s just a proof of concept!

However, it does get the job done unintrusively, leaving the rest of your application unchanged (in theory). I’d be interested in anyone else’s ideas about how to deal with the central real-world problem, which is getting visitors into and out of SSL mode in an ASP.NET MVC application. I’d also be interested in hearing any official guidance about how to deal with HTTPS with System.Web.Routing – anyone know of any?

PS: Troy Goode has an idea about how to link to SSL in ASP.NET MVC. His technique is fine (and certainly a lot less hacky than the one I just showed you). However, that technique requires you to fiddle with every Html.ActionLink(), remembering to convert its output when you want an SSL link. You can’t just say in any one central place that a certain action method is supposed to be requested over SSL – not as DRY as I’d like. But as I said, his technique is less hacky.

21 Responses to Adding HTTPS/SSL support to ASP.NET MVC routing

  1. Steve

    Note: this code isn’t compatible with “cookieless sessions”. But it’s very unlikely that you’ll use cookieless sessions (I’ve never seen them in the real world).

  2. I took a different approach & decorated my controller actions with [RequireSSL]… of course we still have the issue of outbound routing (ie: links/forms) but I think this with a combination of extra view helpers can solve the problem.

  3. Ben,

    That is what we have done as well (use a [RequireSSL] attribute). I definitely think this is something where there should be a baked-in answer provided by the MVC framework team.

  4. Steve

    @Ben, @Troy: Yeah, after going through the whole process of creating AbsoluteRouting, I’d probably go with your technique (using [RequireSSL], and managing entry/exit to HTTPS manually). However, if I had a large number of entry/exit points, it would be worth considering AbsoluteRouting.

    Ultimately, AbsoluteRouting is a more “correct” solution, in that it’s more DRY and separates concerns more cleanly, but pragmatically, as System.Web.Routing stands today, it’s too messy to achieve. Your technique looks like the best one right now. Maybe a future version of System.Web.Routing will deal with this natively!

  5. Pingback: To (ASP.NET)MVC or not to MVC (or, ASP.NET MVC Hyperlink Acupuncture) « MOVED TO: thefreakparade.com

  6. Hi Steve,
    Thank you very much for the great idea.

    I’ve found couple of problems with it.
    Here is the fix.

  7. Steve

    Hi Dmitriy – thanks for letting me know! I’ve updated AbsoluteRouting.zip.

  8. Pingback: To (ASP.NET)MVC or not to MVC (or, ASP.NET MVC Hyperlink Acupuncture) | The Freak Parade

  9. Faisal

    Hi, This is an excellent stuff. I used this small project in my web solution and is working fine, shows me all https links but I dont understand one thing that routing cant keep the url as https. It converted into normal http once you click and in the page.. For checking and debugging purposes I used Phil Hacked http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx debugger. I noticed that it doesnt show me DataTokens with this link and dont execute user defined “sheme” DataToken route.. Can you please help me out..Am I missed something here?

  10. Steve

    Sorry Faisal, I’m not really sure what you mean. Maybe you could email me with a step-by-step list of what you’re doing, what you expected to happen, and what happened instead.

  11. sam

    I hope a solution for cookieless mode, because I am working on a mobile site building project. as you know, almost mobile devices do not support cookie mode.

  12. Pingback: SSL support in ASP.NET MVC « Exploring the world …

  13. So if i understand this correctly if i go with this solution, will have to add a new routes entry for every link/button that i have in “SSL zone”?

  14. Really great post, thanks Steve for the many insights! I’ve come up with an alternative approach which uses the MVC2 native [RequireHttps] attribute and handles exiting from HTTPS with another custom attribute. I’m quite happy with it’s DRYness, and I’d love to hear what you think of it.

    Here it is:
    http://lukesampson.com/post/471548689/entering-and-exiting-https-with-asp-net-mvc

  15. Marco

    Steve

    Is this still the *recommended* practice with the latest release of MVC?

  16. Steve

    My question is what about the subdomain part of the URL?
    Does this redirect to
    https://mywebsite.com
    I need to redirect to
    https://secure.mywebsite.com

    Is this handled automatically by IIS, do I need to set up an IIS rule on the server or do I need to extend the filer RequireHttps?

    The RequireHttps and custom ExitHttpsIfNotRequiredAttribute from Luke here http://lukesampson.com/post/471548689/entering-and-exiting-https-with-asp-net-mvc works well, combined with a debug/release if statement to allow you to develop locally (or setup a test certificate in IIS)
    #if !DEBUG
    [RequireHttps]
    #endif

  17. Steve

    I think I have just answered it.
    Create a replacement for RequireHttps as:

    public class EnterHttpsSecureSubDomainIfRequiredAttribute : FilterAttribute, IAuthorizationFilter
    {
    public void OnAuthorization(AuthorizationContext filterContext)
    {
    // abort if i is a secure connection
    if (filterContext.HttpContext.Request.IsSecureConnection) return;

    // abort if it’s not a GET request – we don’t want to be redirecting on a form post
    if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, “GET”, StringComparison.OrdinalIgnoreCase)) return;

    // redirect to HTTP
    //might need to remove secure. here
    string url = “https://secure.” + filterContext.HttpContext.Request.Url.Host.ToLower() + filterContext.HttpContext.Request.RawUrl;
    //string url = “http://” + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
    filterContext.Result = new RedirectResult(url);
    }
    }

    and then use as before for the non https, see Luke’s article but change the last line to:

    public class ExitHttpsIfNotRequiredAttribute : FilterAttribute, IAuthorizationFilter
    {
    public void OnAuthorization(AuthorizationContext filterContext)
    {
    // redirect to HTTP
    string url = “http://” + filterContext.HttpContext.Request.Url.Host.ToLower().Replace(“secure.”, string.Empty) + filterContext.HttpContext.Request.RawUrl;
    }
    }

  18. Terza

    Steve,

    I am trying to implement this solution using ASP.NET 4 and MVC 2 (we are still not switched to the MVC 3) and it seems that is not working as expected.

    I am always getting URL that’s like, for example, this:
    http://localhost/site/https:/localhost/site/user/login

    Maybe we have some better solution or AbsoluteRouting got to be updated according to the latest releases of MVC and ASP.NET?

  19. I do not even know how I ended up here, but I thought this post was good I don’t know who you are but certainly you are going to a famous blogger if you aren’t already Cheers

  20. Peter Maas

    Absolutely right, I do agree with your post. By the way I am 27 years old guy & I work for Software Company. Since my childhood I wanted to spend vacation at riverside & I love fishing. My co worker suggested me Kenai river fishing is the best place. Can anyone give feedback about them? I will appreciate.