RestFul Design for CakePHP 2.x and API Implementation using oauth2

elevysi · 04 July 2019 |

A couple of days ago I was introduced to a new challenge; getting a project of mine to be an API for another application that would act as a client. So my client would "GET" and "POST" from and to the API. Both the server and the client use CakePHP 2.x

So I went ahead and started to research about how I would go about it. I was not interested in authenticating the client's users on the Server but the client itself. I was concerned about two aspects: how would I get the Server to reply to the client requests?  and how would the Server be able to authenticate the client when it issues the request? The solution I found was to use REST (which is well documented in CakePHP) as a web service in combination with Oauth2 for the security part. 

First let me start with an introduction to REST in CakePHP; as explained on the documentation page here is a sample for a restful Controller:

<?php 
class RecipesController extends AppController {

    public $components = array('RequestHandler');

    public function index() {
        $recipes = $this->Recipe->find('all');
        $this->set(array(
            'recipes' => $recipes,
            '_serialize' => array('recipes')
        ));
    }

    public function view($id) {
        $recipe = $this->Recipe->findById($id);
        $this->set(array(
            'recipe' => $recipe,
            '_serialize' => array('recipe')
        ));
    }

    public function add() {
        $this->Recipe->create();
        if ($this->Recipe->save($this->request->data)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }

    public function edit($id) {
        $this->Recipe->id = $id;
        if ($this->Recipe->save($this->request->data)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }

    public function delete($id) {
        if ($this->Recipe->delete($id)) {
            $message = 'Deleted';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }
}

This was then my controller for the API (Server) side. However, my app needed authentication for all the controllers since I had defined it in the beforeFilter() method of the AppController, from which all the others inherit; hence this was never going to work unless I was logged in. The solution was to authorize all the methods of this controller in its beforeFilter() method, overriding the setting of the AppController. Here is the code used below:

public function beforeFilter() {
    	$this->Auth->allow();
}

This would ensure that no authentication is needed for this part of the Application. At the same time, it would leave all the information open to the outside world.

That is where oauth2 comes into the play. Remember that my challenge is to authenticate the client and not the users of this client. Oauth has support for these two but I will focus on the client one since it is what is needed here. I used CakePHP OAuth2 Server Plugin, a cakePHP plugin. I downloaded the release and put it in the Plugin Folder (app/Plugin/Oauth) and declared it in app/config/bootstrap.php as follows (all on the server side):

CakePlugin::load('OAuth'); // Loads Plugin

I also put the  oauth2-php library in the Vendor folder (server side) since the Plugin uses oauth-2 (downloaded to app/Vendor/oauth-2-php); but all this is also explained on the Plugin page.

Now the plugin was available in my app; the next step was to declare it in the Restful Controller under the controller's components as follows:

public $components = array('RequestHandler', 'OAuth.OAuth');

After adding this line, all of my restful controller methods were protected again. I could not access any of the controller's method without providing extra information; however, would I have needed to allow one or several methods, I would use it the same way Auth is used: $this->Oauth->allow() in the beforeFilter() method. 

As explained in the Plugin documentation, you need to create tables in the Server Application Database that will hold the access tokens, the clients IDS and so on. Once all this is set up, create a client as described in the documentation; remember to debug the result of Client->add() since it is the only way you can retrieve the client secret which will be hashed in the table storage. Note down the client ID and secret to later provide them to the Client application. To complete, it is required to implement a getToken method which I implemented in my RestFul Controller (will be called later):

public function token() {
    	$this->autoRender = false;
    	try {
    		$this->OAuth->grantAccessToken();
    	} catch (OAuth2ServerException $e) {
    		$e->sendHttpResponse();
    	}
    }

 

I edited my oauth2-php/lib/Oauth2Client.php file to be able to support Client credentials since it seemed that it was not supported under the downloaded release. As I kept getting an error, I looked into the lib code and I modified as follow by adding a method that was completely left out. The class implements an interface that has the added method. However when I looked into the class, the method was not implemented hence throwing an Exception. I then researched for what the method was for and I implemented the code myself; it retrieves a client ID and secret and forwards them to the verification method.

private function getAccessTokenFromPassword($username, $password) {
		if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) {
			return json_decode($this->makeRequest(
				$this->getVariable('access_token_uri'),
				'POST',
				array(
					'grant_type' => 'password',
					'client_id' => $this->getVariable('client_id'),
					'client_secret' => $this->getVariable('client_secret'),
					'username' => $username,
					'password' => $password
				)
			), TRUE);
		}
		return NULL;
	}

Not much but once you start getting your hands around this, this piece of code will be self-explanatory.

That was it for the server side; on the client side I use HttpSocket. Note that since it is needed to authenticate the client (through oauth2), I would first need to send the client ID and secret to the Server which would reply with an access token if the credentials were valid. Then I needed to provide this access token whenever I would make a request to the Server. There are several ways you can send the access token, most of which are explained in the oauth docs. It is a lot of reading but it is worthy it if you want to get at least a glimpse at how all this work. 

Below is a sample of a Client equest to the Server:

public function index() {
    
    	$recipes = array();
    	/**
    	 * Look for Recipes on the other server
    	*/
    	$link_parameters = array();
    	$session_access_token = NULL;
    	$access_token = $this->Session->read('access_token');
    	if($access_token){
    		$link_parameters['access_token'] = $access_token;
    		$session_access_token = $access_token;
    	}
    
    	$link = SERVER."recipes.json";
    
    	if($returnedData = $this->establishRestConnection($link, $link_parameters, 'get', $session_access_token)){
    		$recipes = $returnedData['recipes'];
    			
    	}
    	$this->set('recipes', $recipes);
    
    }
    
    public function establishRestConnection($link = NULL, $link_parameters = array(), $method = NULL, $access_token = NULL){
    
    	if($data = $this->User->restConnect($link, $link_parameters, $method, $access_token)){
    		if($data['GIVEN_ACCESS_TOKEN']){
    			$this->writeAccessToken($data['GIVEN_ACCESS_TOKEN']);
    			return $data;
    		}
    	}
    
    	return FALSE;
    }
    
    public function acquireAccessToken(){
    	$httpSocket = new HttpSocket();
    	$link = SERVER. "/recipes/token";
    	
    
    	$data = array(
    			'grant_type' => 'client_credentials',
    			'client_id' => 'NTRjZjcwxZWUxZxxxx',
    			'client_secret' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'
    	);
    
    
    	$response = $httpSocket->post($link, $data);
    
    	if($response->code == 200){
    		$data = json_decode($response->body, true);
    		return $data['access_token'];
    	}
    	return FALSE;
    }
    
    
    public function restConnect($link = NULL, $link_parameters = array(), $method = NULL, $access_token = NULL, $repeat = FALSE){
    
    	$httpSocket = new HttpSocket();
    	switch ($method){
    		case 'get':
    			$response = $httpSocket->get($link, $link_parameters);
    			break;
    		case 'put':
    			$token_string = '?access_token='.$access_token;
    			$response = $httpSocket->put($link.$token_string, $link_parameters);
    			break;
    		case 'post':
    			$token_string = '?access_token='.$access_token;
    			$response = $httpSocket->post($link, $link_parameters);
    			break;
    		case 'delete':
    			$token_string = '?access_token='.$access_token;
    			$response = $httpSocket->delete($link, $link_parameters);
    			break;
    
    		default:
    			$response = $httpSocket->get($link, $link_parameters);
    			break;
    	}
    
    	if($response->code == 200){
    			
    		$data = json_decode($response->body, true);
    		$data['GIVEN_ACCESS_TOKEN'] = $access_token;
    		return $data;
    	}elseif($repeat){
    			
    		/**
    		 * Second Access token is invalid
    		 */
    			
    		return FALSE;
    	}
    	elseif($response->code == 401){
    			
    		/**
    		 * UnAuthorized
    		 * Request New Token
    		 */
    		if($new_access_token = $this->acquireAccessToken()){
    			/**
    			 * Recursive the function replace access_token
    				*/
    			$link_parameters['access_token'] = $new_access_token;
    			return $this->restConnect($link, $link_parameters, $method, $new_access_token, TRUE);
    		}
    
    
    	}
    	return FALSE;
    }

Here is how my Server Restful Controller looks like in the end:

<?php 
class RecipesController extends AppController {

    public $components = array('RequestHandler', 'OAuth.OAuth');
    public $layout = FALSE;
    
    public function beforeFilter() {
    	$this->Auth->allow();
    }

	public function index() {
        $recipes = $this->Recipe->find('all');
        $this->set(array(
            'recipes' => $recipes,
            '_serialize' => array('recipes')
        ));
    }	
    
   

    public function view($id) {
        $recipe = $this->Recipe->findById($id);
        $this->set(array(
            'recipe' => $recipe,
            '_serialize' => array('recipe')
        ));
    }

    public function add() {
        $this->Recipe->create();
        if ($this->Recipe->save($this->request->data)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }

    public function edit($id) {
        $this->Recipe->id = $id;
        if ($this->Recipe->save($this->request->data)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }

    public function delete($id) {
        if ($this->Recipe->delete($id)) {
            $message = 'Deleted';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }
    
    public function token() {
    	$this->autoRender = false;
    	try {
    		$this->OAuth->grantAccessToken();
    	} catch (OAuth2ServerException $e) {
    		$e->sendHttpResponse();
    	}
    }
}

And that was how I got it to work!