IAM

ARTICLE

Kohana Authentication using Red

Red is a Kohana module for user authentication based on the ORM module. This article gives an introduction to the module and explains some of the used security concepts.

Update. The usage of the Kohana Red module is also demonstrated in the demo application presented here.

Introduction

Most of today's web applications are based on a user authentication system. As default Kohana provides the auth module. Throughout my work with Kohana I developed a custom authentication module named Red. Red is based on the default auth module in many ways except some additional features to strengthen security. Before introducing the basic usage of the module let's discuss some of its features. You may also right jump to the SQL scheme, Configuration or Usage of the module. Red on GitHub

Login Delay

For resisting brute-force attacks the length of the password is crucial. The problem: most users prefer short and simple passwords. However we can slow down the login process itself such that brute-force attacks are slowed down dramatically. I will refer to this process as login delay. Manually setting a login delay is a simple but powerful method to hinder attackers from brute forcing your authentication system. The implementation is really straightforward: All logins are registered in the database including the used user agent, the time of the login and the email used for the login. With the next login all old logins from the same user agent are checked. When trying to login within the delay the login fails. So here is the table definition for saving the logins:
-- -----------------------------------------------------
-- Table `user_logins`
-- -----------------------------------------------------
CREATE  TABLE `user_logins` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `ip` VARCHAR(65) NOT NULL ,
  `agent` VARCHAR(65) NOT NULL ,
  `login` VARCHAR(255) NOT NULL ,
  `time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ,
  `user_id` INT(11) DEFAULT NULL ,
  PRIMARY KEY (`id`))
DEFAULT CHARACTER SET = utf8;
In addition to the user agent and ip the user itself will be saved after successfully logging in. So the application can check for the last logins of any user if required. Have a look at the code within the login() method:
/**
 * First check login delay.
 * Note: The login delay is not based on the email the user tried to login with but mainly on ip and user agent.
 */
$login = ORM::factory('user_login')
  ->where('ip', '=', hash_hmac($this->_config['login']['method'], Request::$client_ip, $this->_config['login']['key']))
  ->and_where('agent', '=', hash_hmac($this->_config['login']['method'], Request::$user_agent, $this->_config['login']['key']))
  ->and_where('time', '>', date('Y-m-d H:i:s', time() - $this->_config['login']['delay']))
  ->find();

if ($login->loaded()) {
  return FALSE;
}

/**
 * Save the current login attempt.
 */
$login = ORM::factory('user_login')
  ->values(array(
    'ip' => hash_hmac($this->_config['login']['method'], Request::$client_ip, $this->_config['login']['key']),
    'agent' => hash_hmac($this->_config['login']['method'], Request::$user_agent, $this->_config['login']['key']),
    'login' => $email,
  ))->create();
Another possible solution for slowing down the login process is using slow hash algorithms, or simply iterating the fast ones many times, this is also recommended for storing passwords savely.

Storing Passwords

The next question to think about is how to store passwords savely. By now it is common knowledge that MD5 and SHA-1 are not considered secure anymore. Instead an algorithm from the SHA-2 family should be used. But beneath using an appropriate hash algorithm there are some more things to consider:
  • Iterations: Add multiple iterations of the used hash algorithm. This concept is also known as hash based key stretching.
  • Salts: Append or prepend a random string to the password in each iteration step. The module will add an application wide salt but is also capable of adding different salts for different users.
/**
 * For hash strengthening iterations are done.
 * Using do-while so the password is at least hashed once.
 * The application and user salt is applied in each iteration.
 */
$i = 0;
do {
  $password = hash_hmac($config['password']['method'], $config['password']['salt'] . $password . (isset($user->salt) ? $user->salt : ''), $config['password']['key']);
  $i++;
} while($i < (int)$config['password']['iterations']);

"Remember Me" Functionality

The "remember me" feature uses cookies and tokens saved in the database to remember the user, so he is automatically logged in on his next visit when using the same IP address and user agent.
if ($remember !== FALSE) {
  $token = ORM::factory('user_token')->values(array(
    'user_id' => $user->id,
    'expires' => time() + $this->_config['token']['lifetime'],
    'user_agent' => hash_hmac($this->_config['token']['method'], Request::$user_agent, $this->_config['token']['key']),
  ));
  $token->create();
}

Cookie::set($this->_config['token']['cookie_key'], $token->token, $this->_config['token']['lifetime']);
A token consists of the corresponding user id, an expiration time, the user agent and an unique token string:
-- -----------------------------------------------------
-- Table `user_tokens`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `user_tokens` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `token` VARCHAR(40) NOT NULL ,
  `user_id` INT(11) UNSIGNED NOT NULL ,
  `user_agent` VARCHAR(64) NOT NULL ,
  `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ,
  `expires` INT(11) UNSIGNED NOT NULL ,
  PRIMARY KEY (`id`) ,
  UNIQUE INDEX `uniq_token` (`token`) ,
  INDEX `fk_user_tokens_user_id` (`user_id` ASC) ,
  CONSTRAINT `fk_user_tokens_user_id`
    FOREIGN KEY (`user_id` )
    REFERENCES `users` (`id` )
    ON DELETE CASCADE
    ON UPDATE NO ACTION)
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci;
The unique token will be automatically generated on token creation:
do {
  $token = sha1(uniqid(Text::random('alnum', 32), TRUE));
} while (ORM::factory('user_token')->where('token', '=', $token)->count_all() > 0);

