Keeping Active User Logged In in ZF3


With all of our Zend Framework 2 apps, our users can stay logged in as long as they are active – as determined by them essentially hitting pages within the app.  If they stopped navigating around, the system would time them out after X amount of time (usually 20 minutes, but app dependent) or if they closed their browser.  This kept users from getting time out in the middle of an action (something that is very annoying!) while still ensuring that they were automatically logged out if they walked away from the app without logging out themselves (a frequent issue).

As I’ve mentioned before, we’re now in the process of transitioning to ZF3.  While the organization and dependency handling in ZF3 changed significantly, most of the core helper components didn’t seem to have major changes.  In some ways, this has been great as we could reuse much of the existing code to speed up the redo…this has also been bad because we could reuse much of the existing code.  Let’s be honest, we developers are still human and copy/paste is way faster than retyping it all from scratch.  So some stuff we’d copy, paste, then clean up to current standards, including bringing it to PSR-2 standards and implementing the lovely typing abilities of PHP 7.

In doing so, though, we have also hit snags where the underlying ZF3 components had, in fact, changed.  Some we stumbled on quick.  For example, the AbstractValidator’s isValid function no longer has a $context parameter, so we had to update our instances to comply with that spec. Another one we caught after we’d already done 2 apps and were well on to the third.  Namely (and to bring it back around to the start of this whole post), the same basic code we’d used for our authentication stuff was no longer keeping active users logged in!  And while the broader context had changed a bit, the actual code to store the authenticated user in Identity was pretty much line for line identical, meaning something in the underlying AuthenticationService’s Result had changed.

Some Googling confirmed that we weren’t the only one having this issue with ZF3, but what we weren’t seeing was anyone who actually had a solution!  Not good.  We tried a few of the suggested possibilities, but much like the original posters, found they didn’t work.  One promising solution worked a little too well and just stopped logging them out altogether.  Whoops!

We continued working through the records, while occasionally revisiting the issue and testing any new hints we found, until finally, finally, we hit on the solution.  And so, yeah, I’m posting about it one – in case I forget down the road (LOL) and two – to help the others in the same situation who get frustrated with the lack of a single solution.  Here’s hoping this saves someone else a lot of hours of searching, testing, frustration, and all that. 🙂

I’ll start by saying that our basic Authentication system is based on the seemingly standard way of doing it in ZF3, which is already well detailed by Oleg Krivtsov in his free Using Zend Framework 3 book (his version is so much easier to get through than Zend’s own examples).  So if you need to know how to implement user authentication at all, check that out first.  The only difference with our system is that our authentication is done through a CAS system, but that doesn’t have any bearing on the actual issue (we confirmed it’s the same on a regular user-authenticated system as well). For ease of discussion, I’ll presume you stuck with Oleg’s example and your authentication module is called User. If you called it something else (we did), then just adjust the references accordingly 😀

So, this solution has a few parts.  First, it starts with the cookie settings in your global config – namely, due to whatever change was made in ZF3, whatever time outs you set there will be the timeout period.  As such, you’ll need to up the time outs significantly to allow for the average time a user would likely be in your app – in our case, we went with the standard 8-hour workday.  Anyone working 8 hours straight in our app needs a vacation anyway! Feel free to lower this to match your own user needs (really 4 hours is probably good).

        // Session configuration.
        'session_config' => [
            // Session cookie will expire in 8 hours
            'cache_expire' => 60*60*8,
            'cookie_lifetime' => 60*60*8,
            // Session data will be stored on server maximum for 7 days.
            'gc_maxlifetime' => 60*60*24*7,
            // only allow on secure connections
            'cookie_secure' => true,
            'cookie_httponly' => true,

        ],
        // Session manager configuration.
        'session_manager' => [
            'config' => [
                'class' => Session\Config\SessionConfig::class,
                'options' => [
                    'name' => 'my_app',
                ],
            ],
            'storage' => SessionArrayStorage::class,
            // Session validators (used for security).
            'validators' => [
                RemoteAddr::class,
                HttpUserAgent::class,
            ]
        ],
        // Session storage configuration.
        'session_storage' => [
            'type' => SessionArrayStorage::class
        ],

