TokenBundle

A while ago, we needed a solution to secure some URLs for single use. As this problem seemed to be quite common, we thought of making a reusable solution, which resulted in a separate Bundle that we used in several projects, and updated it to best fit other encountered situations.

We believe this could be useful to other projects, as well, so we’ve decided to make it open-source and share it with the community.

This article introduces you with this TokenBundle, and describes a few situations in which it can be used, along with examples.

When to use? / Purpose

The goal of TokenBundle is to provide a means to validate/restrict actions taken by users.

The validation is achieved by using a hash, which could, for example, be embedded in a URL, be it in an email or a link generated on-the-spot inside a page.

Basic usage includes securing an action by enforcing the link to work only once.

Other use cases include:

  1. It could be used for securing an account activation link for a given user, valid only for 1 day – by setting the expiry time.
  2. Another use case could be allowing a user to vote, the change this vote only 3 times – by setting a max uses value of 4.
  3. A slightly more advanced situation could be securing a forgot password link, enforcing the change to be made from a given IP, in a time frame of 1 hour since the email was sent – however, this scenario requires using the additional data and implementing some custom rules validation conditions.

Why to use? / Strong points

This bundle provides a clean way for generating Tokens, and then validating, respectively consuming them. It exposes a service, innobyte_token, which abstracts all the work needed for common situations.

The only dependencies are Doctrine and Dependency Injection, so no third party library is needed.

Unit testing offers the needed confidence in terms of reliability for future development, containing over 20 tests and 80 assertions, with 100% code coverage for Service and Entity.

Another benefit is that the code is well commented and documented, and detailed usage examples are provided inside the README.md file.

The Database table structure is optimized in both storage (by choosing the right field types) and search performance (by using indexes and properly ordering columns, which is particularly useful for tables containing many rows, as such).

The approach for failures is throwing exceptions when something unexpected occurs.

The Doctrine Entity Manager to use is configurable (exposed in semantic configuration).

How to use? / Examples

The Service API is fairly simple and intuitive, exposing the following methods:

  • generate – used to generate the Token – the needed information includes:
    • the scope of the Token (the purpose for which it is used, such as account confirmation, password reset etc.)
    • the owner type (typically the entity name that the Token is generated for: user, project etc.)
    • the owner ID (the identifier for the owner entity)
    • optionally, the Token can be used multiple times, by setting the max uses parameter
    • optionally, an expiration time can be specified
    • also optional, additional data can be set, in order to build more advanced validation rules
      //generate token
      $token = $this->get('innobyte_token')->generate(
          'scope',
          'owner_type',
          123,                       // owner_id
          1,                         // number of uses - optional
          new \DateTime('+1 day'),   // expiry time - optional
          array('author' => 'admin') // additional data to check against - optional
      );
      
      // use hash (embed in a link/email etc.)
      $hash = $token->getHash();
  • get – used to retrieve the Token by the mandatory info provided at generation time
    $token = $this->get('innobyte_token')->get(
        '5c15e262c692dbaac75451dcb28282ab',
        'scope',
        'owner_type',
        123            // owner_id
    );
  • isValid – will verify if the Token is available to use, meaning: being active, with the usage number lower than the maximum usage number and expiry time not passed
    // if wrong link or disabled/overused token
    if (!$this->get('innobyte_token')->isValid($token)) {
        echo 'Handle invalid token here';
    }
  • consume – will find the Token by the given criteria (the mandatory info), and then increase its number of uses

    try {
        $this->get('innobyte_token')->consume(
            '19debf971fb937853d77fca8fd3bb775',
            'scope',
            'owner_type',
            123            // owner_id
        );
    } catch (\Innobyte\TokenBundle\Exception\TokenNotFoundException $e) {
        echo 'cannot find Token';
    } catch (\Innobyte\TokenBundle\Exception\TokenInactiveException $e) {
        echo 'handle explicit disabled token here';
    } catch (\Innobyte\TokenBundle\Exception\TokenConsumedException $e) {
        echo 'handle over-used token here';
    } catch (\Innobyte\TokenBundle\Exception\TokenExpiredException $e) {
        echo 'handle expired token here';
    }
    
    echo 'Token is valid. Token is valid. Perform logic here.';
  • consumeToken – same as consume, except it operates on a given managed Token instead of fetching it by the mandatory info provided when the Token was generated
    // or even more explicit
    if (!$token instanceof \Innobyte\TokenBundle\Entity\Token) {
        echo 'handle invalid token here';
    } else {
        // manually mark the usage
        try {
            $this->get('innobyte_token')->consumeToken($token);
        } catch (\LogicException $e) {
            echo 'cannot consumed Token because it is not managed';
        }
    
        echo 'Token is valid. Perform logic here.';
    }
  • invalidate – will find the Token by the given criteria, and then deactivate it
    try {
        $this->get('innobyte_token')->invalidate(
            '5c15e262c692dbaac75451dcb28282ab',
            'scope',
            'owner_type',
            123            // owner_id
        );
    } catch (\Innobyte\TokenBundle\Exception\TokenNotFoundException $e) {
        echo 'Handle Token not found here';
    }
  • invalidateToken – which operates on the managed Token directly, and deactivates it
    if (!$token instanceof \Innobyte\TokenBundle\Entity\Token) {
        echo 'Handle invalid token here';
    } else {
        try {
            $this->get('innobyte_token')->invalidateToken($token);
        } catch (\LogicException $e) {
            echo 'Cannot consume Token because it is not managed';
        }
    }