$this->token = $token;
The token string is saved within a cookie. When the user visits the application again logged_in() or login() automatically checks for the cookie and finds the corresponding token. If the token is not expired, the user will be logged in.

SQL Scheme

The SQL scheme is mainly self explanatory: Logins and tokens are stored separately as discussed above. User roles are not necessary for the authentication system itself, but concepts as user groups or roles are widely used for access control systems - for example Green an access control module based on Red. For storing additional user information the table itself could be extended by the needed fields or an additional table like user_information could be added storing all the additional information not needed for the authentication itself.
-- -----------------------------------------------------
-- Table `users`
-- -----------------------------------------------------
CREATE  TABLE `users` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `email` VARCHAR(255) NOT NULL ,
  `first_name` VARCHAR(255) NOT NULL ,
  `last_name` VARCHAR(255) NOT NULL ,
  `password` VARCHAR(65) NOT NULL ,
  `salt` VARCHAR(255) DEFAULT NULL ,
  -- Additional fields can be added ...
  PRIMARY KEY (`id`) ,
  UNIQUE INDEX `uniq_email` (`email` ASC) )
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `user_roles`
-- -----------------------------------------------------
CREATE  TABLE `user_groups` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `name` VARCHAR(32) NULL ,
  -- Additional fields can be added ...
  PRIMARY KEY (`id`) ,
  UNIQUE INDEX `uniq_name` (`name` ASC) );

-- -----------------------------------------------------
-- Table `users_user_roles`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `users_user_groups` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `user_id` INT(11) UNSIGNED NOT NULL ,
  `user_role_id` INT(11) UNSIGNED NOT NULL ,
  PRIMARY KEY (`id`) ,
  INDEX `fk_users_user_groups_user_id` (`user_id` ASC) ,
  INDEX `fk_users_user_groups_user_role_id` (`user_role_id` ASC) ,
  CONSTRAINT `fk_users_user_groups_user_id`
    FOREIGN KEY (`user_id` )
    REFERENCES `users` (`id` )
    ON DELETE CASCADE
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_users_user_groups_user_role_id`
    FOREIGN KEY (`user_role_id` )
    REFERENCES `user_roles` (`id` )
    ON DELETE CASCADE
    ON UPDATE NO ACTION)
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci;

-- -----------------------------------------------------
-- Table `user_logins`
-- -----------------------------------------------------
CREATE  TABLE `user_logins` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `ip` VARCHAR(65) NOT NULL ,
  `agent` VARCHAR(65) NOT NULL ,
  `login` VARCHAR(255) NOT NULL ,
  `created` INT(11) UNSIGNED NOT NULL ,
  `user_id` INT(11) DEFAULT NULL ,
  PRIMARY KEY (`id`))
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `user_tokens`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `pl_user_tokens` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT ,
  `token` VARCHAR(40) NOT NULL ,
  `user_id` INT(11) UNSIGNED NOT NULL ,
  `user_agent` VARCHAR(64) NOT NULL ,
  `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ,
  `expires` INT(11) UNSIGNED NOT NULL ,
  PRIMARY KEY (`id`) ,
  UNIQUE INDEX `uniq_token` (`token`) ,
  INDEX `fk_user_tokens_user_id` (`user_id` ASC) ,
  CONSTRAINT `fk_user_tokens_user_id`
    FOREIGN KEY (`user_id` )
    REFERENCES `pl_users` (`id` )
    ON DELETE CASCADE
    ON UPDATE NO ACTION)
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_general_ci;

Configuration

