bolln's log

Feb
03
2010

User authentication with a secure cookie protocol in PHP

Introduction

The HyperText Transfer Protocol (HTTP) is a stateless protocol. This means that hosts do not retain information about their interactions. However, web applications often need to maintain a state where the client can be identified when he sends a request to the server. Examples of such scenarios are user authentication, clickstream analysis and the storage of information (preferences, shopping cart, ...).

A common method to maintain a state over HTTP is the sending and receiving of cookies. Cookies are small pieces of data that can be appended to a HTTP response by the server. When the client receives the response, the browser will store the data and its metadata (expiration date, path and domain) locally. The cookie will be sent along with each HTTP request back to the server until it expires.

The use of cookies introduces a few security issues. The data in the cookie is stored in plain text and can easily be modified or hijacked. Liu et al. (2005) propose a secure cookie protocol which we will use as a guideline in this example. Their protocol offers the following services:

  1. Authentication: the server can verify that the client has been authenticated within a certain period of time.
  2. Confidentiality: the contents of the secure cookie can only be read by the server.
  3. Integrity: the server can detect if a secure cookie has been modified.
  4. Anti-replay: the server can detect if a cookie has been hijacked and replicated.

Using the secure cookie protocol for user authentication

Data integrity and authentication are the most important services that our user authentication protocol must deliver. A construction has to be placed in the cookie's data field that identifies the user for a period of time. To achieve this we will have to keep track of a user identification string and an expiration time. The user identification string is a unique string that can be associated to a user in the server's backend (in most cases the primary key of the user table in the database). Next to the user identification string, the time when the cookie expires is saved in the cookie's data field. This information is also available in the cookie's metadata, but is stored in the data field as well making it possible for the server to validate the expiration time without relying on client information that could be modified.

The cookie's data is stored in plain text and could be changed manually at any time. To safeguard the data's integrity we will add a hash to the data which cannot be recomputed by a party other than the server.

In the example of user authentication, confidentiality is not an issue because the contents of the cookie do not hold sensitive information which should not be read by third parties (low-level confidentiality) or the client (high level confidentiality). Low level confidentiality could be achieved by sending the cookie over a SSL connection. High level confidentiality can be enforced by encrypting the data of the cookie.

There are also possibilities to prevent replay attacks, but we will ignore these in this example to make it more straightforward.

The following construct is used in the cookie's data field:

test

The user identification is a string that the server can use to link the cookie to a certain user in the server's backend. The expiration time has the same value as the expiration time described in the cookie's metadata. It will be used to check if the user has been authenticated within a certain period. The last bit in the data field is a hash that will be used to secure the cookie's data integrity. HMAC is a keyed-hash authentication code given a message and a key "where it is computationally cheap to compute HMAC(message, key) but given HMAC(message, key), it is computationally infeasible to compute the message and the key".

The secure cookie protocol suggests the following encryption key to encrypt the user identification and expiration time: HMAC(user identification string|expiration time, secret key) where secret key is only known to the server. This key has the following three advantages:

  1. The encryption key is unique for each different cookie because the combination of the expiration time and the user identification string are different for each cookie.
  2. The encryption key cannot be deciphered because the secret key only known to the server.
  3. No server-side database storage is required to validate the cookie.

Using a variable key will also prevent volume attacks where a potential attacker could discover the server's secret key by analyzing a large collection of intercepted HMACs.

Generating the cookie

PHP's hash_hmac function is used to generate a keyed hash value using the Hash-based Message Authentication Code (HMAC) method. This will calculate a message authentication code (MAC) involving a cryptographic hash function in combination with a secret key. The message authentication code is used to verify the authenticity and integrity of a message. We use MD5 a cryptographic hash function in this example, but other (and safer) algorithms like SHA-2 can be used as well.

private function generateCookie( $id, $expiration ) {

	$key = hash_hmac( 'md5', $id . $expiration, SECRET_KEY );
	$hash = hash_hmac( 'md5', $id . $expiration, $key );

	$cookie = $id . '|' . $expiration . '|' . $hash;

	return $cookie;

}

First, the hash will be computed that will serve as the key to hash the cookie's data field. The secret key is used as key for the MAC. In this case, the key is a predefined constant (e.g. in the application's configuration settings). Next, the calculated hash is used as a key to create the message digest of the cookie's data field. Finally, the user identification string, expiration time and digest are concatenated and returned.

Verifying the cookie

