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));
- 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.