There are a lot of configuration options provided. I want to discuss only some of them. For the others have a look at config.php below.
  • keys: All keys should be chosen as random strings.
  • session.type: Red supports all kinds of session drivers, but I strongly recommend using the database driver. See here for the needed configuration and an SQL scheme.
  • token.gc: Expired tokens remain in the database until fetched by the garbage collection which will be run with a chance of 1/token.gc every time a token is accessed.
  • methods: For hashing the password a hash algorithm of the SHA-2 family should be used, for hashing IP addresses or user agents any hashing algorithm will do the job.
return array(
  /**
   * Password hashing configurations.
   * 
   * The method defines used hash algorithms. 
   * Do not use md5 or sha1, these are not considered secure any more.
   * Use one of the sha2 family instead.
   * The key for the hmac.
   * The nubmer of iterations. Thus to a good speed of most hash algorithms ~1000
   * iterations are not any problem and considered secure.
   */
  'password'  => array(
    'method' => 'sha256',
    'key' => '',
    'iterations' => 10000,
    /**
     * Salt configuration.
     * 
     * The application wide salt (configure here) is added to each password.
     * It should have at least 20 random characters.
     * A user salt can be added manually in the 'salt' column of
     * the suer table and should contain around 20 random characters.
     */
    'salt' => '', 
  ),

  /**
   * Session configuration: Session type to use and session key used.
   */
  'session' => array(
    'type' => 'database',
    'key'  => 'red_user',
  ),

  /**
   * Options concerning the logins.
   */
  'login' => array(
    'method' => 'sha256', // Method for hashing IPs and user agents.
    'key' => '',
    'delay' => 10, // Delay between logins in seconds.
    'store' => 604800, // Store logins for x seconds.
  ),

  /**
   * Tokens are used to remember a user looged in.
   * This is done using cookies. A token is saved in the database and a reference
   * to this token is set in a cookie with the key cookie_key.
   */
  'token' => array(
    'method' => 'sha256',
    'key' => '',
    'lifetime' => 1209600, // Lifetime of the cookie and the token.
    'cookie_key' => '', // The key for the cookie to use.
    'gc' => 100, // Garbage collector is run in 1/100 of times.
  ),
);

Usage

Using Red will feel familiar to using the default auth module. Based on a singleton Red provides the following methods:
login Try to login a user using the given email and password (for example provided by the login form). If successful the method will return the logged in user, FALSE otherwise.
$success = Red::instance()->login($email, $password, $remember);
logout Logout the current user. Optional the current session can be destroyed.
Red::instance()->logout($destroy);
logged_in Check if a user is currently logged in. If not it tries to automatically login a user based on the "remember me" functionality. Returns TRUE or FALSE.
$logged_in = Red::instance()->logged_in();
get_user Get the current user or FALSE if no user is logged in.
$user = Red::instance()->get_user();
hash Hash the given password for the given user. The user is needed to add the user salts while hashing.
$hash= Red::hash($password, $user);

Example

As code example consider the following login form and the corresponding controller action. The login form is based on twitter bootstrap.
<?php echo Form::open(Route::get('authentication')->uri(), array('class' => 'form-horizontal')); ?>
  <div class="control-group">
    <label class="control-label" for="email"><?php echo __('Email'); ?></label>
    <div class="controls">
      <input type="text" name="email" placeholder="Email">
    </div>
  </div>
  <div class="control-group">
    <label class="control-label" for="password"><?php echo __('Password'); ?></label>
    <div class="controls">
      <input type="password" name="password" placeholder="Password">
    </div>
  </div>
  <div class="control-group">
    <div class="controls">
      <label class="checkbox">
        <input type="checkbox" name="remember"> <?php echo __('Remember me'); ?>
      </label>
      <button type="submit" class="btn"><?php echo __('Login'); ?></button>
    </div>
  </div>
<?php echo Form::close(); ?>
The controller action first checks whether a user is already logged in. This will cause the user to get logged in if he used the "remember me" functionality. Otherwise the user may login using the form.
public function action_login() {
  // Check if the user is already logged in.
  if (Red::instance()->logged_in()) {
    $this->redirect(Route::get('dashboard')->uri());
  }

  if (Request::POST === $this->request->method()) {
    // Remember the login?
    $remember = Arr::get($this->request->post(), 'remember', FALSE);

    if (Red::instance()->login($this->request->post('email'), $this->request->post('password'), $remember)) {
      // Login successful, redirect ...
      $this->redirect(Route::get('dashboard')->uri());
    }
    else {
      // Login failed ...
    }
  }
}

References

What is your opinion on this article? Let me know your thoughts on Twitter @davidstutz92 or LinkedIn in/davidstutz92.