Implementing token-based authorization for API access

In the previous recipe, Adding authentication to REST services, we built a REST API using JSON for our PostsController actions. With it, clients that utilize our REST services use a user account to validate their requests.

Without neglecting the need to authorize all requests, several companies take a different approach when publishing their APIs: the use of API tokens. The advantage of using API tokens is that our user accounts are not exposed in client scripts, so the authorization information can't be used to log in to the site.

In this recipe we will take our authenticated REST service system and enable the use of tokens to use the exposed API. We will also add a usage limit, so client API usage is only allowed within a certain time and number of uses threshold.

Getting ready

To go through this recipe, we need some JSON-based REST services implemented with authentication in place, so follow the previous recipe.

How to do it...

  1. We start by adding some fields to our users table. Issue the following SQL statements:
    ALTER TABLE `users`users
    ADD COLUMN `token` CHAR(40) default NULL,
    ADD COLUMN `token_used` DATETIME default NULL,
    ADD COLUMN `token_uses` INT NOT NULL default 0,
    ADD UNIQUE KEY `token`(`token`);
    
  2. Edit your app/controllers/users_controller.php file and add the following method to the UsersController class:
    public function token() {
    $token = sha1(String::uuid());
    $this->User->id = $this->Auth->user('id'),
    if (!$this->User->saveField('token', $token)) {
    $token = null;
    $this->Session->setFlash('There was an error generating this token'),
    }
    $this->set(compact('token'));
    }
    
  3. Create its view in a file named token.ctp and place it in your app/views/users folder, with the following contents:
    <h1>API access token</h1>
    <?php if (!empty($token)) { ?>
    <p>Your new API access token is: <strong><?php echo $token; ?></strong></p>
    <?php } ?>
    
  4. Let us add the parameters that will define the API access limits. Edit your app/config/bootstrap.php file and add the following at the end:
    Configure::write('API', array(
    'maximum' => 6,
    'time' => '2 minutes'
    ));
    
  5. Edit your app/controllers/posts_controller.php file and change the _restLogin() method, replacing it with the following contents:
    public function _restLogin($credentials) {
    $model = $this->Auth->getModel();
    try {
    $id = $model->useToken($credentials['username']);
    if (empty($id)) {
    $this->redirect(null, 503);
    }
    } catch(Exception $e) {
    $id = null;
    }
    if (empty($id) || !$this->Auth->login(strval($id))) {
    $this->Security->blackhole($this, 'login'),
    }
    }
    
  6. Create the User model in a file named user.php and place it in your app/models folder, with the following contents:
    <?php
    class User extends AppModel {
    public function useToken($token) {
    $user = $this->find('first', array(
    'conditions' => array($this->alias.'.token' => $token),
    'recursive' => -1
    ));
    if (empty($user)) {
    throw new Exception('Token is not valid'),
    }
    $apiSettings = Configure::read('API'),
    $tokenUsed = !empty($user[$this->alias]['token_used']) ? $user[$this->alias]['token_used'] : null;
    $tokenUses = $user[$this->alias]['token_uses'];
    if (!empty($tokenUsed)) {
    $tokenTimeThreshold = strtotime('+' . $apiSettings['time'], strtotime($tokenUsed));
    }
    $now = time();
    if (!empty($tokenUsed) && $now <= $tokenTimeThreshold && $tokenUses >= $apiSettings['maximum']) {
    return false;
    }
    $id = $user[$this->alias][$this->primaryKey];
    if (!empty($tokenUsed) && $now <= $tokenTimeThreshold) {
    $this->id = $id;
    $this->saveField('token_uses', $tokenUses + 1);
    } else {
    $this->id = $id;
    $this->save(
    array('token_used'=>date('Y-m-d H:i:s'), 'token_uses'=>1),
    false,
    array('token_used', 'token_uses')
    );
    }
    return $id;
    }
    }
    ?>
    
  7. Edit your app/vendors/shells/consume.php test script, remove the $user and $password properties, and then add the following property:
    protected $token;
    
  8. While still editing the shell script, make the following changes to its main() method:
    public function main() {
    if (empty($this->args) || count($this->args) != 2) {
    $this->err('USAGE: cake consume <baseUrl> <token>'),
    $this->_stop();
    }
    list(self::$baseUrl, self::$token) = $this->args;
    $this->test();
    }
    
  9. Finally, make the following changes to the request() method:
    protected function request($url, $method='GET', $data=null) {
    if (!isset(self::$httpSocket)) {
    self::$httpSocket = new HttpSocket();
    } else {
    self::$httpSocket->reset();
    }
    $body = self::$httpSocket->request(array(
    'method' => $method,
    'uri' => self::$baseUrl . '/' . $url,
    'body' => $data,
    'auth' => array(
    'user' => self::$token,
    'pass' => ''
    )
    ));
    if ($body === false || self::$httpSocket->response['status']['code'] != 200) {
    $error = 'ERROR while performing '.$method.' to '.$url;
    if ($body !== false) {
    $error = '[' . self::$httpSocket->response['status']['code'] . '] ' . $error;
    }
    $this->err($error);
    $this->_stop();
    }
    return $body;
    }
    

