Zend Framework 2 has implemented session mechanism which works quite well, but there are few missing features. One of this features are good handling of session timeout extension.

The case

User logs in at 10:00 AM. Authentication feature (based on Zend\Auth) do all the checkings and finally persists login result in session. There is a session timeout set to 1800 seconds (30 minutes). User works on the website all the time and suddenly he gets logged out at 10:30. That’s truly incorrect, because he was still active.

The goal

Implementation of idle timeout for authentication. If user is active on page, we do constantly extend the session. But once he is inactive during given time he should be logged out after that time.

The try

We can call method rememberMe() on the Zend\Session\SessionManager object which will of corse set cookie lifetime to new value. Unfortunately this method will also call regenerateId method, which will delete old session by default. This works almost well, but sometimes session is lost in random time periods. That’s the issue we would like to avoid.

The solution

The only solution for that moment is own implementation of authentication timeouts. To have that we should do couple of things.

Let’s start with adding new configuration value authentication_expiration_time in \config\autoload\global.php

{...}
'session' => array(
  'config' => array(
    'class' => 'Zend\Session\Config\SessionConfig',
    'options' => array(
      'name' => 'cookieName',
      'cookie_httponly' => true,
      'cookie_lifetime' => 3600,
      'gc_maxlifetime' => 3600,
      'remember_me_seconds' => 3600,
    ),
    'authentication_expiration_time' => 300
  ),
  'validators' => array(
    'Zend\Session\Validator\RemoteAddr',
    'Zend\Session\Validator\HttpUserAgent',
  ),
),
{...}

As we can see global timeout for session we have set to 3600 seconds and could be extended to much more, depending on the requirements for service. authentication_expiration_time is set to 300 seconds so user will be logged out after 5 minutes of inactivity.

Next step is to create own session storage class which will extend Zend\Storage\Session:

use Zend\Authentication\Storage;
use Zend\Session\Container;

class AuthSessionStorage extends Storage\Session {

  const SESSION_CONTAINER_NAME = 'authContainer';
  const SESSION_VARIABLE_NAME = 'authContainerVariable;

  private $allowedIdleTimeInSeconds = 1800;

  public function setAuthenticationExpirationTime()
  {
    $expirationTime = time() + $this->allowedIdleTimeInSeconds;

    $authSession = new Container(self::SESSION_CONTAINER_NAME);

    if( $authSession->offsetExists(self::SESSION_VARIABLE_NAME) ) {
       $authSession->offsetUnset(self::SESSION_VARIABLE_NAME);
    }

    $authSession->offsetSet(self::SESSION_VARIABLE_NAME, $expirationTime);
  }

  public function isExpiredAuthenticationTime() {
    $authSession = new Container(self::SESSION_CONTAINER_NAME);

    if($authSession->offsetExists(self::SESSION_VARIABLE_NAME)) {
      $expirationTime = $authSession->offsetGet(self::SESSION_VARIABLE_NAME);
        return $expirationTime < time();
      }
    return false;
 }

  public function clearAuthenticationExpirationTime() {
    $authSession = new Container(self::SESSION_CONTAINER_NAME);
    $authSession->offsetUnset(self::SESSION_VARIABLE_NAME);
  }

  public function getAuthenticationExpirationTime() {
    $authSession = new Container(self::SESSION_CONTAINER_NAME);
    return $authSession->offsetGet(self::SESSION_VARIABLE_NAME);
  }

  /**
   * @param int $allowedIdleTimeInSeconds
   */
  public function setAllowedIdleTimeInSeconds($allowedIdleTimeInSeconds) {
    $this->allowedIdleTimeInSeconds = $allowedIdleTimeInSeconds;
  }
}

