Channels ▼
RSS

Web Development

Implementing Secure Login, Part 2


Last month, we looked at all the issues surrounding secure login in an AJAX application. This month, will look at code: a generic login implementation that you can slot into your application with minimal work. The code is all JavaScript, with a small Java Servlet, so it will work in any generic AJAX app. (The implementation actually coexists with GWT just fine, but I'm not going to cover the GWT issues this month.)

The Login Object in Action

I'm an OO sort of guy, so the login system is very object oriented. Architecturally, login is done with a single object that serves as an intermediary between the user and the server. That object is defined in an HTML file called loginPanel.html, loaded into its own <iframe>; but it's best to think of this file as a class definition for the login class. The <iframe src='.../loginPanel.html'> effectively instantiates an object of that class.

This "login object" handles the entire login process, interacting with the server when required. The login object notifies the containing page when the user successfully logs in or out. As is the case with any object, all communication both to and from the object is done by sending messages. (Server-side messaging is done using a REST protocol over HTTP, and client-side messaging is done using the nested-frame strategy we looked at last month.)

On the client side, all that you need to do to use the login object is instantiate it with a <iframe src='https://.../loginPanel.html'> somewhere on your page. Note the https protocol — that's important. The containing page does not have to be secure, so it doesn't have to be loaded using https, but the login object does have to be secure. You also must set up a very small JavaScript object to handle the messages sent by the login object to the containing page. On the server side, you need to provide a handler for incoming messages by deriving a class from a Servlet-derived base class and overriding a small number of methods. These methods handle storing and retrieving the password from the data store, and so forth. We'll look at all this setup in detail shortly.

Before delving into the code, let's examine how the login object works. Like any properly implemented object, it's responsible for creating its own UI, and it displays itself in different ways depending on state. Figure 1 shows the login object in the logged-out state. The Create Account link is placed by the containing page (as is the page title), but everything else is created by the login object itself.

Figure 1: The login object in the logged-out state.

If you forget your password, clicking the forgot link causes the login object to redraw itself to a basic password-recovery box (Figure 2). You can either cancel out of that and get back to the original login box, or you can submit your user name. (Everything that happens from that point is up to the server — we'll look at those details in a moment as well.)

Figure 2: The login object doing password recovery.

When you log in successfully, the login object enters the logged-in state. It gets a login-session key from the server and stores that key in a cookie (in the secure URL's namespace). The login object then redraws itself again, but this time as a simple logout link (Figure 3). The login object has notified the containing page at this point, and the containing page creates the Account Preferences and positions it under the login object.

Figure 3: The login object in the logged-in state.

When you log out, the login object destroys the client-side version of the login-session key and notifies both the server and the containing page that a logout has occurred.

When the login object first loads, it looks for the cookie that holds the login-session ID; if it finds the cookie, the login object attempts to log in using the session ID before it displays anything at all. (Note that this session ID is secure. It was passed to the login object from the server using HTTPS and stored in the HTTPS-URL namespace on the client, so it can't escape into the wild.) If the login is successful, the login object goes directly into the logged-in state, displaying itself as a logout link. To make things a little easier on the end user, the login object effectively enables the remain-logged-in feature for a couple minutes after you leave a page, even if you didn't check "remain logged in." This way, you can leave the page briefly and then come back to it without having to log in again. This feature is disabled when you hit the log out link, though. Once you're logged out, you're logged out.

There's one other other display option that you can enable from the query string in the URL used to pull in the login object from the server. The login object can display itself initially as a single login link (Figure 4). (The gray border is there on my test page so that I can see the extent of the <iframe> that holds the object.) When you click login, the object redraws itself (Figure 5) to look much like the earlier version, but this time with a big red "X" button that you can click to dismiss the login box.

Figure 4: The initially hidden login object.

Figure 5: The login object in the logged-out state when initially hidden.

Setting Up the Client Side

Now, let's look at the code you need to get the login object to work in your program. You have to do two things on the client side:

Create message handlers

First, you need to define a small JavaScript object to handle notifications from the login object. I do that inside my page's <head> element as follows:

<script type="text/javascript">
    var listener = new Object();
    listener.login           =  function(stringReturnedFromServer)  
                                {   token = unescape( stringReturnedFromServer );
                                    //...
                                };
    listener.logout          =  function()      
                                {   //...
                                };
    listener.recoverPassword =  function( htmlReturnedFromServer )  
                                {   text = unescape(  htmlReturnedFromServer  );
                                    //...
                                };
                                
    window.top.loginListener = listener;
    window.top.loginPanelURL = 'https://myApp/login/LoginPanel.html;
</script>

I've declared an object called listener that has three methods in it. The login method is called when a successful login occurs (either from a username/password or from a previously stored login-session ID). In this case, the server can pass an arbitrary string down to the client, which is passed to this method as an argument. There's no secure information in this string, by the way. The login-session ID is managed by the login object, so you don't have to do anything with it.

The second method is logout, which is called when the user logs out (but not when he or she simply leaves the page).

The third method, recoverPassword, is called during the password recovery process. The login object passes the sever the user name, the server figures out if the password for that user is recoverable, and then it returns an arbitrary string back down to the client. Typically, this string will be the HTML for a password-recovery dialog. Since the password-recovery process varies considerably from one website to another, I didn't want to build it in to the login object.

The last thing you have to do in JavaScript is to define two variables at the top level of the DOM. loginListener points at the object we just looked at. loginPaneURL contains the URL of the login object (we'll come back to that in a moment). The login object will not work if it doesn't find these two variables.

Instantiate the Login Object

Moving down to the <body> element, instantiate the login object itself in the body of the page using a <iframe> element like the following one:

    <iframe src='https://secure.myDomain.com/login/loginPanel.html?mediator=http://myDomain.com/login/loginMediator.html&useLoginButton=true&prompt=User+Name'
                     id='loginPanel' name='loginPanel' scrolling='no' frameborder='0'>
    </iframe>

Use CSS to style the frame as desired. I usually use:

<style type='text/css'>
    #loginPanel {
         float:right;
         width:2.5in;
         height:.8in;
         padding: 5 5 5 5;
         margin-right:1em;
    }
</style>

The src URL effectively pulls in a login-object class definition, and the URL's query string (to the right of the ?) effectively provides constructor arguments for the object.

It is essential that loginPanel.html be pulled in using the https protocol, and the domain from which you download the frame be different from the domain used for content that doesn't have to be secure. I use holub.com for standard content and secure.holub.com for secure content, and I suggest that you use the same subdomain-based approach. Using a separate domain assures that the cookie that holds the login-session ID, which must be kept secure, won't "leak out" into insecure content where it can be stolen by a hacker. On the server, I usually map this subdomain to the same physical directory system as the main domain. That is, both the main domain and the secure subdomain have the same "docroot." That way, I don't have to scatter source code files between two server-side directory systems.

This URL (without the query string) is the one that you put into the window.top.loginPanelURL JavaScript variable I mentioned earlier.

You can put up to four constructor arguments in the query string:

  • useLoginButton=
    If true, the login object initially displays as a login link that you click to display the username/password-prompt UI.

  • prompt=
    Specifies the actual prompt string for the user-name input field in the UI. The default string is "User Name," but you can use this argument to change it to something like "Email Address."

  • mediator=
    You'll remember from last month's article that the best way to send a message from an https frame (the login panel) to the containing page is to instantiate a small <iframe> that runs a little JavaScript. That <iframe> must be instantiated from the same "origin" as the containing page (same protocol, domain, and port). This argument specifies the URL of the file that contains the JavaScript that sends the login, logout, and recover password messages to the containing page, and it must have the same "origin" as the containing page. The loginMediator.html file is part of the login package — you don't have to write it. (We'll look at the source code in a moment.) The mediator= argument just tells the login object where to find the file.

  • redirect=
    If this argument is present, then the login panel redirects to another page if the login is successful. That is, it replaces the entire current page — login panel and all — with a new one, usually loaded using https. This argument takes the form redirect=http://secure.mydomain.com/loggedIn.html. The containing page is not notified anymore (because the containing page is replaced by whatever page we just redirected to). This is by far the simplest way to handle the login, but it's a Web 1.0 approach. If you use redirect=, you don't have to provide the loginListener object that I discussed in the previous section, and you don't need to provide a mediator= query argument in the current URL.

Setting Up the Server Side

So, that's it for the client side. Now let's look at the server. Two classes are used. The first isLoginManager, which handles the hard work of parsing the REST requests that the login object uses to talk to the server. It also defines a few password-management methods for use by other classes. The second is LoginManager, which is opaque. It does it's work, then delegates the application-level work to the four abstract methods that are defined in LoginManagerAdapter (Listing One). For those of you who care (do you detect a plaintive note of desperation? You should care!), this is an example of the Template Method design pattern, which is not usually my architecture of choice. The fact that LoginManager is a Servlet makes other patterns, like Strategy, impractical, however.

Listing One

package com.holub.login.server;

import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** This class provides a stub for (and an example of) how to hook server-side login support up to the
 *  login-manager servlet. You can register this class as a servlet to test the system. You can
 *  also extend this class to inherit default test methods if you want to use them. The default
 *  implementations of the methods log a "severe" error (using the java.util.logging system). For
 *  testing purposes, they recognize a username and password of "user" and "password" as valid and
 *  everything else as invalid, and correctly process a re-login if remain-logged-in is specified.
 *  <p>
 *  The login-test methods return a small JSON String of the form:
 *  <pre>
	{'value':'User-supplied login value (login 1)'}
 *  </pre>
 *  where the number is incremented on every login attempt. The recover-password method returns
 *  the following HTML:
 *  <pre>
 *  {@value #PASSWORD_RECOVERY_STRING}
 *  </pre>
 * 
 * @author allen
 */

public class LoginManagerAdapter extends LoginManager
{
	private static final long serialVersionUID = 1L;
	private static final Logger log = LoggerFactory.getLogger(LoginManager.class);

	private static String currentHashedPassword = LoginManager.hashPassword("password");  // STORE THE HASHED VALUE IN THE DATABASE !
	private static String currentLoginSessionID = null ;
	private static int	  count = 0;
	
	private static final String PASSWORD_RECOVERY_STRING = "<b>Internal \"error\"</b>: Could not recover password." ;
	
	//----------------------------------------------------------------------
	// ServerSideLoginSupport overrides
	//----------------------------------------------------------------------
	
	@Override public String checkPasswordValid(String userName, String password, String loginSessionID) throws Exception
	{
		log.error("**** SIMULATING **** login on server: " + userName + "/newID=" + loginSessionID  );
		
		// Verify the plain-text candidate password against the hash stored in the database
		//
		if( !(userName.equals("user") && LoginManager.verifyHash(password, currentHashedPassword)) )
			return null;
		
		// In the real code, this value should be cached in memory and backed up to the database. You'll
		// need it to handle the remain-logged-in feature. Do not return this value in the return-value String.
		//
		currentLoginSessionID = loginSessionID;
		
		return "{'value':'User-supplied login value (login " + ++count + ")'}";
	}

	@Override public String verifyLoginID(String userName, String oldLoginSessionID, String newLoginSessionID) throws Exception
	{
		log.error("**** SIMULATING **** verify on server: " + userName + "/oldID=" + oldLoginSessionID +"/newID=" + newLoginSessionID );
		
		// If the presented (old) session ID matches the cached one, then we can log in.
		//
		if( !oldLoginSessionID.equals(currentLoginSessionID) )
			return null;
		
		// Replace the old session ID with a new one. Not, strictly speaking, necessary, but it improves security.
		//
		currentLoginSessionID = newLoginSessionID;
		return "{'value':'User-supplied login value (verify " + ++count + ")'}";
	}

	@Override public boolean logout(String userName, String loginSessionID )
	{
		// if the session ID doesn't match the cached one, then this request is a hacker trying to log off someone else!
		
		log.error("**** SIMULATING **** logout on server: " + userName + "/" + loginSessionID  );
		return true;
	}

	@Override public String recover(String userName, String requestURI )
	{
		log.error("**** SIMULATING **** password recovery on server ("+ requestURI +"). user=" + userName + ". Returning: " + PASSWORD_RECOVERY_STRING );
		return PASSWORD_RECOVERY_STRING;
	}

	public void initializeLoginServices(HttpServletRequest request)
	{	log.info("Initializing login-support from class: " + getClass().getName() );
	}
}

If you decide to use this file (and other Java sources) as is, note that in addition to the standard dependencies, I'm using the slf4j logging adapter (another design pattern: Object Adapter) available from slof4j.org. You'll have to put the slf4j jars onto your classpath to get a clean compile, and look at the slf4j docs to figure out which jars to include — they'll vary. This nifty little package isolates your code from a specific logging system. You log to slf4j, and it relays the log messages to the system of your choice (log4j, java.util.logging, etc.). I use slf4j because my logging system of choice is log4j; but when I deploy to the Google App Engine, I have to log using java.util.logging. Slf4j lets me switch from one logging system to the other without touching the source code (or the class files, for that matter).

Moving on to the actual class definition, LoginManagerAdapter extends LoginManager and provides overrides of the required template methods. Modify this code as required, or extend this class and override the methods you care about. The default implementations log a error-level message saying that they're simulating the real behavior, and in fact, they will correctly simulate logging in a user named "user" with the password "password." This way, you can use this class as a "mock object" to test the initial installation, and then incrementally replace the mock methods with ones that actually do something. The methods work as follows:

  • checkPasswordValid(userName, password, loginSesionID)
    If the user name and (plain text) password match, then return an arbitrary String that's relayed to the client-side container that holds the login object. This String is the argument to the listener.login JavaScript method we looked at earlier. If you take a look at Listing One, you'll see that the mock object handles the password the same way that the real implementation should handle it. The BCrypt hash of the password is stored, not the plain-text password, and checkPasswordValid(...) uses the base class's verifyHash(plainTextPassword,hashedPassword) method to do the comparing. This method hashes the plain text version and compares the two hashes. You must store the hashed version, not the plain-text version, in the database.

    The account-setup process creates the password hash and puts it into the database, so there's no code that does that here. The account-setup code, however, uses the LoginManager.hashPassword(plainTextPassword) method to create the hash. The mock object uses that method to initialize the currentHashedPassword field, up at the top of the class definition.

    Finally, note that the base class passes checkPasswordValid(...) the login-session ID as an argument. Do not, under any circumstances, pass this value back down to the client using the return-value String, because that String is not secure. If the container page were loaded using http (not https), the String would be passed to the client using http (not https), potentially exposing the session ID to a hacker. The login-session ID is managed securely by the client-side login object itself, so you don't need to worry about it on the client side at all.

    The reason you're passed the login-session ID is that you'll need it later on to validate a returning user who's chosen to remain logged in. You should cache loginSesionID in memory (indexed by userName) and also put it into the database entry for that user so that you can restore the memory cache if necessary. The mock object just stores the value in an instance variable, but that approach won't work in real code — use a real cache like Jakarta JCS or JCache (which is Oracle's caching system as standardized by JSR 107). JSR 107 is in something of an orphan state, but The Google App Engine uses the JCache APIs. Google's implementation is based on the jsr107Cache project on SourceForge. I recommend using that system if you think you might deploy to the App Engine at some juncture. Find information about App-Engine caching here.

  • verifyLoginID(userName,oldLoginSessionID,
    newLoginSessionID)

    This method handles the automatic login that occurs if the user checked the "remain logged in" box. The first time this method is called, the oldLoginSessionID argument must have the same value as the loginSessionID argument to checkPasswordValid(...). Approve the login if the oldLoginSessionID for the specified userName matches the cached ID.

    The login system replaces the old login-session ID with a new one as part of the login process, and the login system passes us that new value (in newLoginSessionID) so that we can replace the one currently in the cache. That is, the login system uses a given login-session ID only once. The next time the system calls verifyLoginID, the oldLoginSessionID argument should match the newLoginSessionID argument of the current call.

    The mock object correctly replaces the old with the new value.

  • logout(userName, loginSessionID)
    Log out the indicated user. If the session ID doesn't match the cached one, this is a hacker trying to log out somebody else. This method should clear the login-session ID out of the cache entirely to force any subsequent verifyLoginID(...) calls to fail.

  • recover(userName, requestURL)
    Handle password recovery. The request URL is the URL from which the password-recovery request originates. You can use it to determine if the request is legitimate. You'll typically return the HTML for a password-recovery dialog, and this String is returned to the client as the argument to the listener.recoverPassword() JavaScript function we looked at earlier. The client will typically display this HTML in a dialog box.

  • initializeLoginServices(servletRequest)
    A one-time initialization method called from the base-class's (Servlet's) init() method. It doesn't have to do anything in particular, but if you need it, you need it.

Since the derived class is a Servlet, you'll need to register it in your web.xml file. The one I use is shown in Listing Two. Note that the URL (the "servlet-mapping") is .../loginManager, but the actual class is the derived class we just looked at (LoginManagerAdapter).

Listing Two

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <!--  Servlet that handles login requests. Change the URL to accommodate your deployment environment.
  		The URL should be in the same relative directory as the loginPanel.html file. For example,
  		if the login panel is in  war/gwt/loginPanel.html, then the URL pattern would be
    	<url-pattern>/gwt/loginManager</url-pattern>
  -->
  <servlet>
    <servlet-name>loginManager</servlet-name>
    <servlet-class>com.holub.login.server.LoginManagerAdapter</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>loginManager</servlet-name>
    <url-pattern>/login/loginManager</url-pattern>
  </servlet-mapping>
</web-app>

Sending Messages To The Login Object (Replacing the Default UI)

Though it's nice to use the login object as an off-the-shelf component, you can also use it as an invisible login agent that handles the session-key management and server-communication stuff, but doesn't expose a UI at all. To do that, just hide the <iframe> that holds the login object (src=loginPanel.html), and build your own UI to collect the user name and password. To actually log in or log out, you'll need to delegate to the login object by sending it a message.

Since we're communicating between frames, you'll send a message by instantiating a little JavaScript in a hidden <iframe>, with a src URL has the same origin as the login object (that is, of the loginPanel.html page used to instantiate the login object itself). The easiest way to do that is to put an empty <div> onto your page, and then, in the clickhandler for the login button, instantiate the frame dynamically into that <div> with the following JavaScript.

var mediatorURL = "myApp/loginRequestMediator.html" ;
var username = ...          // the user name to log in
var password = ...          // the user's password
var remainLoggedIn = ...    // true to remain logged in

var request =
        "<iframe src='"  + mediatorURL 
        + "?username="  + username
        + "&password="  + password 
        + (remainLoggedIn ? "&remainLoggedIn" : "")
        + "' style='position:absolute;width:0;height:0;border:0'></iframe>"
        ;

document.getElementByID('communicationDiv').innerHTML = request;

The mediatorURL must have the same origin (protocol, domain, port) as the login panel itself, and the loginRequestMediator.html file is part of the login package. We'll look at it in a moment. If you omit both the username and password arguments from the query string, the system logs out the user.

The Server-Side Implementation

We've covered the User's Guide — now for the implementation. Since it's easier, let's start with the server side. The LoginManager base class we extended earlier is in Listing Two.

Working from the top of the file down, doGet(...) handles the incoming REST requests from the client. The query string can specify four arguments:

  • action=
    Tells the servlet what to do. Recognized values are login, verify, logout, and recover.

  • userName=
    Required for all of the actions.

  • password=
    Required for the login action.

  • loginSessionID=
    This is the session ID that was created by the login or verify action. It's required for all actions except recover.

The result (put into the response object) is a small chunk of JSON created by one of the four methods just below doGet(...). Taking doLogin() as characteristic, it calls the checkPasswordValid(...) method that's defined in the derived class, then assembles one of two JSON strings, depending on whether or not things worked. In the event of a failure, it returns:

{ 'status':'FAIL','userData':'' }

If the login succeeds, doLogin(...) returns:

{   'status':'OK',
    'userData':'String returned from checkPasswordValid()',
    'loginSessionID':'theSessionIDForThisLogin'
}

That JSON is easy to parse on the client side, since it's legal JavaScript.

The next couple methods just chain to methods in the BCrypt class, which does the password hashing. I've included that class in the source code distribution, but you also can download the most recent version from Damian Miller's web site.

The only other method of interest is getSessionID(). This method, obviously, creates the login-session ID, and it goes to some trouble to make it universally unique. That is, the login-session ID has to be unique even if the system is distributed to several load-balanced servers, and it has to be both long enough and random enough that it's hard to attack using a brute-force attack. The generated session ID is a concatenation of a sequence of random bytes (which comes first for better distribution in a hash table), the current time, a counter that's incremented on every call, and the IP address of the server. A simple random number isn't good enough because random numbers can repeat (however unlikely, code shouldn't work solely because the odds are in favor of it working). You need the counter because the time has only millisecond resolution, so two requests might come in at the same effective time. The IP address is needed because two requests that come in at exactly the same time can be handled by different servers. I am assuming that the servers are all on the same subnet, so they'll have unique addresses. That assumption might be incorrect in some network topologies, but those would be very nonstandard configurations, and getting a machine's MAC address (a bulletproof solution) in Java is just too hard to do.

The default session-ID length (56 bytes) contains either 6 or 20 random bytes, depending on whether the server address is IPV4 or IPV6. The worst case is 48 bits or roughly 280,000,000,000,000 possible values, which should be secure enough to defend against a brute-force attack in most applications. You can use the keyLen argument to add (or remove) randomness. The shortest permitted key is 48 bytes, which has only 16 bits of randomness in the worst case. That's not a great choice if the app is running on the Internet, outside of the firewall. The short key can save some space in the database for enterprise-only applications, however.

Finally, note that I'm encoding the session key using standard hex encoding (two characters per byte). If you change the code to use base-64 encoding, you can squeeze way more randomness into the same number of characters or get an equally secure key in fewer characters. (Base 64 encoding uses one character for every 6, rather than 4, bits, so it's 2/3 the size.)

You can download Listing Three here.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV