Secure Sessions with Social OS

A classic problem in security is confirming that someone is who they say they are - without keeping a secret code lying around that could potentially be stolen or copied. And the classic solution is to use a password, but rather than recording the password itself for later verification, recording a cryptographic hash of the password. Cryptographic hashes are what are known as trap-door functions, because it is easy to go through in one direction, but very difficult in the other. (The goal is for reversal to be impossible, but we don't live in a perfect world.)

Early examples of hashing functions included the Unix crypt algorithm, MD5, and SHA-1, though none of these are considered robust by modern standards. Social OS uses PBKDF2 with SHA-512, making for passwords thousands of times more secure than SHA-1. Other examples of modern, secure hashing functions include bcrypt and scrypt.

If you are building a system where the user knows the password, and the application needs to verify it, any one of those three algorithms (PBKDF2, bcrypt, scrypt) should do a good job of keeping your passwords safe. But if you are working with a multi-tier environment, where the application is issuing requests on behalf of users to other APIs – such as Social OS – then the application needs to not only confirm the user’s identity, but to be able to reproduced the user’s credentials with the API as well.

Three Tiers...

The situation we will explore is the simplest multi-tier system: A user with a web browser, an application layer, and an API working in the background. With web-based applications there’s a simple and robust default for securing the connection – HTTPS, which uses SSL/TLS encryption. We’ll assume that the application is using HTTPS to communicate with both the user and the API.

We want to achieve three things here: First, we create a session at the application level so that the browser doesn’t need to send the username and password with every request, protecting the user as much as possible from browser and network-based attacks. Second, we create a session at the API level (a session key), so that even if the credentials are stolen by an attacker, their use is time-limited (and optionally function-limited). And third, we encrypt the session details held by the application so that even if the application database is compromised, those details cannot practically be used to access the Social OS instance storing the users’ data.

The specific steps required to implement this process are as follows:

  • When the user logs in to the application, we create a session key in Social OS using the user’s primary key.
  • The API session consists of a token and a password. The token is returned to the browser in a session cookie.
  • The token is then hashed – we use SHA-1 for this, as this is only one stage in the encryption – and used by the application as the session ID.
  • We encrypt the API password with AES (a reversible block cipher), using a combination of the token and an application-level password as the encryption key. We store the encrypted password against the session ID in the application’s database.
  • For subsequent requests, the browser only needs to provide the session cookie. (Unless the session times out, or the user logs out.)
  • When the user logs out, we delete the session key from the Social OS instance, delete the application session record from the database, and delete the session cookie from the browser. Any one of these steps would suffice to maintain security, but there’s no sense in leaving invalid access credentials lying around; that just makes maintenance difficult.

...Many Benefits

This sounds rather complicated, so what are the real-world benefits?

  • The only opportunity for an attacker to steal the user’s credentials directly is at login time, and then only if the user is on an insecure network. HTTPS should always be used for logins. Indeed, HTTPS should be used for all data transfers requiring any degree of security or privacy.
  • If the application database is compromised, the attacker gets only a list of hashed session IDs and encrypted passwords.
  • There are two ways to defeat cryptographic hashes: By guessing the original value, and by finding a hash collision. Since the original value here is a randomly-generated session token of configurable length, we can make it as hard to guess as we like for effectively zero cost; there is no issue with users selecting weak passwords.
  • On the other hand, if the hashing algorithm is weak and the attacker finds a hash collision, the plain text producing that collision will not be the right key to decrypt the password.
  • Even then, unless the attacker has also stolen the application’s security key, they won’t be able to decrypt the session password except by brute-force calculation. There are ways to protect the security key to make difficult for attackers to retrieve; we will discuss this in a future article.
  • The API session can be configured precisely to the application’s requirements, so that even if all the above measures are bypassed, the session provides no more access than is needed for the common case operation of the application. If you need to provide elevated access to a user, you can require them to log in again – though of course this is a trade-off between security and convenience.
  • For applications where security is critical, the session duration can be tailored appropriately – so that even if an attacker retrieves the entire password database, they have only hours before the contents become worthless.
  • Finally, the Social OS instance powering the application is not necessarily available to the attacker, so that even if they retrieve a user’s token and password, they may have no opportunity to use it.
    Implementation

Despite all the complicated details, implementing this three-tier, doubly-encrypted security model with Social OS is quite straightforward. Here is a basic login method, implemented in Python:

def login(self, user, password):
                api = API()
                key = api.post('key', user=user, password=password)
                id = sha1(key['token']).hexdigest()
                pcrypt = encrypt(key['password'], '%s%s' % (securekey, key['token']))
                Session(id, data={'auth': pcrypt, 'user': user})
                setcookie('session', key['token'])
                expirecookie(['session'], 3600)

                return True

To authorise a session within the API library:

def authdetails(self):
                assert self.id, 'No session data available.'

                session = database.Session.objects.find_one(id=self.id)
                if session:
                    data = session.data
                    user = data['user']
                    password = decrypt(data['auth'], '%s%s' % (securekey, getcookie('session')))

                    return user, password

                raise Unauthorized

And to securely delete a session:

def logout(self):
                '''To log out, delete the API key, the session record, the data in the
                session object, and the session cookie, n that order.'''
                assert self.id, 'No session data available.'

                api = API()
                api.auth()
                api.delete('key/%s' % cherrypy.response.cookie['session'])
                database.Session.objects.delete_one(id=self.id)

                self.id = None
                self.data = None

                delcookie('session')