Create ZF Client Authentication for Apigility Oauth with ApigilityConsumer
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.
Testing Zend Expressive 2 with kahlan 3
Zend\Expressive ^2.0 has different default approach for piping and routing middleware which is programmatically way. In this post, I am going to show you how you can test Zend\Expressive ^2.0 application, with assumption, you use its skeleton with kahlan 3.
First, of course, install the Expressive ^2.0 skeleton, for example, install into directory named “expressive2”:
$ composer create-project zendframework/zend-expressive-skeleton:^2.0 expressive2 Installing zendframework/zend-expressive-skeleton (2.0.0) - Installing zendframework/zend-expressive-skeleton (2.0.0) Downloading: 100% Created project in expressive2 > ExpressiveInstaller\OptionalPackages::install Setting up optional packages Setup data and cache dir Removing installer development dependencies What type of installation would you like? [1] Minimal (no default middleware, templates, or assets; configuration only) [2] Flat (flat source code structure; default selection) [3] Modular (modular source code structure; recommended) Make your selection (2): 3
Now, install kahlan:
$ cd expressive2 $ composer require kahlan/kahlan:^3.1
We are going to need the $app
variable inside tests, for example, when testing functionality for each routed middlewares. To simplify and avoid repetitive code, we can register it into kahlan-config.php in root application:
// ./kahlan-config.php use Kahlan\Filter\Filter; use Zend\Expressive\Application; Filter::register('initialize app', function($chain) { $root = $this->suite(); ob_start(); $root->beforeAll(function ($var) { ob_start(); $var->app = $app = (require 'config/container.php')->get(Application::class); require 'config/pipeline.php'; require 'config/routes.php'; }); return $chain->next(); }); Filter::apply($this, 'run', 'initialize app');
By assign $app
into “$var->app” like above, the “$app” is accessible from entire tests via “$this->app”, so, we can write test like the following:
// ./src/App/spec/Action/HomePageActionSpec.php namespace AppSpec\Action; use Zend\Diactoros\ServerRequest; describe('HomePageAction', function () { describe('/', function () { it('contains welcome message', function () { $serverRequest = new ServerRequest([], [], '/', 'GET'); $this->app->run($serverRequest); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to <span class="zf-green">zend-expressive</span>'); }); }); });
Now, let’s run the tests:
$ vendor/bin/kahlan --spec=src/App/spec/ _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/expressive2 . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.375 seconds (using 8Mo)
That’s it 😉
Querying PostgreSQL’s JSONB with Zend\Db
PostgreSQL is one of Databases that has json support. If you have table(s) that has json column(s), we can query it with Zend\Db in your php application layer.
For example, we have the following data:
CREATE TABLE album (id serial primary key, data jsonb); INSERT INTO album VALUES (1, '{ "title": "Hello", "singer": "Adelle" }'), (2, '{ "title": "September", "singer": "Justin Timberlake" }') ;
We then want to produce SQL like :
select * from album where data ->> 'title' = 'Hello'
There is Zend\Db\Sql\Predicate\Expression
for that. So, you can build select with:
use Zend\Db\Sql\Select; use Zend\Db\Sql\Predicate\Expression; $select = new Select(); $select->from('album') ->where([new Expression("data ->> 'title' = ?", 'Hello')]);
That’s easy! Let’s make it more complicated. We then want to produce SQL with subset of jsonb like:
select * from album where data @> '{"singer": "Adelle"}'
In this case, as it is not a common sql operation across DBs, you need to pass the filter as $expression
, the first parameter of Zend\Db\Sql\Predicate\Expression::__construct
:
use Zend\Db\Sql\Select; use Zend\Db\Sql\Predicate\Expression; $expression = <<<expr data @> '{"singer": "Adelle"}' expr; $select = new Select(); $select->from('album') ->where([new Expression($expression)]);
That’s it 😉
8 comments