Apigility: Using zf-oauth2’s refresh_token_lifetime to create client’s remember me functionality
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 😉
leave a comment