There are couple of methods in this class:

  • setAuthenticationExpirationTime – this method gets current time, adds timeout for authentication and stores value in session variable
  • isExpiredAuthenticationTime – checks if authentication should expire or not
  • clearAuthenticationExpirationTime – removes expiration time from session. This method is called when isExpiredAuthenticationTime will return true or on user logout action
  • getAuthenticationExpirationTime – returns authentication expiration time (therefor we can present to user when he will be logged out
  • setAllowedIdleTimeInSeconds – setter for idle time with value get ie. from config

Now it’s time to modify Module.php file:

{...}
public function getServiceConfig() {
  return array(
    {...}
    'factories' => array(
      {...}
      'AuthSessionStorage' => function($sm) {
        $config = $sm->get('config');

        $authSessionStorage = new AuthSessionStorage('theName', null, $sm->get('Zend\Session\SessionManager'));
        $authSessionStorage->setAllowedIdleTimeInSeconds($config['session']['config']['authentication_expiration_time']);
        return $authSessionStorage;
      },
      'AuthService' => function($sm) {
        $authAdapter = new AuthAdapter();
        $authAdapter->prepareAdapter($sm->get('Zend\Db\Adapter\Adapter'));
        $authAdapter->initStorage($sm->get('AuthSessionStorage'));

        return $authAdapter;
      },
      {...}
    )
  )
}
{...}

First we create AuthSessionStorage which gets the value of authentication_expiration_time from config. Then we set this as the storage for the AuthAdapter().

So everything what we need is in the place. Now we can show how to use that features.

Usage

I will put a short code snippets with few words of comment to explain how and where to do it. But the implementation is on your side, depending on your application architecture.

Checking if user authentication session is not expired

/** @var AuthSessionStorage $authStorage */
$authStorage = $e->getApplication()->getServiceManager()->get('AuthSessionStorage');

if($authStorage->isExpiredAuthenticationTime()) {
  $authStorage->clearAuthenticationExpirationTime();
  $authStorage->forgetMe();
}

As we can see if the authentication time is expired we immediately clear information about expiration time and call forgetMe() method which is responsible to destroy authentication informations.

Setting/extending authentication session timeout

/** @var AuthSessionStorage $authStorage */
$authStorage = $e->getApplication()->getServiceManager()->get('AuthSessionStorage');
$authStorage->setAuthenticationExpirationTime();

The code above should be put to the part of code where we do user authentication. Once the user is authenticated we should call setAuthenticationExpirationTime() method to set timeout for the user authentication session.

Also this piece of code should be called in the service which is called by onBootstrap where we do check if the user is authenticated and authentication session is not expired.

Getting expiration time for the authentication session timeout

/** @var AuthSessionStorage $authStorage */
$authStorage = $e->getApplication()->getServiceManager()->get('AuthSessionStorage');
$authStorage->getAuthenticationExpirationTime();

This will return expiration time in seconds.

Clearing authentication session expiration time during logout

/** @var AuthSessionStorage $authStorage */
$authStorage = $e->getApplication()->getServiceManager()->get('AuthSessionStorage');
$authStorage->clearAuthenticationExpirationTime();

When the user press logout button on website, the code above should be called together with the authentication information removal.

Zend Framework 2 – implementation of session authentication timeouts
Tagged on:                     

2 thoughts on “Zend Framework 2 – implementation of session authentication timeouts

  • 2016/01/20 at 15:59
    Permalink

    Im new to Zend, Wr to create class in zend? Do I need to create seperate module for this?

    Reply
  • 2016/05/17 at 19:32
    Permalink

    Hello,

    Thanks to you, I was able to implement authentication session as I wanted. I made this class, inspired by yours, which is simpler because the storage clears itself when needed and it uses session_config configuration keys to populate timeouts. Also, gc_maxlifetime configuration key is was completely ignored by the framework. This class implements it.
    For debugging, I made this class very verbose but the logger is optional.

    use Zend\Authentication\Storage\Session;
    use Zend\Session\SessionManager;
    use Zend\Session\Config\StandardConfig;
    use Zend\Log\LoggerAwareInterface;
    use Zend\Log\LoggerAwareTrait;
    
    /**
     *
     * @author bertrand
     *
     */
    class SessionStorage extends Session implements LoggerAwareInterface
    {
    
        const DEFAULT_REMEMBER_ME_SECONDS = 1800;
    
        private $rememberMeSeconds;
    
        private $gcMaxLifeTime;
    
        use LoggerAwareTrait;
    
        const TIMEOUT_KEY = 'timeout';
    
        public function __construct($namespace = null, $member = null, SessionManager $manager = null)
        {
            // INIT
            $this->gcMaxLifeTime = ini_get('session.gc_maxlifetime');
            $this->rememberMeSeconds = static::DEFAULT_REMEMBER_ME_SECONDS;
            if ($this->getLogger()) $this->logger->debug(__METHOD__);
            parent::__construct($namespace, $member, $manager);
            if (! is_null($manager)) {
                $config = $manager->getConfig();
                if ($config instanceof StandardConfig) {
                    $rememberMeSeconds = $config->getRememberMeSeconds();
                    if (!is_null($rememberMeSeconds)) {
                        $this->rememberMeSeconds = $rememberMeSeconds;
                    }
                    $gcMaxlifetime = $config->getGcMaxlifetime();
                    if (!is_null($gcMaxlifetime)) {
                        $this->gcMaxLifeTime = $gcMaxlifetime;
                    }
                }
            }
    
            // PROCESS
            ini_set('session.gc_maxlifetime', $this->gcMaxLifeTime);
            $auth = parent::read();
            if(!$this->isAuthenticationExpirationTimeExpired() && !is_null($auth)) {
                $this->setAuthenticationExpirationTime();
            }
        }
    
        /**
         * {@inheritDoc}
         * @see \Zend\Authentication\Storage\Session::read()
         */
        public function read() {
            if ($this->getLogger()) $this->logger->debug(__METHOD__);
            if ($this->isAuthenticationExpirationTimeExpired()) {
                if ($this->getLogger()) $this->logger->debug('Authentication time expired');
                $this->clear();
                if ($this->getLogger()) $this->logger->debug('Cookie cleared');
            }
            $auth = parent::read();
            if ($this->getLogger()) $this->logger->debug('Value returned: ' . serialize($auth));
            return $auth;
        }
    
        public function clear() {
            if ($this->getLogger()) $this->logger->debug(__METHOD__);
            if ($this->getLogger()) $this->logger->debug('Clearing now: "' .$this->member . '" and "' . self::TIMEOUT_KEY. '"');
            parent::clear();
            unset($this->session->{self::TIMEOUT_KEY});
        }
    
        /**
         * @return SessionStorage
         */
        private function setAuthenticationExpirationTime()
        {
            if ($this->getLogger()) $this->logger->debug(__METHOD__);
            $time = time();
            $expirationTime = $time + $this->rememberMeSeconds;
            if ($this->getLogger()) $this->logger->debug('Current time: ' . $time
                . ', allowed idle time: ' . $this->rememberMeSeconds
                . ', calculated expiration time: ' . $expirationTime);
            $this->session->{self::TIMEOUT_KEY} = $expirationTime;
    
            // Cookie lifetime is recalculated thanks to the following, without destroying authentication:
            $this->session->getManager()->regenerateId(false);
            if ($this->getLogger()) $this->logger->debug('Called regenerateId(false)');
            return $this;
        }
    
        /**
         * @return boolean
         */
        private function isAuthenticationExpirationTimeExpired()
        {
            $isExpired = false;
            if ($this->getLogger()) $this->logger->debug(__METHOD__);
            if (isset($this->session->{self::TIMEOUT_KEY})) {
                $expirationTime = $this->session->{self::TIMEOUT_KEY};
                $time = time();
                if ($this->getLogger()) $this->logger->debug('Expiration time read: ' . $expirationTime . ' against now: ' . $time);
                $isExpired = $expirationTime getLogger()) $this->logger->debug('Is expired: ' . ($isExpired ? 'true' : 'false'));
            } else {
                if ($this->getLogger()) $this->logger->debug('NO "'.self::TIMEOUT_KEY.'" key in cookie, so not expired');
            }
            return $isExpired;
        }
    }
    
    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Social Widgets powered by AB-WebLog.com.