If you now browse to http://localhost/users/token, you will be asked to Log in. Log in with the user account you created during the Getting Started section and you will then obtain an API token.

Let us now run the testing script with the following command. Change http://localhost to match your application's URL, and token to match the API token you just generated:

  • If you are on a GNU Linux / Mac / Unix system:
    ../cake/console/cake consume http://localhost token
    
  • If you are on Microsoft Windows:
    ..cakeconsolecake.bat consume http://localhost token
    

If we specified the right token, we will get the same successful output as shown in the recipe Building REST services with JSON.

If you run the script again within 2 minutes since the last run, you will get a 503 (Service Unavailable) HTTP status error, indicating that we are overusing our API token. We will have to wait two minutes to be able to successfully run the script again, because each run makes six requests to the API, and six is the maximum allowed requests within two minutes, as configured in app/config/bootstrap.php.

How it works...

We start by adding three fields to the users table:

  • token: The API access token, unique to each user. This is what a user will use to use our API services.
  • token_used: The last time the API usage counter (token_uses) was reset.
  • token_uses: The number of API uses since the date and time specified in token_used.

We then create an action called token in the UsersController class to allow users to get new API access tokens. This action will simply create a new token by hashing a UUID (Universally Unique Identifier), and saving it to the users table record.

We proceed to set our application configuration in bootstrap.php by defining the API access limits with two settings:

  • maximum: The maximum number of API requests allowed within a given time frame.
  • time: The time frame that is used to check for API overuse. Any string that can be used by the PHP function strtotime() is allowed.

We set time to 2 minutes, and maximum to 6 requests, which means that we will allow up to six API requests per user, every two minutes.

As we are no longer using real accounts to authenticate our API users, we changed the _restLogin() method in ProfilesController to only use the given username field value. This value is in fact a user's API token. The password field is therefore ignored, which allows our test client script to simply pass an empty value as the password.

We use the method useToken() of the User model to check the validity of the token. If the method throws an Exception, then the given token does not exist, so we end the request with a 401 status (Unauthorized) by calling the blackhole() method of the Security component. If the useToken() method returns false, then the token is being overused, so we send back a 503 (Service Unavailable) status. If we are given back a valid user ID, we convert this value to a string, and pass it to the login() method of the Auth component, which will log in a user with a given ID if the specified parameter is a string.

As we can see, the whole token usage logic relies on the User::useToken(). This method starts by looking for a user record with the given token. If none is found, it throws an Exception. If a valid token is being used, it checks to see if the token has been used. If so, we set the time limit since the first update of the token usage in the $tokenTimeThreshold local variable. If we are within this time frame, and if the number of token uses exceeds the configured setting, we return false.

If none of the above conditions are met, then the token use is valid, so we either increment the number of uses if $tokenTimeThreshold is within the current time frame, or reset it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.98.148