Form Based HTTP Authentication

One of the most fundamental aspects to web development is authentication. All too often, developers insist on developing their own security model using cookies, sessions, urls, etc. Instead, why not use a standards based approach using the browser's native HTTP authentication model? Developers raise several objections:

  • Browser login prompts are ugly
  • Browser based authentication doesn't provide a clean way to logout

In this article, I will illustrate how to implement form based authentication that overcomes these limitations. The code provided here has been tested on Internet Explorer 9, Firefox 13, and Safari 5. Browser technology changes quickly so what might work today, may not work tomorrow...

Standard HTTP Authentication

The standard HTTP Authentication model is pretty simple. When a client enters a restricted area, the server will respond with a 401 response:

HTTP/1.1 401 Access Denied
WWW-Authenticate: Basic realm="Access Denied"
Date: Mon, 25 Jun 2012 10:07:42 EST
Content-Length: 12
Connection: Keep-Alive
Server: JavaXT Web Server

Unauthorized

The browser, in turn, will present the client a login prompt that looks something like this:

Unfortunately, developers have little/no control of the look and feel of the dialog. To make matters worse, there's no obvious way to logoff! User supplied credentials are stateless and persist from site to site, session to session. Fortunately, there is an alternative.

Form Based Authentication

Form based authentication provides developers a means to implement a custom login form while leveraging the browser's native security model. This alleviates the need for cookies and http sessions for authentication. Here's an example of a custom html form used to perform authentication.

This dialog was generated using ExtJS but you can use whatever HTML/JS stack you like. Below is a simple html form that you can use. Note the "Log In" and "Log Off" buttons. They call javascript functions to login and logoff the user, respectively.



Javascript

