Tag: Kohana

Cache Control With Kohana 3

January 11, 2012 » Geek

I recently did some work with cache control in Kohana and found the documentation a little thin out there, so I thought I would share what I learned.

Kohana has nice built in functionality for ETag validations, so you don’t really need to roll your own cache headers.

If you need to brush up on web caching in general, I would recommend a quick read of Caching Tutorial for Web Authors and Webmasters, an excellent and concise reference.

Setup

For the purposes of this example, I’m creating a small controller which will use a short string as it’s response body. The implementation here is trivial.

1
2
3
4
5
6
7
8
9
<?php defined('SYSPATH') or die('No direct script access.');
 
  class Controller_Example extends Controller {
 
    public function action_index () {
      $this->response->body( 'Hello, world!' );
    }
 
  } // Controller_Example

Let’s look at the headers that are returned by default. Your headers may vary, so adjust accordingly.

If you look at the response headers from this request, you will note that there no Cache-Control, Last-Modified or ETag headers are returned. That gives us a blank slate to work with.

ETag

An ETag is a unique identifier string describing your content for cache validation. ETags have an advantage over Last-Modified headers in that there is no need to worry about clock synchronization. The server can determine how to generate ETags in any manner it desires.

The Response object in Kohana provides two methods useful for ETag based caching. The first is Response::generate_etag(), the second is Response::check_cache().

Response::generate_etag()

This method uses the sha1 hash to create a unique ETag based on the content of the rendered response. Because it renders and hashes the response before returning a result, there is a memory and CPU time hit, which increases with the size of your response.

Response::check_cache()

This method is the one we will use directly, as it compares the ETag of the response to request headers and takes the appropriate action.

It’s signature is ($etag = NULL, Request $request = NULL). This is a bit odd, because although both are NULL by default, and the Request parameter is second, it is not optional while $etag is.

If you provide NULL for $etag the method will use Response::generate_etag() to get a valid ETag. As mentioned above, this is not always the optimal choice, so if you have a unique identifier that you can provide, you should.

Since this is a simplistic example, I will let Response::generate_etag() create my ETag value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php defined('SYSPATH') or die('No direct script access.');
 
  class Controller_Example extends Controller {
 
    public function after () {
      parent::after();
      $this->response->check_cache( null, $this->request );
    }
 
    public function action_index () {
      $this->response->body( 'Hello, world!' );
    }
 
  } // Controller_Example

Let’s see the response headers for this version. We now have an ETag header, at the very bottom.

If we refresh the page again, we see that the browser sends an “If-None-Match” request header, which Response::check_cache() compares to the ETag. Finding that they match, the method returns a 304 response and immediately exits the script, causing the browser to use the cached version and saving the time it would take to send those bytes.

To demonstate how the ETag is generated let’s modify our response body so that it returns new content for every request (well, every second at least).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php defined('SYSPATH') or die('No direct script access.');
 
  class Controller_Example extends Controller {
 
    public function after () {
      parent::after();
      $this->response->check_cache( null, $this->request );
    }
 
    public function action_index () {
      $this->response->body( 'Hello, world at ' . date( DATE_RSS ) . '!' );
    }
 
  } // Controller_Example

After refreshing we get a new body, and a new ETag, breaking the cache and re-sending the entire page.

Remember, if you implement this, you should try to use an alternate ETag value if you can.

Cache Control

ETags aren’t useful without a Cache-Control header, but you can set that yourself with Response::headers(), just be aware that Response::check_cache() will append must-revalidate to your header value, so don’t add that part yourself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php defined('SYSPATH') or die('No direct script access.');
 
  class Controller_Example extends Controller {
 
    public function after () {
      parent::after();
      $this->response->headers( 'cache-control', 'private' );
      $this->response->check_cache( null, $this->request );
    }
 
    public function action_index () {
      $this->response->body( 'Hello, world at ' . date( DATE_RSS ) . '!' );
    }
 
  } // Controller_Example

Hopefully that clears up how to use the built in browser cache handling in Kohana 3, please leave your own tips or experiences in the comments!

Replacing Kohana 3 Auth module hashing

November 9, 2011 » Geek

The password hashing in the Auth module provided with Kohana 3.1 is not very good. By default it is a simple sha256 hmac with a global salt.

modules/auth/classes/kohana/auth.php

156
157
158
159
160
161
162
public function hash($str)
{
  if ( ! $this->_config['hash_key'])
    throw new Kohana_Exception('A valid hash key must be set in your auth config.');
 
  return hash_hmac($this->_config['hash_method'], $str, $this->_config['hash_key']);
}

This isn’t strong. If you loose the hashes and the salt it’s just a matter of winding up a GPU.

So how can we fix this? Well, thanks to Kohana’s structure we can easily override the Auth class and tweak it. However, due to Auth’s structure, we can’t drop the global salt. The hash function has to stand alone, so no passing in salts from the database.

