Welcome to Abdul Malik Ikhsan's Blog

Apigility: Using zf-oauth2’s refresh_token_lifetime to create client’s remember me functionality

Posted in Tutorial PHP, Zend Framework 2, Zend Framework 3 by samsonasik on August 13, 2017

If you’re building client based application which require oauth authentication to apigility application which uses time based expire access token, you may want to create a remember me functionality in client.

PHP Configuration

The very first required is set the client and the server side has same timezone, eg:

# your php.ini
date.timezone = "Asia/Jakarta"

This is ensure that you have actually same time for both client and api server.

DB data requirements

If you already setup the Oauth2 DB, you need to insert/update the client to support “password” and “refresh_token”, that use both means use space-separated value eg:

INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `grant_types`, `scope`, `user_id`) VALUES
(
    'test',
    '$2y$10$vbuy12RNSTJ.LHDdivegwu9dqkxh8h6OS4VoIX64HQGngAqUfcSJe',
    '/oauth/receivecode',
    'password refresh_token',
    NULL,
    NULL
);

Above sql insert data to oauth_clients table with client_id valued “test” with bcrypted client_secret “test”.

You can also insert a users data, for example:

INSERT INTO `oauth_users` (`username`, `password`) VALUES
(
    'test',
    '$2y$10$vbuy12RNSTJ.LHDdivegwu9dqkxh8h6OS4VoIX64HQGngAqUfcSJe'
),

Above sql insert data to oauth_users table with username valued “test” with bcrypted password “test”.

ZF-Oauth2 configuration

In apigility side, we can configure the “zf-oauth2” config, for example, as follows:

// config/autoload/global.php
return [
    // ...
    'zf-oauth2' => [
        'access_lifetime' => 1800,
        'options' => [
            'refresh_token_lifetime' => 604800,
            'always_issue_new_refresh_token' => true,
        ],
    ],
];

The configuration above means we can have an access token lifetime in 1800 seconds, and we can re-issue the new token lifetime with existing “refresh_token” as far as the time range is not > 604800 seconds ( 1 week ). For example, we authenticate with data:

{
    "grant_type": "password",
    "username": "test",
    "password": "test",
    "client_id": "test",
    "client_secret" : "test"
}

have authenticated tokens data like the following:

{
  "access_token": "8e4b0e5ddc874a6f1500514ef530dbea3976ae77",
  "expires_in": 1800,
  "token_type": "Bearer",
  "scope": null,
  "refresh_token": "d19b79cd376924409c14ee46e5230617482fb169"
}

The “refresh_token” is the key here.

The client application

I assume you’re using Zend Framework 2/3 application for client side, which we can use Zend\Authentication\AuthenticationService service. We can build custom Auth storage for it, eg:

namespace Application\Storage;

use Zend\Authentication\Storage;

class AuthStorage extends Storage\Session
{
    public function __construct()
    {
        parent::__construct('app_client');

        $sessionConfigOptions = [
            'use_cookies'     => true,
            'cookie_httponly' => true,
            'gc_maxlifetime'  => 1800,
            'cookie_lifetime' => 1800,
        ];
        $this->getSessionManager()->getConfig()
                                  ->setOptions($sessionConfigOptions);
    }

    public function rememberMe($time)
    {
        $this->getSessionManager()->rememberMe($time);
    }

    public function clear()
    {
        $this->getSessionManager()->forgetMe();
        parent::clear();
    }

    public function getSessionManager()
    {
        return $this->session->getManager();
    }
}

You can now create factory to build the AuthenticationService service with the authstorage like I blog posted at Create ZF Client Authentication for Apigility Oauth with ApigilityConsumer post.

On authentication part, eg: AuthenticationController, you can do:

$result = $this->authenticationService->authenticate();