One the javascript side, we have two functions used to process form inputs. In the login method, an AJAX request is made to login a user. There is a special case made for Firefox which is incorrectly caching credentials. To circumvent this issue, we first make a call to logout the current user before submitting a new login request. In the logoff method, there is browser specific logic to clear the authentication cache. Internet Explorer has a really nice way to do this. For everyone else, we need to make an AJAX request. Again, in the case of Firefox, there are some unique hacks to clear the authentication cache which results in a very chatty interface.


    var loginURL = "/WebServices/LogIn";
    var logoutURL = "/WebServices/LogOff";
    var userAgent = navigator.userAgent.toLowerCase();
    var firstLogIn = true;


    var login = function() {
        var form = document.forms[0];
        var username = form.username.value;
        var password = form.password.value;
        var _login = function(){

          //Instantiate HTTP Request
            var request = ((window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
            request.open("GET", loginURL, true, username, password);
            request.send(null);

          //Process Response
            request.onreadystatechange = function(){
                if (request.readyState == 4) {
                    if (request.status==200) alert("Success!");
                    else{
                        if (navigator.userAgent.toLowerCase().indexOf("firefox") != -1){
                            logoff();
                        }
                        alert("Invalid Credentials!");
                    }
                }
            }
        }

        var userAgent = navigator.userAgent.toLowerCase();
        if (userAgent.indexOf("firefox") != -1){ //TODO: check version number
            if (firstLogIn) _login();
            else logoff(_login);
        }
        else{
            _login();
        }

        if (firstLogIn) firstLogIn = false;
    }


    var logoff = function(callback){

        if (userAgent.indexOf("msie") != -1) {
            document.execCommand("ClearAuthenticationCache");
        }
        else if (userAgent.indexOf("firefox") != -1){ //TODO: check version number

            var request1 = new XMLHttpRequest();
            var request2 = new XMLHttpRequest();

          //Logout. Tell the server not to return the "WWW-Authenticate" header
            request1.open("GET", logoutURL + "?prompt=false", true);
            request1.send("");
            request1.onreadystatechange = function(){
                if (request1.readyState == 4) {

                  //Login with dummy credentials to clear the auth cache
                    request2.open("GET", logoutURL, true, "logout", "logout");
                    request2.send("");

                    request2.onreadystatechange = function(){
                        if (request2.readyState == 4) {
                            if (callback!=null) callback.call();
                        }
                    }
                    
                }
            }
        }
        else {
            var request = ((window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
            request.open("GET", logoutURL, true, "logout", "logout");
            request.send("");
        }
    }

Server-Side Code

On the server side, you can see how the login and logout requests are processed. The snippet provided here is for Java but if you're a PHP or .NET developer you should be able to follow the logic.

Note that there is a special server-side hack when processing logout requests for Firefox - specifically the "LogOff" request. Firefox needs a 401 response to clear the authentication cache. Typically, 401 responses include a "WWW-Authenticate" header. However, if the server returns a "WWW-Authenticate" header, Firefox will prompt the user for their credentials. As a workaround, the javascript (above) for Firefox will ask the server not to return the "WWW-Authenticate" header.


  //**************************************************************************
  //** processRequest
  //*************************************************************************/
  /** Used to process http get and post requests. */

    public void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, java.io.IOException {

      //Validate request (accept only SSL requests)
        String protocol = request.getURL().getProtocol();
        if (protocol.equals("http")) throw new ServletException(403);


      //Parse requested URL
        javaxt.utils.URL url = new javaxt.utils.URL(request.getURL());
        String path = url.getPath();
        if (path.length()>1 && path.startsWith("/")) path = path.substring(1);

        String service = path.toLowerCase();
        if (service.contains("/")) service = service.substring(0, service.indexOf("/"));

        String webmethod = path.substring(service.length());
        if (webmethod.length()>0) if (webmethod.startsWith("/")) webmethod = webmethod.substring(1);
        if (webmethod.endsWith("/")) webmethod = webmethod.substring(0, webmethod.length()-1);
        if (webmethod.length()==0) throw new ServletException(403);


      //Process logout directive as needed
        if (webmethod.equalsIgnoreCase("LogOff")){
            response.setStatus(401, "Access Denied");
            boolean prompt = new javaxt.utils.Value(request.getParameter("prompt")).toBoolean();
            if (prompt) response.setHeader("WWW-Authenticate", "Basic realm=\"My Site\""); //FF Hack!
            response.write("Unauthorized");
            return;
        }
        

      //Authenticate user
        String username = null;
        java.util.HashMap<String, String> credentials = getCredentials(request);
        if (credentials==null){
            response.setStatus(401, "Access Denied");
            response.setHeader("WWW-Authenticate", "Basic realm=\"My Site\"");
            response.write("Unauthorized");
            return;
        }
        else{
            username = credentials.get("username");
            String password = credentials.get("password");
            if (ActiveDirectory.authenticateUser(username, password)==false){
                response.setStatus(403, "Not Authorized");
                response.write("Unauthorized");
                return;
            }
        }


      //If we're still here, generate a response
        response.write("Hello World!");
    }



  //**************************************************************************
  //** getCredentials
  //*************************************************************************/
  /**  Returns username/password from an HTTP request
   */
    private java.util.HashMap<String, String> getCredentials(HttpServletRequest request)
        throws ServletException, java.io.IOException {

        String authorization = request.getHeader("Authorization");
        if (authorization!=null){
            String authenticationScheme = authorization.substring(0, authorization.indexOf(" "));
            if (authenticationScheme.equalsIgnoreCase("Basic")){
                String credentials = authorization.substring(authorization.indexOf(" ")+1);
                credentials = new String(javaxt.utils.Base64.decode(credentials));
                String username = credentials.substring(0, credentials.indexOf(":"));
                String password = credentials.substring(credentials.indexOf(":")+1);

                java.util.HashMap<String, String> map = new java.util.HashMap<String, String>();
                map.put("username", username);
                map.put("password", password);
                return map;
            }
        }
        return null;
    }

BASIC vs DIGEST

Note that the code presented here is for BASIC authentication but there is no reason why you can't use DIGEST instead. For most use cases, BASIC authentication should suffice - provided that you are running over SSL/TLS. Otherwise, user supplied credentials can be easily compromised. If you do not intend to secure your password-protected site using SSL, I recommend using DIGEST authentication.

Conclusion

Implementing form based authentication is relatively straightforward using a combination of JavaScript, AJAX, and Server Side logic. Of course, there are browser-specific hacks that you need to be aware of and should look out for as new browsers are released. Good luck!

References

Many thanks to the outstanding work published in these articles:

Firefox Bugs and Enhancement Requests:

Revision History

  • 6/22/2012 - Initial Publication
  • 6/23/2012 - Bug Fix for Firefox. Script was causing Firefox to prompt for credentials on initial login.
  • 6/25/2012 - Updated content to improve readability.