Site Meter
 
 

Monthly Archives: August 2008

Using the browser’s native login prompt

These days, every web site has its own unique “login” screen, along with its own separate system for remembering your login name and password. How many millions of developer-hours are spent designing and implementing these screens? And yet, every web browser has a built-in standard login prompt ready for you to use. For example:

image image

These have been standard in web browsers since since 1558 AD, when the legendary Duke of Login first invented the idea of logging in. Respect!

Most web developers who want to do programmatic authentication (i.e., validating credentials against Forms Authentication or directly against a database) don’t use this native browser login prompt, for two reasons:

  • It’s not clear how to make it work with programmatic authentication
  • It’s believed to be insecure (for some reason)

First, I’ll show how to use the native login prompt programmatically with ASP.NET MVC, then we’ll talk about security.

A Quick Overview of the HTTP Basic Authentication Protocol

So, how does the native login prompt actually work? What makes it appear, and what data does it send from the browser to the server?

  1. The browser makes a request to some URL
  2. The server sends back a response with an HTTP status code of 401 (meaning “Not authorized”), plus a header describing the types of authentication it will accept. For example:
        WWW-Authenticate: Basic
  3. This makes the browser display a login prompt, but it doesn’t display any other text that’s in the response. (It only displays that response text if the user clicks “Cancel”.)
  4. When the user enters some credentials, the browser resubmits the same request to the same URL, plus it also adds this extra header:
        Authorization: Basic username:password
    Note that the username:password bit is actually Base-64 encoded.
  5. The server parses the username and password from the request, and decides whether the credentials are valid or not. If they are valid, it lets the user continue (so it might return a proper HTML response, or it might redirect to somewhere else). If they are invalid, it returns a 401 again (i.e., goes back to step 2).
  6. If the user enters the same incorrect credentials twice in a row, the browser normally won’t bother resubmitting them and will just give up.

Using HTTP Basic Authentication programmatically in ASP.NET MVC

Now you know how HTTP Basic authentication works, it’s easy to use it from ASP.NET MVC. Just follow the above script, playing the role of the server.

Let’s say you want to combine Forms Authentication with a browser-native login prompt. Start by setting up Forms Authentication, i.e., put into your web.config file:

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" defaultUrl="~/">
    <credentials passwordFormat="SHA1">
      <user name="admin" password="e9fe51f94eadabf54dbf2fbbd57188b9abee436e" />
    </credentials>
  </forms>
</authentication>

Note that e9fe51… is the SHA1 hash of “mysecret”, so this configuration has a single hard-coded login name, “admin”, with password “mysecret”. In a more realistic app you’d probably not have any <credentials> in your web.config, and instead set up a membership provider to store credentials in a database. But that doesn’t change the rest of this example.

Now, assuming you’ve decorated some controller or action method with [Authorize], when the user visits that controller or action, they’ll be redirected to ~/Account/Login. To handle that request, create a new controller class called AccountController, as follows. You can replace the default implementation of AccountController if you have one.

public class AccountController : Controller
{
    public void Login()
    {
        // Ensure there's a return URL
        if (Request.QueryString["ReturnUrl"] == null)
            Response.Redirect(FormsAuthentication.LoginUrl + "?ReturnUrl=" + Server.UrlEncode(FormsAuthentication.DefaultUrl));
 
        if (TempData.ContainsKey("allowLogin"))
        {
            // See if they've supplied credentials
            string authHeader = Request.Headers["Authorization"];
            if ((authHeader != null) && (authHeader.StartsWith("Basic")))
            {
                // Parse username and password out of the HTTP headers
                authHeader = authHeader.Substring("Basic".Length).Trim();
                byte[] authHeaderBytes = Convert.FromBase64String(authHeader);
                authHeader = Encoding.UTF7.GetString(authHeaderBytes);
                string userName = authHeader.Split(':')[0];
                string password = authHeader.Split(':')[1];
 
                // Validate login attempt
                if (FormsAuthentication.Authenticate(userName, password))
                {
                    FormsAuthentication.RedirectFromLoginPage(userName, false);
                    return;
                }
            }
        }
 
        // Force the browser to pop up the login prompt
        Response.StatusCode = 401;
        Response.AppendHeader("WWW-Authenticate", "Basic");
        TempData["allowLogin"] = true;
 
        // This gets shown if they click "Cancel" to the login prompt
        Response.Write("You must log in to access this URL.");
    }
}

(By the way, I’m fully aware that the Login() action eschews a number of ASP.NET MVC best practices – it doesn’t return a useful ActionResult, and it calls Response.Redirect() directly. This makes it unsuitable for unit testing. I did this because fundamentally it’s using the static and hard-to-test FormsAuthentication API anyway. You can wrap all the static method calls inside an interface and use constructor injection, and perhaps return some special HttpBasicActionResult, if you want to make it testable – but I didn’t want to distract from the real point of this example.)

That does it! Now when a visitor goes to anything protected with [Authorize], they’ll get a browser-native login prompt, such as the one shown below. If the visitor enters valid credentials (i.e., admin/mysecret), then they’ll be given a Forms Authentication cookie, and will be redirected back to the action method they requested.

image

Notice in this screenshot that IE gives a warning about “basic authentication without a secure connection“. We’ll talk about secure connections (SSL) in a moment.

Next, you’ll want to give visitors some way of logging out. This has nothing to do with HTTP basic authentication; it’s just a matter of revoking the visitor’s Forms Authentication cookie. So, add this to AccountController:

public RedirectResult Logout()
{
    FormsAuthentication.SignOut();
    return Redirect(FormsAuthentication.DefaultUrl);
}

What a very simple way of getting a nice login UI.

Is this secure?

HTTP Basic authentication has an undeserved reputation for being insecure. Yes, it does send credentials over the wire in plain text (well, Base-64 encoded, but that’s basically the same). But then if you make a custom login form (such as the one in the default ASP.NET MVC project template), that sends credentials in plain text too. The level of security is identical.

Either way, you must protect the transmission by doing it over SSL. And that’s just as easy, or difficult, whether you use the browser’s native login prompt or create your own custom login screen.

One quirk of HTTP basic authentication is that the browser keeps on sending the Authorization header with every request that appears to be in the same folder as the one where it was originally requested. So, in this example, the browser will keep sending the Authorization header with every request to AccountController (but not to other controllers). That means you shouldn’t let the browser perform any requests to AccountController that aren’t wrapped in SSL. Also, HTTP basic authentication doesn’t give any natural way to log out, which is why I added the TempData["allowLogin"] test so that you always get a login prompt the first time you go to Login(). When a visitor clicks “log out”, it does erase their Forms Authentication cookie, but the browser still has the credentials in its HTTP Basic cache. The user acts as logged out, but the credentials are still in the browser’s memory until they close the browser.

Conclusion

Using ASP.NET MVC, it’s easy to make a browser pop open its native login prompt, and to parse out the credentials that a user enters. These login credentials are no more or less secure than credentials entered into a normal custom login form.

However, it’s also easy to create a custom login form. This gives you more control over its appearance, and avoids the quirks of HTTP basic authentication with regard to logging out. Therefore, for most applications, it’s usually best still to create a custom login form.

This article was expanded from a short example that I was originally going to put in my forthcoming ASP.NET MVC book, but I decided to remove it from the book because it isn’t quite worthy enough…

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.