Ruby Sinatra Authentication

Topics like authentication often give me the heebie-jeebies. I worry about nefarious hackers in some corner of Beijing trying to hack into my account by somehow circumventing the authentication mechanism I put in place. To fight the situation, I would write the entire authentication routines myself, but I worry that I haven’t tested it thoroughly; on the other hand, I worry about using a library solution that I don’t fully understand and could therefore leave myself open to an attacker that does fully understand the solution.

A good compromise is to understand a bit about authentication and then use a known solution. When it comes to Sinatra, both are within easy grasp.

First, understand that there is a difference between authentication and encryption. Authentication is simply the process of identifying yourself as an authorized user of some service. Encryption, on the other hand, is totally hiding the communication so that it cannot be read and understood by anyone else. You can have authentication without encryption, and you can have encryption without authentication. (Admittedly, the latter case is not very useful.)

What I describe here is authentication without encryption.

The problem with authentication insofar as HTTP is concerned, is that right after a person authenticates him/herself, the connection is dropped. HTTP is just that way. Unless it’s running Node.js, the server simply doesn’t implement persistent connections with client browsers. So unless you want your clients to send a username and password with every connection – which is impractical if not impossible – you’re going to have to temporarily store authentication information on the client browser in the form of a cookie.

For example, when you, as a user, login to a service, you present your username and password to authenticate. The server validates your information, sends you a cookie, and then terminates the connection.  Thereafter, when you reconnect to the service (perhaps to get another page or download a picture), you send the authenticated cookie with your request, and the server recognizes you and allows the connection to proceed.

The problem with cookies is that the guy innocently sipping coffee next to you at Starbucks might actually be intercepting your wireless connection and replicating your cookie, thereby allowing him to access your account. So you have to use cookies creatively to provide authentication to Sinatra on a continuous basis.

The good news is that Sinatra helps you by automatically creating a session with each connection. Sessions are similar to cookies. Sessions are tracked with a “session variable” that is stored on the client machine in much the same way as a cookie, but it hides information within an encrypted string. It’s actually under control of the underlying Rack middleware, so it appears as a cookie on your machine with the name, “rack.session.”

You enable this capability with the:

enable :sessions

command when initializing Sinatra. This tells Sinatra to create a session variable and store it on the client browser. Now keep in mind that every browser client will receive a unique session variable, whether or not it is logged in, so clients still need to authenticate. The advantage here is that the session variable is unique to each client machine and you can actually put critical user-related information inside it without having to worry about prying eyes.

You begin this process by having the client authenticate with the server as follows:

  1. Client connects with server and asks to be logged in.
  2. Sinatra – running on the server – issues a session variable and sends it to the client, just like a cookie.
  3. Sinatra then sends client a challenge, which is essentially a random value, R.
  4. Client responds with the username and an MD5 hashed password as follows:
    MD5(R+MD5(password))
  5. Sinatra shouldn’t keep passwords in the clear in its database, but it typically does store the MD5 hash of each user’s password, so it can validate the client’s response, comparing the received MD5 value with a value that it calculates from its own MD5 of the user’s password. If the two values are equal, the client is considered authenticated. Sinatra then modifies the client’s session value by adding authentication information to it, using a key-value structure. This is trivial to perform within Sinatra:
    session['userstatus'] = 'authenticated'

    This new information is encoded into the session variable and can be queried by the server on each subsequent client connection.

Well, all done, right?

Almost.

The problem is that someone could still intercept the session information and replicate your user session before you get a chance to logout. Admittedly, it’s difficult, but it’s doable.

One way to solve this problem is to have the server create another key-value session variable that gets stored along with the authentication information. This new session variable is simply a counter that gets incremented with each exchange between the client and server.

For example, each time the server receives a connection from the client, the counter is checked against a value that was stored for that particular client. The value could be within a memcached array. If the two counts are not the same, then an invalid message was received from the client, and the client is then forced to login again. (Alternatively, the message could just be ignored.) On the other hand, if the counter is valid, its value is incremented and tucked away inside the session variable, to be used on the next exchange.

The counter gets encrypted, so a hacker would have no chance of learning its identity or – even worse – guessing its next value. The above scenario therefore severely cripples any chance at stealing the session and/or performing a replay attack.

The downside is that Sinatra must internally maintain a counter for each logged-in client. But memcached is cheap and easy to implement, so the severity of this downside is limited. Another possible downside may occur if the connection somehow gets interrupted and reestablished, and the counter value between the client and server gets out of sync. So you may want to loosen the tolerance on the counter somewhat and just have the server look for a count that is greater than the most recent successfully received client count.

Cheers,

Dan