We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Adding minimalist token authentication to the REST Tutorial

Hello

I have looked at the HMAC authentication (https://phalcon-rest.redound.org/) users suggested here but it looks too excessive for me and it does not use Micro framework. I just want to add a very simple and minimaist authentication to the REST example of the phalcon website.

The only thing I need is when a call arrives (to specific API functions that require auth) I can somehow find which user is behind the call.

So I need to:

1- Intercept calls and "if they require authentication" but the request does not provide a valid token (i.e. one that belongs to a user) I can send back a json error message.

2- Inside api functions I can call a getUser function (looks which user is the owner of current token) and get the user info. Even if I can get just the token, I can do the rest.

That's it.

So, is there a minimalist example that has implemented just that?

Thanks.



93.7k
Accepted
answer

Hey, I think I have just the thing you need. One of my clients has an API only for internal use. Everything is only for local network, so I needed to know which user did the request, nothing more...

For this I used Authorization: Bearer UNIQUE_TOKEN header which is sent with every API request.

Sample code that does the job:

BaseController - this is the controller that all other API Controllers extend.


public function beforeExecuteRoute()
{
    // Route names that do not need authorization
    $authorizeExceptions = [
        'api-documentation'
    ];
    if (!in_array($this->router->getMatchedRoute()->getName(), $authorizeExceptions)) {
        // Authorize
        $result = $this->authorize();
        if (is_null($result)) {
            $this->_response['messages'] = 'Please authorize with valid API token!';
            $this->_response['statusCode'] = 401;
            $this->afterExecuteRoute();
            die();
        }
    }

    // We accept only application/json content in POST and PUT methods
    if (in_array($this->request->getMethod(), ['POST', 'PUT']) AND $this->request->getHeader('Content-Type') != 'application/json') {
        $this->_response['messages'] = 'Only application/json is accepted for Content-Type in POST requests.';
        $this->_response['statusCode'] = 400;
        $this->afterExecuteRoute();
        die();
    }
}

// Return the API response.
public function afterExecuteRoute()
{
    // Status code & Response header
    $this->_response['statusMessage'] = $this->_statusCodes[$this->_response['statusCode']];
    $this->response->setStatusCode($this->_response['statusCode'], $this->_statusCodes[$this->_response['statusCode']]);
    $this->response->setHeader('Access-Control-Allow-Origin', '*');
    $this->response->setHeader('X-Content-Type-Options', 'nosniff');
    $this->response->setHeader('X-Frame-Options', 'deny');
    $this->response->setHeader('Content-Security-Policy', 'default-src \'none\''); 

    // Set content
    $this->response->setContentType('application/json', 'UTF-8'); 
    $this->response->setJsonContent($this->_response, JSON_UNESCAPED_UNICODE); 

    // Log
    if (!is_null($this->user) AND $this->user->id != 1) {
        $request = $this->request->get();
        $request['accept'] = $this->request->getHeader('Accept');
        $this->db->insertAsDict('api_access_logs', [
            'api_user_id' => $this->user->id,
            'endpoint' => $this->request->getMethod() .' '. getCurrentUrl(false, false),
            'request' => json_encode($request, JSON_UNESCAPED_UNICODE),
            'response' => json_encode($this->_response, JSON_UNESCAPED_UNICODE),
        ]);
    }
    return $this->response->send();
}

// Check if valid Token is given
private function authorize()
{
    $this->user = null;
    $authorizationHeader = $this->request->getHeader('Authorization');
    if ($authorizationHeader AND preg_match('/Bearer\s(\S+)/', $authorizationHeader, $matches)) {
        $tokenParts = explode('|', \Helpers\Common::decodeString($matches[1]));
        // For now token has 3 parts. Update here if you modify token
        // IMPORTANT: Here you can make your custom logic to check for valid token
        if (count($tokenParts) === 3) {
            $this->user = (object) [
                'id' => $tokenParts[0],
                'level' => $tokenParts[1],
            ];
        }
    }
    return $this->user;
}

In the authorize() method I'm encoding/decoding the token with \Phalcon\Crypt library.


class Common
{
    private static $cryptKey = 'i$1^&/:%[email protected]!R1Q<@{([email protected]*!<7u|R2~0';
    public static function encodeString($string)
    {
        return (new \Phalcon\Crypt)->encryptBase64($string, self::$cryptKey, true);
    }
    public static function decodeString($string)
    {
        return (new \Phalcon\Crypt)->decryptBase64($string, self::$cryptKey, true);
    }
    ...
}


5.5k

Nikolay, Thank you very much.

All I should do now is to convert it to a middleware (since I am using Micro Framework).

May I ask what might go wrong if I use that for an internet api? Is there a risk (if I use https with all calls)?

Thanks again for generus sharing of your code.

Well all depends on what you want to achieve.

I found an article where its explained better then i ever will :)

https://zapier.com/engineering/apikey-oauth-jwt/



5.5k

Just converted the code provided by Nicolay to a very minimal middleware:

<?php
use Phalcon\Mvc\Micro;
use Phalcon\Mvc\Router;
use Phalcon\Events\Event;
use Phalcon\Events\Manager;
use Phalcon\Mvc\Micro\MiddlewareInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function beforeExecuteRoute(Event $event, Micro $application)
    {
        $authorizeExceptions = [
            'doc'
        ];
        if (!in_array($application->router->getMatchedRoute()->getName(), $authorizeExceptions)) {
            $result = $this->authorize($application);
            if (is_null($result)) {
                $application->response->setStatusCode(401,'Please authorize with valid API token!');
                $application->response->setContent('Please authorize with valid API token!');
                $application->response->send();
                die();
                //return false;
            }
        }

        if (in_array($application->request->getMethod(), ['POST', 'PUT']) AND $application->request->getHeader('Content-Type') != 'application/json') {
            $application->response->setStatusCode(400,'Only application/json is accepted for Content-Type in POST requests.');
            $application->response->send();
            die();
            //return false;
        }

        return true;
    }

    private function authorize(Micro $application)
    {
        $application->token = null;
        $authorizationHeader = $application->request->getHeader('api-token');

        if (strlen($authorizationHeader)>5) {  //check token validity and find from database what user has the token
            $application->token = $authorizationHeader;
            //$application->userid = ?;
        }

        return $application->token;
    }    

    public function call(Micro $application)
    {
        return true;
    }    
}

$app = new Micro();

$eventsManager = new Manager();
$eventsManager->attach('micro', new AuthMiddleware());
$app->before(new AuthMiddleware());
$app->after(new AuthMiddleware());
$app->setEventsManager($eventsManager);

$app->get(
    '/',
    function () use ($app){
        echo '<h1>Welcome!</h1> token: '. $app->token ."<br>";
    }
);

$app->get(
    '/doc',
    function () {
        echo '<h1>api-documentation!</h1>';
    }
)->setName('doc');

$app->get(
    '/profile/{username}',
    function ($username) use ($app) {
        $content = "<h1>This is profile of: {$username}!</h1>";
        $app->response->setContent($content);
        $app->response->send();
    }
);

$app->handle();


43.9k

thanks for sharing.