That leaves us with key stretching.

Now, I don’t want to deal with a custom key stretching implementation, I’m not a cryptographer. So, let’s find an existing algorithm.

One that pops to mind is PBKDF2. This is a pretty simple algorithm, so it was easy to find and spot check a PHP implementation

We just take some test vectors from RFC 3962 and run them against the code we found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
  require_once( 'pbkdf2.php' );
 
  header( 'Content-Type: text/plain' );
 
  $tests = array(
    array(
      'rounds' => 1,
      'bits' => 128,
      'expected' => "cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15"
    ),
    array(
      'rounds' => 1,
      'bits' => 256,
      'expected' => "cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15 0a d1 f7 a0 4b b9 f3 a3 33 ec c0 e2 e1 f7 08 37"
    ),
    array(
      'rounds' => 2,
      'bits' => 128,
      'expected' => "01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d"
    ),
    array(
      'rounds' => 2,
      'bits' => 256,
      'expected' => "01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d a0 53 78 b9 32 44 ec 8f 48 a9 9e 61 ad 79 9d 86"
    ),
    array(
      'rounds' => 1200,
      'bits' => 128,
      'expected' => "5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b"
    ),
    array(
      'rounds' => 1200,
      'bits' => 256,
      'expected' => "5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b a7 e5 2d db c5 e5 14 2f 70 8a 31 e2 e6 2b 1e 13"
    ),
  );
 
  foreach( $tests as $test ) {
    print $test['rounds'] . ' rounds at ' . $test['bits'] . ' bits ' . "\n";
    $start = microtime( TRUE );
    $result = trim( preg_replace( '/(..)/', '\1 ', bin2hex( pbkdf2( 'password', 'ATHENA.MIT.EDUraeburn', $test['rounds'], $test['bits']/8, 'sha1' ) ) ) );
    $diff = microtime( TRUE ) - $start;
    print 'Expected: ' . $test['expected'] . "\n";
    print '     Got: ' . $result . "\n";
    if( $result == $test['expected'] ) {
      print "MATCH\n";
    }
    else {
      print "NO MATCH\n";
    }
    print 'Took ' . number_format( $diff, 10 ) . "\n\n";
  }

Run it, and everything checks out:

1 rounds at 128 bits
Expected: cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15
     Got: cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15
MATCH
Took 0.0000329018
 
1 rounds at 256 bits
Expected: cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15 0a d1 f7 a0 4b b9 f3 a3 33 ec c0 e2 e1 f7 08 37
     Got: cd ed b5 28 1b b2 f8 01 56 5a 11 22 b2 56 35 15 0a d1 f7 a0 4b b9 f3 a3 33 ec c0 e2 e1 f7 08 37
MATCH
Took 0.0000190735
 
2 rounds at 128 bits
Expected: 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d
     Got: 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d
MATCH
Took 0.0000147820
 
2 rounds at 256 bits
Expected: 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d a0 53 78 b9 32 44 ec 8f 48 a9 9e 61 ad 79 9d 86
     Got: 01 db ee 7f 4a 9e 24 3e 98 8b 62 c7 3c da 93 5d a0 53 78 b9 32 44 ec 8f 48 a9 9e 61 ad 79 9d 86
MATCH
Took 0.0000200272
 
1200 rounds at 128 bits
Expected: 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b
     Got: 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b
MATCH
Took 0.0019500256
 
1200 rounds at 256 bits
Expected: 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b a7 e5 2d db c5 e5 14 2f 70 8a 31 e2 e6 2b 1e 13
     Got: 5c 08 eb 61 fd f7 1e 4e 4e c3 cf 6b a1 f5 51 2b a7 e5 2d db c5 e5 14 2f 70 8a 31 e2 e6 2b 1e 13
MATCH
Took 0.0144000053

So now all that’s left is to drop it in, which is pretty simple. One thing to note is that I wanted this to stay compatible with the default auth config file, so I just extended that a little bit.