if ($result->isValid()) {
    $storage = $this->authenticationService->getStorage();

    // save next "expires" time to session
    $storage->write(
        $storage->read() +
        [
            // it is better to use
            // api service to get the `oauth_access_tokens` real expires
            'expires' => date('Y-m-d H:i:s', time() + $read['expires_in'])
        ]
    );

    // for example, you have "rememberme" checkbox
    if (($rememberme = $request->getPost('rememberme')) == 1 ) {

        $storage->rememberMe(604800);

        $read = $storage->read();
        $storage->write(
            compact('rememberme') +
            $read +
            [
                // it is better to use
                // api service to get the `oauth_refresh_tokens` real expires
                'refreshExpires' => date('Y-m-d H:i:s', time() + 604800)
            ]
        );
    }
    // ...
}

We are going to use “expires” as immediate check session lifetime hit the expires, and “refreshExpires” to check when it stil be able to re-issue new token.

In bootstrap, for example, in Application\Module::onBootstrap() you can verify it to re-issue the token when access lifetime has hit.

namespace Application;

use Zend\Authentication\AuthenticationService;
use Zend\Mvc\MvcEvent;

class Module
{
    // ...
    public function onBootstrap(MvcEvent $e)
    {
        $services = $e->getApplication()->getServiceManager();
        $storage  = $services->get(AuthenticationService::class)->getStorage();
        $read     = $storage->read();

        if (isset($read['access_token'])) {

            $timeFirst   = strtotime($read['expires']);
            $currentTime = date('Y-m-d H:i:s');
            $timeSecond  = strtotime($currentTime);
            $counter     = $timeFirst - $timeSecond;

            if (! empty($rememberme = $read['rememberme'])) {

                $storage->getSessionManager()
                        ->getConfig()
                        ->setStorageOption('gc_maxlifetime', 604800)
                        ->setStorageOption('cookie_lifetime', 604800);
                
                if ($counter < 0 && $currentTime < $read['refreshExpires']) {

                    // API CALL to apigility oauth uri
                    // with grant_types = "refresh_token" and uses
                    // refresh_token as the key to re-issue new token
                    //
                    //    {
                    //        "grant_type"    : "refresh_token",
                    //        "refresh_token" : $read['refresh_token']
                    //        "client_id"     : "test",
                    //        "client_secret" : "test"
                    //    }
                    //
                    $storage->write(
                        [
                            // it is better to use
                            // api service to get the `oauth_access_tokens` real expires
                            'expires' => date('Y-m-d H:i:s', time() + $read['expires_in']),

                            // api service to get the `oauth_refresh_tokens` real expires
                            'refreshExpires' => date('Y-m-d H:i:s', time() + 604800),
                        ] +
                        compact('rememberme') +
                        [
                            "access_token": "<new access token based on oauth call>",
                            "expires_in": <new expire_in based on oauth call>,
                            "token_type": "<new token_type based on oauth call>",
                            "refresh_token": "<new refresh_token token based on oauth call>"
                        ] +
                        $read
                    );

                    $read = $storage->read();
                }
            }

            if ($currentTime > $read['expires'])) {
                // force clean up session
                $storage->clear();
            }

        }

    }
    // ...
}

Note

As commented in the codes sample above, in your real life application, it is beter to use real token expires instead of adding current time with expire_in time or manual fill refresh token lifetime. Do more automation yourself!

If you use Zend Framework 2/3 or Zend Expressive, you can try ApigilityConsumer for client module to consume api services. Enjoy 😉

Advertisements

Auto add _links property of HAL Resources into all api service in Apigility

Posted in Tutorial PHP, Zend Framework, Zend Framework 2, Zend Framework 3 by samsonasik on July 10, 2017

If you want to have the _links property value to HAL Resource in apigility api service, for example:

{
    "id": 1,
    "name": "Abdul Malik Ikhsan",
    
    "_links": {
        "self": {
            "href": "http://api.dev/user/1"
        }
    }
}

you can do manually in every api service:

use ZF\ContentNegotiation\ViewModel;
use ZF\Hal\Entity as HalEntity;
use ZF\Hal\Link\Link;

// ...

    public function userAction()
    {
        $halEntity = new HalEntity([
             'id' => 1,
             'name' => 'Abdul Malik Ikhsan',   
        ]);

        $link = $halEntity->getLinks();
        $link->add(Link::factory(
              [
                  'rel' => 'self',
                  'url' => $this->getRequest()->getUriString(),
              ]
        ));

        return new ViewModel([
            'payload' => $halEntity,
        ]);
    }

// ...

You can eliminate that by apply via EventManager’s Shared Manager which attach to Zend\Mvc\Controller\AbstractActionController on dispatch event, like below:

namespace Application;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use ZF\Hal\Link\Link;
use ZF\Hal\Plugin\Hal;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $app       = $e->getApplication();
        $sharedEvm = $app->getEventManager()->getSharedManager();

        $sharedEvm->attach(AbstractActionController::class, 'dispatch',
            function($event) use ($sharedEvm) {

                $uri = $event->getRequest()->getUriString();

                $sharedEvm->attach(Hal::class, 'renderEntity', function($event) use ($uri) {
                    $event->getParam('entity')
                          ->getLinks()
                          ->add(Link::factory(
                                [
                                    'rel' => 'self',
                                    'url' => $uri,
                                ]
                            ));
                });

            },
            100
        );

    }

    public function getConfig() { /* */ }
}

On above code, we attach ZF\Hal\Plugin\Hal on renderEntity event which get the ZF\Hal\Entity object from ZF\ContentNegotiation\ViewModel payload property, and apply Link into it via ZF\Hal\Link\Link::factory().

Now, you can eliminate unneeded repetitive codes in all every api services.

Done 😉

Tagged with: , ,

Using Direct ArrayObject instance as ObjectPrototype in Zend\Db

Posted in Zend Framework 2, Zend Framework 3 by samsonasik on May 25, 2017

When creating a table model for ZF2 or ZF3 application with Zend\DB, direct ArrayObject instance can be usefull as ResultSet object prototype. We can no longer need to create an individual class that has getArrayCopy() or exchangeArray() for data transformation.

For example, we have the following table model:

<?php
namespace Application\Model;

use Zend\Db\TableGateway\AbstractTableGateway;

class CountryTable
{
    public static $table = 'country';
    private $tableGateway;

    public function __construct(AbstractTableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function getCountriesInAsia()
    {
        $select  = $this->tableGateway->getSql()->select();
        $select->where([
            'continent' => 'ASIA'
        ]);

        return $this->tableGateway->selectWith($select);
    }
}

The ArrayObject usage we can use is:

new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);

So, we can build the factory for above table model as follows:

<?php
namespace Application\Model;

use ArrayObject;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;

class CountryTableFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $resultSetPrototype = new HydratingResultSet();
        $resultSetPrototype->setObjectPrototype(
             new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
        );
        
        $tableGateway =  new TableGateway(
            CountryTable::$table,
            $container->get('Zend\Db\Adapter\Adapter'),
            null,
            $resultSetPrototype
        );

        return new CountryTable($tableGateway);
    }
}

and register it into service_manager under factories:

<?php
namespace Application;

return [
    // ...
    'service_manager' => [
        'factories' => [
            Model\CountryTable::class => Model\CountryTableFactory:class,
        ],
    ],
];

When retrieving the data, you can do the followings:

use Application\Model\CountryTable;

$countryTable    = $container->get(CountryTable::class);
$countriesInAsia = $countryTable->getCountriesInAsia();

