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.
To go through this recipe, we need some JSON-based REST services implemented with authentication in place, so follow the previous recipe.
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`);
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')); }
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 } ?>
app/config/bootstrap.php
file and add the following at the end:Configure::write('API', array( 'maximum' => 6, 'time' => '2 minutes' ));
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'), } }
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; } } ?>
app/vendors/shells/consume.php
test script, remove the $user
and $password
properties, and then add the following property:protected $token;
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();
}
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:
../cake/console/cake consume http://localhost token
..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
.
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.
18.188.98.148