The following steps will need to be completed to verify a cookie:

  1. Check if an authentication cookie is set.
  2. Compare the cookie's expiration time to the server's current time. If the cookie has expired, return false.
  3. Compute the encryption key as follows: key = HMAC( user identification string|expiration time, secret key ).
  4. Compute the hash using the user identification and expiration with the generated key.
  5. Compare the computed hash to the hash in the cookie. If they don't match, the data's integrity has been compromised: return false.
public static function verifyCookie() {

	if ( empty($_COOKIE[COOKIE_AUTH]) )
		return false;

	list( $id, $expiration, $hmac ) = explode( '|', $_COOKIE[COOKIE_AUTH] );

	$expired = $expiration;

	if ( $expired < time() )
		return false;

	$key = hash_hmac( 'md5', $id . $expiration, SECRET_KEY );
	$hash = hash_hmac( 'md5', $id . $expiration, $key );

	if ( $hmac != $hash )
		return false;

	return true;

}

Completing the implementation

These two functions are the basic building blocks of the authentication procedure. To complete the implementation, we will have to integrate the logic to use to cookies as a medium to authenticate the client to the server. A typical session between the client and the server consists of two phases. The first phase is called the login phase and the second phase is called the subsequent request phase.

In the login phase, the client and the server mutually authenticate each other. On one hand, the client authenticates the server using the server's PKI certificate after a SSL connection is established with the server. On the other hand, the server authenticates the client using the client's e-mail address (or any other unique identification string) and matching password, and sends a secure cookie to the client.

The code to achieve this is displayed below. It does the following things:

  1. Check if an entry in the database exists with a given e-mail address.
  2. If the user exists, check if the password in the database matches the password entered in e.g. a log in form. The Portable PHP password hashing framework is used here to create and match password hashes.
  3. If the password is correct we call the setCookie() function which will determine the expiration date and call the generateCookie() function that we discussed earlier. Finally the cookie is added to the HTTP header using PHP's setcookie() function.
private function authenticate( $email, $password, $remember ) {

	$sql = "SELECT * FROM `user_table` WHERE `email` = '%s'";

	$db = Database::getInstance();
	$result = $db->getRecords( sprintf ( $sql, $db->makeSafe( $email ) ) );

	if ( $db->getAffectedRows() == 1 ) {

		$user = $result[0];

	} else {
			
		throw new AuthException( "This e-mail address was not found in the database." );

	}

	require_once( "PasswordHash.class.php" );

	$hasher = new PasswordHash( 8, TRUE );

	if ( !$hasher->CheckPassword( $password, $user["password"] ) ) {
			
		throw new AuthException( "Invalid password." );
			
	}

	$this->setCookie( $user["id"], $remember );

}

private function setCookie( $id, $remember = false ) {

	if ( $remember ) {

		$expiration = time() + 1209600; // 14 days

	} else {

		$expiration = time() + 172800; // 48 hours

	}

	$cookie = $this->generateCookie( $id, $expiration );

	if ( !setcookie( COOKIE_AUTH, $cookie, $expiration, COOKIE_PATH, COOKIE_DOMAIN, false, true ) ) {
		
		throw new AuthException( "Could not set cookie." );
		
	}

}

In the subsequent request phase we will simply examine if the client sends a valid cookie to the server with each HTTP request using the verifyCookie method. If the cookie is verified, we can for example create a User object using the user identification string in the cookie's data field.

if ( Authenticate::verifyCookie() ) {
	
	require_once( CLASS_PATH . "User.class.php" );
	$user = new User( Authenticate::getUserId() );

}

A final note on replay attacks

We have ignored the problem of replay attacks so far in this example. A replay attack occurs when an attacker sends a stolen cookie to the server, effectively authenticating the attacker as the client that the cookie was originally issued to.

To counter a replay attack, the secure cookie protocol suggests to add the SSL session key to the keyed-hash message authentication code used to generate a message digest of the cookie. Adding this information makes the cookie session specific. Even if an attacker steals a cookie, he cannot successfully replay it since the session key is known only to a legitimate client and the server that creates the cookie. The disadvantage of adding the session key is that the cookie will become invalid if the client and server renegotiate about their session (creating a new session key).

The SSL session ID can be accessed through the predefined variable $_SERVER["SSL_SESSION_ID"].

References

Liu, A. X., Kovacs, J. M., Huang, C., & Gouda, M. G. (2005). A secure cookie protocol. Proceedings of the 14th IEEE International Conference on Computer Communications and Networks (pp. 333-338). San Diego, CA. [PDF]

Example of the authentication class