application/classes/auth.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
 
  abstract class Auth extends Kohana_Auth {
 
 
    public function hash ( $str ) {
      if ( ! $this->_config['hash_key'] )
        throw new Kohana_Exception( 'A valid hash key must be set in your auth config.' );
 
      if ( 'pbkdf2' == $this->_config['hash_method'] ) {
        return base64_encode( self::pbkdf2(
          $str,
          $this->_config['hash_key'],
          Arr::get( $this->_config['pbkdf2'], 'rounds', 1000 ),
          Arr::get( $this->_config['pbkdf2'], 'length', 45 ),
          Arr::get( $this->_config['pbkdf2'], 'method', 'sha256' )
        ) );
      }
      else {
        return parent::hash( $str );
      }
    }
 
    /** PBKDF2 Implementation (described in RFC 2898)
     *
     *  @param string p password
     *  @param string s salt
     *  @param int c iteration count (use 1000 or higher)
     *  @param int kl derived key length
     *  @param string a hash algorithm
     *
     *  @return string derived key
     *
     *  @url http://www.itnewb.com/tutorial/Encrypting-Passwords-with-PHP-for-Storage-Using-the-RSA-PBKDF2-StandardL
    */
    public static function pbkdf2 ( $p, $s, $c, $kl, $a = 'sha256' ) {
 
        $hl = strlen(hash($a, null, true)); # Hash length
        $kb = ceil($kl / $hl);              # Key blocks to compute
        $dk = '';                           # Derived key

        # Create key
        for ( $block = 1; $block <= $kb; $block ++ ) {
 
            # Initial hash for this block
            $ib = $b = hash_hmac($a, $s . pack('N', $block), $p, true);
 
            # Perform block iterations
            for ( $i = 1; $i < $c; $i ++ )
 
                # XOR each iterate
                $ib ^= ($b = hash_hmac($a, $b, $p, true));
 
            $dk .= $ib; # Append iterated block
        }
 
        # Return derived key of correct length
        return substr($dk, 0, $kl);
    }
 
  }

application/config/auth.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php defined('SYSPATH') or die('No direct access allowed.');
 
  return array(
    'driver'       => 'orm',
    'hash_method'  => 'pbkdf2',
    'hash_key'     => 'zomg',
    'lifetime'     => 1209600,
    'session_key'  => 'auth_user',
    'pbkdf2'       => array(
      'method'  => 'sha256',
      'rounds'  => 1000,
      'length'  => 45,
    )
  );

One item to note is that I am packing these with base64_encode. This is to fit into the default field type for the ORM driver. That is also why my length is stunted to 45. If you really want to go all out, alter your table to use a TINYBLOB, up the length to 256 bit and up the rounds.

So that is how I replace weak hashing in K3 with something a bit better.

How do you do it?

Kohana 3 OAuth & Twitter Demo Code

October 6, 2011 » Geek

The Internet seems a bit sparse when looking for good demo code for using the Kohana 3 OAuth module with Twitter.

I think the main issue is that the OAuth module isn’t very well documented, and doesn’t do API requests. For that you need an API implementation, like shadowhand/apis.

Anyway, here is a gist I put together with an example controller for Twitter OAuth:

https://gist.github.com/1267793

Clean Auth module usage in Kohana

February 24, 2010 » Geek

I’ve been learning the Kohana framework for a project at work, and I have to say I really like it. It has a lot of the things I liked about rails, and it stays out of my way, unlike CakePHP.

I thought I’d highlight my authentication solution that uses the built in Auth module and a base controller that I call Site_Controller. Keep in mind that all of my controllers derive from this one.

So, what’s it boil down to? Essentially you set up Auth and my base controller, then in your children controllers you can set $access_control to an array of methods you want protected. It works with key == method and value == access level. For values you can have “*” which means anyone logged in can use the method, or a string providing a specific role. Take a look at the controller then I’ll show you an example usage.

application/views/site.php

<?php
 
  class Site_Controller extends Template_Controller {
 
    public $template = 'layout';
 
    protected $access_control = array();
    protected $access_denied = "/user/login";
 
    //public $auto_render = false;
 
    function __construct () {
      parent::__construct();
      $this->session = Session::instance();
 
      // Check permissions
      if( array_key_exists( router::$method, $this->access_control ) ) {
        if( '*' == $this->access_control[router::$method] ) {
          if( ! Auth::instance()->logged_in() )
            url::redirect( $this->access_denied );
        }
        else if( is_array( $this->access_control[router::$method] ) ) {
          $can_proceed = false;
          foreach( $this->access_control[router::$method] as $role )
            if( Auth::instance()->logged_in( $role ) )
              $can_proceed = true;
 
          if( ! $can_proceed )
            url::redirect( $this->access_denied );
        }
        else {
          if( ! Auth::instance()->logged_in( $this->access_control[router::$method] ) )
            url::redirect( $this->access_denied );
        }
      }
    }
 
    public function __call( $method, $arguments ) {
      $this->template->title = "404";
      $this->template->content = new View( 'errors/404');
    }
  }

Here’s an example controller. In this case anyone can access login, anyone logged in can access index and only logged in admins can access adminsonly.

application/controllers/user.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
 
  class User_Controller extends Site_Controller {
 
    protected $access_control = array(
        "index" => "*",
        "adminsonly" => "admin"
      );
 
    function  index () {
      $this->template->content = "index";
    }
 
    function login () {
      $this->template->content = "login";
    }
 
    function adminsonly () {
      $this->template->content = "admins only";
    }
  }

I haven’t done a ton of testing and it’s not the most robust solution, but I like it and it was easy to write.