foreach ($countriesInAsia as $key => $row) {

    // dump a copy of the ArrayObject
    var_dump($arrayCopy = $row->getArrayCopy());

    // echoed column as property
    echo $row->name; // with value "INA"
    echo $row->iso;  // with value "ID"
    echo $row->continent; // with value "ASIA"

    // echoed as array with provided key
    echo $row['name']; // with value "INA"
    echo $row['iso'];  // with value "ID"
    echo $row['continent']; // with value "ASIA"

    // modify data via exhangeArray
    $row->exchangeArray(array_merge(
		$arrayCopy,
		[
			'name' => 'INDONESIA',
		]
	));

    // or modify its data by its property
    $row->name = 'INDONESIA';
    // or modify its data by its index array
    $row['name'] = 'INDONESIA';

    echo $row->name; // now has value "INDONESIA"
    echo $row['name']; // now has value "INDONESIA"
}

Bonus:

To avoid repetitive creating factory class for each table model, we can create an abstract factory for it:

<?php

namespace Application\Model;

use ArrayObject;
use Interop\Container\ContainerInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;

class CommonModelTableFactory implements AbstractFactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName)
    {
        return ((substr($requestedName, -5) === 'Table') && class_exists($requestedName));
    }

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $tableModel = '\\' . $requestedName;

        $resultSetPrototype = new HydratingResultSet();
        $resultSetPrototype->setObjectPrototype(
            new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
        );

        $tableGateway =  new TableGateway(
            $tableModel::$table,
            $container->get('Zend\Db\Adapter\Adapter'),
            null,
            $resultSetPrototype
        );

        return new $tableModel($tableGateway);
    }
}

So, now, we can have 1 abstract factory for all table model services:

<?php
namespace Application;

return [
    // ...
    'service_manager' => [
        'abstract_factories' => [
            Model\CommonModelTableFactory:class,
        ],
    ],
];

That’s it 😉

Create ZF Client Authentication for Apigility Oauth with ApigilityConsumer

Posted in Tutorial PHP, Zend Framework, Zend Framework 2, Zend Framework 3 by samsonasik on March 28, 2017

If you have Apigility as API builder in API side, and client app that consume it using Zend Framework 2/3 or ZF Expressive, you can create an authentication from the client application that call oauth in apigility side.

Zend\Authentication has AbstractAdapter that you can extends to create custom adapter for its need. Let’s assume the applications are like the following diagram:

[CLIENT - A ZF Application]              [API - An Apigility Application]
         |                                          |
   AuthController                     ZF\MvcAuth\Authentication\OAuth2Adapter          
         |                                          |
         |       authenticateAction()               |
         |   ------------------------------------>  |
         |         identity json                    |
         |   <------------------------------------  |

On oauth result call, you may get the following result:

{
  "access_token": "8e4b0e5ddc874a6f1500514ef530dbea3976ae77",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": null,
  "refresh_token": "d19b79cd376924409c14ee46e5230617482fb169"
}

The ApigilityConsumer

ApigilityConsumer is a ZF2/ZF3 Apigility Client module (can also be used in ZF Expressive) to consume Apigility API Services.

You can install by run composer command:

composer require samsonasik/apigility-consumer

For full configurations and features, you can read at its README, for this post’s need, you can do something like this:

<?php
// config/autoload/apigility-consumer.local.php
return [
    'apigility-consumer' => [
        // your apigility host url
        'api-host-url' => 'https://your.apigilty.api.host',

        // your apigility oauth setting
        'oauth' => [

            'grant_type'    => 'password',
            'client_id'     => 'your client id',
            'client_secret' => 'your client secret',

        ],

    ],
];

and register the module into config/application.config.php or config/modules.config.php:

<?php
// config/application.config.php or config/modules.config.php
return [
    'ApigilityConsumer', // <-- register here
    'Application',
],

Create Adapter

You need to extends Zend\Authentication\Adapter\AbstractAdapter and implements Zend\Authentication\Adapter\AdapterInterface. So, You can have the class:

<?php

namespace Application\Adapter;

use ApigilityConsumer\Service\ClientAuthService;
use Zend\Authentication\Adapter\AbstractAdapter;
use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Authentication\Result;

