JavaXT
|
|
Form Based HTTP AuthenticationOne 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:
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 AuthenticationThe 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 AuthenticationForm 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. JavascriptOne 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 CodeOn 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 DIGESTNote 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. ConclusionImplementing 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! ReferencesMany thanks to the outstanding work published in these articles:
Firefox Bugs and Enhancement Requests:
Revision History
|