Rolling your own authentication for REST API's, part 5 - the Keymaster and the Gatekeeper
Now that our application has users, we need to allow them to log in. As we discussed earlier, this process essentially just consists of two steps: generating temporary tokens, and matching tokens that come back in subsequent API requests to the corresponding user. The latter step also includes determining whether the token is even valid to begin with and, if so, whether it has expired. We'll now take a look at exactly how to do this.
Just as the data transport format you choose for your API is a decision you must make based on priorities you set for your application, you can choose any format you wish for constructing tokens. In order to provide one example of this technique, we will be modeling our authentication scheme based on some principles used by Toodledo, a cloud-based task management service. To be precise, we will construct a temporary token upon request to send to the consuming application. Instead of simply sending that token back, however, the consumer will be required to send back an API "key" based on that token accompanying any subsequent API request.
The format that Toodledo uses for an API key is as follows:
md5 ( md5 ( password ) + token + userId )
Let's express this format briefly in paragraph form. First, the user's password is hashed using the MD5 algorithm. Next we append the temporary token and the user ID to that result. Finally, we MD5 hash the entire combination one more time. The result is a 32-character mixed case key that is a representation not only of the user's credentials, but also of the period of time during which the key is valid.
In order to illustrate how we can generate the temporary token that will be used to construct the API key, let's take a look at the constructor of a TokenRequest class:
- setUserId(userId);
- setDatabase(database);
- DominoUtil util = new DominoUtil(database);
- try {
- if (!(userRecord == null)) {
- DateTime loginTime = util.getDateTime();
- setToken(tokenRecord.getUniversalID());
- tokenRecord.replaceItemValue("Form", "token");
- tokenRecord.replaceItemValue("userId", userId);
- tokenRecord.replaceItemValue("tokenCreated", loginTime);
- .getItemValueString("userPass"), getToken());
- tokenRecord.setUniversalID(HashMaster.md2(key));
- tokenRecord.save();
- } else {
- setError("User Id " + userId + " does not exist");
- }
- } catch (NotesException e) {
- setError("An unexpected error occurred while attempting to request a token");
- e.printStackTrace();
- }
- }
This code attempts to locate the user's account record. Remember, we don't have to search a view to locate this document: because the UNID of an account record is predictable, we know how to locate it even though the document that we'll retrieve doesn't actually store the user ID. Hence, if the hashed user ID does not match the UNID of an existing document, the specified user does not exist. Otherwise, the handle we now have on the account record was retrieved rapidly, no matter how much data our application currently contains. This performance implication is fundamental to why this technique is so useful when designing an application to be as scalable as possible.
If the specified user ID is valid, we create a document that represents the temporary token that is being requested. As before, we set some convenience field values – this time, both the form associated with the document and the creation date of the token. Similar to our user registration process, however, we have no need to ever store the token. As you can see, we're overwriting the UNID of the token document with a value of our own choosing, but until we do, it has a default UNID automatically generated by Domino; for the sake of convenience, this will be the token that we send back to the consumer. We could instead generate our own random or sequential identifier, of course, but Domino has already generated a sequential identifier for us, which we are about to discard anyway, so we may as well put it to good use while we have it.
Having set our return value to be the temporary, automatically-generated UNID, we retrieve the user's password from their account record, and pass it along with the user ID and token to a method that will determine what the corresponding API key must be when it is sent back by the consumer. Because we're already storing the MD5 hash of the user's current password, we don't need to hash it again when calculating the API key (NOTE: this is a slight change from what was discussed in the previous section in response to a suggestion from Karsten; the PDF version of this article will reflect this change in section 4):
- return HashMaster.md5(password + token + userId);
- }
We don't need to calculate this yet, of course: it's the consumer's responsibility to provide this key, not the token generation process. However... immediately before we save the token document, we set its new UNID to be an MD2 hash of the expected API key. As a result, when we do receive an API request that includes a key, determining whether that key is valid – and, if so, the identity of the corresponding user – is easy:
- DominoUtil util = new DominoUtil(database);
- if (!(tokenRecord == null)) {
- try {
- .getLastModified().toJavaDate();
- double timeDifferenceMinutes = (now.getTime() - then.getTime()) / 60000;
- if (timeDifferenceMinutes < 240) {
- result = tokenRecord.getItemValueString span>("userId");
- }
- } catch (NotesException e) {
- e.printStackTrace();
- }
- }
- return result;
- }
We simply hash the key, and try to find a document with a matching UNID. If none exists, that key was never valid. If a document does match, we check the last modified date of the userId item; if it's older than 4 hours (here, again, is an arbitrary portion of this approach that can be changed to suit your own priorities), then it's expired. But if the document exists, and the token it represents hasn't expired, then the userId item tells us on whose behalf the action specified in the API request should be performed. Because each requested token is valid for a specific period of time, we can inform all API consumers how long they can cache tokens within their own application before having to request a new one; they need not request a new token prior to every API request.
In the next section, we'll pull all of these concepts together to create a functional API.

Comments
Did you just use Domino's internal UNID algorithm as salt for your password hash? I'm pretty sure you did.
If I'm reading this correctly, that's extremely clever. Big props for that one. You're way beyond birthdays now, my friend.
Posted by Nathan T. Freeman At 11:59:11 PM On 06/23/2010 | - Website - |
And even if someone does find a match they end up with 64 characters of gibberish; to actually determine the user's password, they'd have to strip off the token and brute force the first 32. Granted, they'd have to intercept the key to begin with, and if they've done that, they can masquerade as that user until the token expires anyway, so it never hurts to go the SSL route if you suspect the sensitivity of the data provides enough motivation for a hacker to be sniffing packets and attempting to unhash API keys.
Posted by Tim Tripcony At 11:08:09 AM On 06/24/2010 | - Website - |