Tools Plugin – Part1: Token

In this first part of the presentation of my tools plugin I will show you how you can easily manage (store, retrieve, validate) "code keys" alias "tokens".
They are useful in the registration process of users, or if you want to send some double opt-in confirmation emails, etc.

For changing a users password, for example, key goals are:

  • Not sending the plain password, but a token to allow changing the password
  • Invalidating the token once it has been used (to avoid the token being abused)
  • Tokens should also expire

More on that below in the security notes.

Token model

The main methods of the model are

  • newKey($type, $key = null, $uid = null, $content = null)
  • useKey($type, $key, $uid = null)
  • spendKey($id = null)

The first method I will explain a little bit more.

public function newKey($type, $key = null, $uid = null, $content = null) {} {}

If you submit an uid (user_id) it can increase security by validation only if the pair "uid + key" are both valid.
As 4th parameter, any string content can be stored. This comes in handy if you use it for changing email addresses. The new one will be stored in content until validation is complete and will then replace the old one.

Usage: Activate

This is a short example on how to use it to activate accounts or change email addresses.

In your register_account() method:

// get new code with key "activate" and specific user id bound to it
$this->Token = ClassRegistry::init('Tools.Token');
$cCode = $this->Token->newKey('activate', null, $uid);

This code can be used to send an activation email with a link to click on:

// in your email
echo $this->Html->url(array('action' => 'activate_account', $cCode));

The activate_account($keyToCheck = null) method:

...
$this->Token = ClassRegistry::init('Tools.Token');
$token = $this->Token->useKey('activate', $keyToCheck);

if (!empty($token) && $token['Token']['used'] > 0) {
    // warning flash message: already activated
} elseif (!empty($token)) {
    // continue with activation, set activate flag of this user from 0 to 1 for example
    $this->User->id = $key['Token']['user_id'];
    $this->User->saveField('active', 1);
    // success flash message, auto-login and redirect to his profile page etc
} else {
    // error flash message: invalid key
}
...

Tip: For email addresses, just store the email address in the $content param as suggested above.

Usage: Allow users to change passwords

For this we want the user to be authenticated if a valid token is provided via URL and to be presented with a form to change the password (including a "repeat" field).
We first check on the token and on success store a temporary "Auth" key in the session.

The change_password_init($keyToCheck = null) method:

$this->Token= ClassRegistry::init('Tools.Token');
$token = $this->Token->useKey('activate', $keyToCheck);

if (!empty($token) && $token['Token']['used'] > 0) {
    // warning flash message: already used
} elseif (!empty($token)) {
    // continue, write to session and redirect
    $this->Session->write('Auth.Tmp.id', $uid);
    $this->redirect(array('action' => 'change_password'));
} else {
    // error flash message: invalid key
}

Then you redirect to the "public" form where you want the password to be changed.

In this change_password() action you need to check on the existence of this temporary Auth key:

$uid = $this->Session->read('Auth.Tmp.id');
if (!$uid) {
    // error message - not allowed
    $this->redirect(array('action' => 'forgot_my_password'));
}
// change password code just as any other "edit" action

Don’t forget to remove this tmp key after a successfull change of password:

$this->Session->delete('Auth.Tmp.id');

Also don’t forget the $this->Auth->allow('change_password') for this action. Since the tmp session key is required it is only accessible after a valid token has been provided.

For more on how to make an easy and DRY "change password form" see my behavior for it.

Tip: You can also unify the "send my a new password request link" and "change_password_init" actions, but that requires a little bit more logic. This example is only to show the basics. Feel free to both solidify and improve the methods yourself.

Security notes

By default, the tokens have a validity of one month. You can modify this value in your model.

Do not send plain passwords with your emails or print them out anywhere. That’s why you should send the expiring tokens (ideally, using the above code only valid one time).
More to the usage of passwords in Cake see my article on how to work with passwords.

And if you feel like you need more information on the implementation process, read this article at troyhunt.com. It describes in a very verbose way what to do and what better not to do.

Where can I find the code

I decided to remove the code here and link to the github rep (to always include the latest stage):

This is all 2.x already (see the 1.3 branch for earlier versions)

Note: you might want to set user_id back to int(10) unsigned if you don’t use UUIDs as keys.

Tip: It is easiest to just clone the Plugin in into /Plugin/Tools/ and load it using CakePlugin::load('Tools') in your bootstrap.

Tips

You can implement a task/cronjob to call garbageCollector() to automatically remove outdated and invalidated tokens from your table.

There is also a method stats() to retrieve statistics if required/useful to you.

CakePHP 3/4

Docs moved to /docs section in the plugin.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.