Lightweight, Decentralized Authentication

0

An essential feature of all respectable web frameworks is a reliable user
authentication mechanism. Nevertheless, one might be tempted to write his own
from scratch, and the reasons are many. It could be that you don’t like the way
your favorite framework does the task, or you might feel a need to tweak it a
little bit. Maybe you’re too paranoid to let a third party handle it for you!
You might not be using a framework in the first place, or you might even be
writing your own!

In this post, I’m going to discuss a simple mechanism that has served us
well in several projects. It’s straightforward and flexible enough to be used
in almost any project, but just like any solution to a software problem,
there’s no such thing as a silver bullet; you have to choose the right tool.
So, before we go any further, let’s first state precisely what we’re looking
for in our sought solution.

  • It has to be at least as secure as regular password-based authentication.
  • It has to be lightweight, minimizing database I/O as much as possible, so that it can be done for each and every request without adding too much overhead.
  • It should require minimum communication between server nodes.
  • It shouldn’t bind a user to a specific machine, allowing requests originating from the same user to be transparently handled by different machines.

It all starts off with a simple and familiar step: the user sends his login
and password, which are then compared to the stored ones. From that point,
things take a slightly different path. Remember that authentication is part of
each and every request. Done the usual way, this means an expensive db query at
the beginning of each request. Our goal here is to convert this into a
lightweight, on-the-fly step. So, how can this be achieved?

Consider this: what if after verifying that the login credentials provided
by the user are actually correct, the user is given a little box that can never
be opened (well, let’s say very hard to open) except by the one who made it,
and which contains a secret message which no one knows, except for the one who
wrote it. For each of the following requests, instead of providing the login
& password again, the user hands back the box. By looking at the enclosed
message, we can instantly determine whether it’s a authentic or fake, depending
on whether the contents match the secret we know or not.

Of course, in programmers’ jargon, that box translates to a secret message
encrypted at the server using a symmetric key, which will be referred to in the
rest of this post as the authentication token, or just as token.
Depending on the size of the secret message and the encryption algorithm used,
decrypting the message should be much faster than querying the user’s password from
the database (from my own experience, the ratio can be something like 1:20
using Triple DES and a secret almost 100 bytes long, but your mileage may
vary).

This can be implemented in a myriad of ways, and it’s not tied to any
technology in particular. Actually, the concept is used over & over in
numerous places so I’m not claiming it’s new. I’m just showing how you can fit
it inside your application.

For that purpose, and since the web community is currently rediscovering
HTTP with the rise of RESTful web services, I’ve chosen to stick as faithfully
as possible to the HTTP protocol throughout my demonstration, giving examples
on how the same can be achieved in different scenarios.

Using HTML forms to send login credentials is known to everyone, but HTTP
actually has built-in support for several authentication schemes (yes, I’m
talking about that annoying modal dialogue that asks for password in some sites
that require authentication). HTTP authentication works as follows: whenever a
client accesses a URI that resides within a set of restricted pages (called realm
in HTTP lingo), the client is challenged by the server. At that moment, the
client is supposed to provide a challenge response. Depending on the
authentication scheme, the challenge response can be one of several things. In
the BASIC scheme, which is what we’re concerned with, the client should provide
a base64 encoded string that when decoded should look like this: <username>:<password>

To simplify things, and since we’re assuming that each request has to be
authenticated, the client is always assumed to embed a challenge response in
each request. If the client fails to do so, the server should reply with 401
Unauthorized
. On the other hand, if the client supplies a valid
username/password combination, the server composes and sends the authentication
token in a response header (a custom header that should preferably include your
application name, for example, to avoid name collision). The encrypted token
should contain the secret as mentioned before, but should also contain
something unique to the user, such as the user ID in the database, or the
username. Otherwise, tokens generated for different users will be identical,
and that’s certainly not wanted!

On the other side of the wire, when the token is received by the client in
the response, it should be extracted & used in the challenge response of
each subsequent request, where the client will still use the BASIC scheme, but
with the username omitted, so that the decoded challenge response should look
like this: <empty string>:<token>

This is so that the server knows whether the client is using a password or a
token.

Client State Diagram 

Figure 1: State diagram describing how the client chooses the appropriate authentication method. 

Server Flow Chart 

Figure 2: Flow chart showing what happens at the server when authenticating a request.

A disadvantage of this method is that there’s no obligation on the
client to ever use the token; it can choose to send the password every
time, and the server can only play along. However, if you have some
control on how the client behaves, this won’t be an issue, but I’m
saving that for another post.

Depending on the capabilities of the client (for instance, the
subset of the HTTP protocol it supports, or if it’s not using HTTP
altogether), you have the freedom to implement this in many different
ways. For instance, you can pass back the token in a cookie, JSON
object, XML response or whatever format you wish. Note, however, than
an encrypted token will most probably have non-ASCII characters, so you
might find it necessary to base64 encode it after encryption.

One very useful extension of this whole thing is to make the
authentication token assume the role of the user session. If the
session is small enough to be piggy backed on each request, then you
needn’t keep anything at all about the user session on the server! This
makes the server completely stateless, allowing user requests to be
served from anywhere. If this is doable in your application, then it’s
even better than session caching, because unlike a cache, it doesn’t
require any maintenance. In my opinion, this is the most interesting
aspect of this approach (and I believe this is the approach endorsed by
Rails 2.0, by the way).

One overlooked issue, however, is: what if someone stole a user’s
token by sniffing the victim’s packets? After all, it’s still an
authentic token, isn’t it? Well, this is not different than
password-based authentication. Both are equally vulnerable to that kind
of attack. Your best call is to use HTTPS. Alternatively, but not as
equally safe, is to add the user’s IP address to the token contents, so
that the token is considered valid only if it’s coming from an IP that
matches the one inside it. Finally, the key & secret used by the
server to create the token should be regularly changed, and in that
case, old tokens will cause the client to receive a status code
indicating session expiration, and the cycle repeats again starting
with password login.

 

Post a Comment

eSpace podcast Prodcast

RSS iTunes