class ApigilityAuthenticationAdapter
    extends AbstractAdapter
    implements AdapterInterface
{
    /**
     * @var ClientAuthService
     */
    private $clientAuthService;

    /**
     * @param  ClientAuthService $clientAuthService
     */
    public function __construct(ClientAuthService $clientAuthService)
    {
        $this->clientAuthService = $clientAuthService;
    }

    /**
     * @return Result
     */
    public function authenticate()
    {
        $clientResult = $this->clientAuthService->callAPI(
            [
                // your oauth registered route segment in apigility. 
                'api-route-segment' => '/oauth', 

                'form-data' => [
                    'username' => $this->getIdentity(),
                    'password' => $this->getCredential(),
                ],

                'form-request-method' => 'POST',
            ]
        );

        if (! $clientResult->success) {
            return new Result(Result::FAILURE, null, $clientResult::$messages);
        }

        return new Result(RESULT::SUCCESS, $clientResult->data);
    }
}

Your can now build a factory from it:

<?php
namespace Application\Adapter;

use ApigilityConsumer\Service\ClientAuthService;

class ApigilityAuthenticationAdapterFactory
{
    public function __invoke($container)
    {
        return new ApigilityAuthenticationAdapter(
            $container->get(ClientAuthService::class)
        );
    }
}

You can then register at service_manager:

<?php
// module/Application/config/module.config.php
namespace Application;

'service_manager' => [
    'factories' => [
        // ...
        Adapter\ApigilityAuthenticationAdapter::class => Adapter\ApigilityAuthenticationAdapterFactory::class,
    ],
],

For ZF Expressive, you can register under ‘dependencies’ key.

Set Adapter into AuthenticationService

You need to set authentication service’s adapter with defined adapter above with factory:

<?php
namespace Application\Factory;

use Application\Adapter\ApigilityAuthenticationAdapter;
use Zend\Authentication\AuthenticationService;
use Zend\Authentication\Storage\Session;

class AuthenticationServiceFactory
{
    public function __invoke($container)
    {
        $adapter = $container->get(ApigilityAuthenticationAdapter::class);

        return new AuthenticationService(
            new Session(), // or your own storage implementing  Zend\Authentication\Storage\StorageInterface
            $adapter
        );
    }
}

You can then register also at service_manager:

<?php
// module/Application/config/module.config.php
namespace Application;

use Zend\Authentication\AuthenticationService;

'service_manager' => [
    'factories' => [
        // ...
        AuthenticationService::class => Factory\AuthenticationServiceFactory::class,
    ],
],

For ZF Expressive, you can register under ‘dependencies’ key.

The AuthController::authenticate()

I assume that you already inject controler with login form, use “username” and “password” as field names, and fill the data, so, your AuthController::authenticate() can be like the following:

<?php
namespace Application\Controller;

use Application\Form\LoginForm;
use Zend\Authentication\AuthenticationService;

class AuthController
{
    public function __construct(
        AuthenticationService $authenticationService,
        LoginForm $loginForm,
    ) { /* ...*/ }

    public function authenticateAction()
    {
        /*
         *    check request and form validity here
         */
        $formData = $this->loginForm->getData();
        $this->authenticationService->getAdapter()
                                    ->setIdentity($formData['username'])
                                    ->setCredential($formData['password']);

        $result = $this->authenticationService->authenticate();
        if (!$result->isValid()) {
            /**
             * For security reason, you should not show user the reason of failure,
             * However, if it actually needed for specific purpose, you can pull by call:
             *
             *     $result->getMessages();
             *
             */
            return $this->redirect()->toRoute('/auth');
        }

        return $this->redirect()->toRoute('/account');
    }
}

For ZF Expressive, you can create routed Authentication middleware.

That’s it, you’re now have successfully created a client authentication for your ZF2/ZF3 or ZF Expressive application that consume Apigility oauth.

Tagged with: , ,