Note: managed means fetched through Doctrine, either directly from DB (less common), either through get method exposed by the service.

Advanced usage allows setting additional data for the Token (stored as an array on the entity), which can be used to implement custom validation rules for the Token (like setting an IP for which the Token is valid, or any other conditions that would have to be met at validation time, such as referrer URI, minimum purchase amount etc., or setting validation scenarios etc.).

// first, generate the Token
$token = $this->get('innobyte_token')->generate(
    'scope',
    'owner_type',
    123,                        // owner_id
    1,                          // number of uses - optional
    new \DateTime('+1 hour'),   // expiry time - optional
    array('ip' => $request->getClientIp()) // additional data to check against - optional
);

// use hash (embed in a link/email etc.)
$hash = $token->getHash();

// then, validate it
$token = $this->get('innobyte_token')->get(
    $hash,
    'scope',
    'owner_type',
    123            // owner_id
);

// if wrong link or disabled/overused token
if (!$this->get('innobyte_token')->isValid($token)) {
    echo 'Handle invalid token here';
}

// additional validation by IP
$additionalData = $token->getData();
if ($additionalData['ip'] != $request->getClientIp()) {
    echo 'Handle invalid token here';
}

How to install? / Setup

The installation is simple and pretty standard:

  1. The package must be added into composer.json, under the require section
    "innobyte/token-bundle": "~1.1"
  2. The bundle must be added into $bundles array inside AppKernel.php
    new Innobyte\TokenBundle\InnobyteTokenBundle(),
  3. As for any bundle that uses the Database connection, the bundle must be added into entity manager’s mapping, inside config.yml. Here, local is the name of the used entity manager.
    doctrine:
        ...
        orm:
            ...
            entity_managers:
                local:
                    ...
                    mappings:
                        ...
                        InnobyteTokenBundle: ~
  4. If the entity manager name is different than the default default, it must be specified inside parameters.yml. Here, local is the name of the used entity manager.
    innobyte_token:
        entity_manager: local
  5. The Database schema needs to be updated. Here, local is the name of the used entity manager.
    app/console doctrine:schema:update --em=local --dump-sql

    to see the resulting SQL

    app/console doctrine:schema:update --em=local --force

    to actually update it

This is all the setup needed for TokenBundle to work properly.

What do you think about TokenBundle? If you find other ways of using it, plese let us know. We’re curious!

Equally, if you have feature requests or bug reports, please let us know on GitHub: http://github.com/innobyte/token-bundle

Leave a Comment

Your email address will not be published.

Scroll to Top