Next, we want to make sure Zend always uses the session we configured versus the default one, so in your Application’s Module.php, add this to your onBootstrap function:

            // The following line instantiates the SessionManager and automatically
            // makes the SessionManager the 'default' one.
            $application = $event->getApplication();
            $service_manager = $application->getServiceManager();
            $session_manager = $service_manager->get(SessionManager::class);

To be honest, I’m not 100% certain that part is absolutely necessary, but it’s part of what works so we kept it in. It seemed like good practice either way. 🙂

Now for the core part – implementing a manual time out function! This is the function that will make sure your users get time out if they stop being active while keeping them logged in if they continue using the app. We also need to make sure they still get logged out if the browser is closed too. Let’s head to User\Service\AuthManager. In the Login function, find this line:

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

then after it, add this line to ensure we get logged out on browser closed:

$this->session_manager->forgetMe();

Also, at the top of our class, add a new private variable:

private $timeout = 60 * 20; // time out in 20 minutes

This is your app’s REAL inactive time out, so adjust accordingly. Now, let’s add our time out function, called simply checkTimeout.

        public function checkTimeout() : bool
        {
            $is_timed_out = false;

            if($this->authentication_service->hasIdentity()) {
                $identity = $this->authentication_service->getIdentity();

                // get the current timeout and check it
    			$stored = $identity['time_out'];
    			$expires = $stored + $this->timeout;

    			// if they already expired, kick them out
    			if ($expires < time() && $identity['account_id'] != 0) {
    				$this->logout();
    				$is_timed_out = true;

    			// otherwise, renew it so it continues going
    			} else {
    				$identity['time_out'] = time();
    				$this->authentication_service->getStorage()->write($identity);
    			}
            } else {
                $is_timed_out = true;
            }

            return $is_timed_out;
        }

As you can see, we basically check the current time against the time the account should have timed out to see if they sat on a page for longer than the allowed inactive time. If so, we force logged them out and kick back a true variable. If not, we update their last active time accordingly and continue on. But where did that time out variable come from? For that, we head to the AuthAdapter. In your authenticate function, when you set the identity of the logged in user, you just need to manually add the time out variable, like so:

return new Result(
    Result::SUCCESS,
    [
        'user_id' => $user->user_id,
        'name' => $user->user_name
        'email_address' => $user->email_address,
        'time_out' => time()
    ],
    ['Authenticated successfully.']
);

Then, finally, we implement the time out check in our User\Module.php’s onDispatch function (which you should already have as part of your general access control):

        public function onDispatch(MvcEvent $event)
        {
            $controller = $event->getTarget();
            $router_config = $event->getRouteMatch()->getParams();

            $is_restricted = false;

            if (array_key_exists('is_restricted', $router_config) ) {
                $is_restricted = $router_config['is_restricted'];
            }

            // Get the instance of AuthManager service.
            $authentication_manager = $event->getApplication()->getServiceManager()->get(AuthenticationManager::class);

            // If trying to access restricted content
            if (!$authentication_manager->isLoggedIn() && !$authentication_manager->filterAccess($is_restricted)) {
                return $controller->redirect()->toRoute('login');

            // keep their session active if they are actually doing stuff
            } elseif($authentication_manager->isLoggedIn()) {
                $timed_out = $authentication_manager->checkTimeout();

                if($timed_out) {
                    $flash_messenger = $event->getApplication()->getServiceManager()->get('ControllerPluginManager')->get('flashmessenger');
                    $flash_messenger->setNamespace('error')->addMessage('Your session has timed out due to inactivity.');
                    return $controller->redirect()->toRoute('login');
                }
            }
        }

Specifically, after confirming we have a logged in user, we call our checkTimeout function. If it comes back true, meaning they had timed out, we set a helpful message and kick them back to the login screen. And that’s it 🙂 You can test it’s working by setting your inactive time out to something short, like 1-2 minutes, just to see it in action.