• This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn more.

Fixed Extending/hooking sessions

Affected version
2.0 RC2

Xon

Well-known member
#1
In XF1, it was possible to extend the session object & storage backend.
In XF2, the session object has been split into a few pieces; ie the session & session storage. Except neither is extendable via class_extensions, and if you want to extend the session object effectively need to replace the 'session.public' container key in app_setup
 

Mike

XenForo developer
Staff member
#2
If you want to change the storage, then you can replace session.public.storage itself. A replacement there makes sense and I don't really think making the storage classes extendable is necessarily important.

I'm unclear on what the use case of extending the session object itself is. For the most part, it's essentially a dumb wrapper around a key-value store. There is some logic in it, though I don't think most of it is really designed to be overrideable.

So it'd be worth explaining the what and why of what you're trying to do.
 

Xon

Well-known member
#3
I've got two XF1 => XF2 add-ons which this affects;
  • Separating the session cache from the general XenForo cache.
    • This allows splitting workloads across multiple redis instances. Separating different roles across redis instances allows different redis-instance wide configuration. I effectively have separate memory pools for sessions vs general caching, with different cache eviction policies.
  • Blocking saving sessions for some blacklisted bots.
    • These are bots often known to just spam new sessions due to no cookie handling.
 

DragonByte Tech

Well-known member
#4
To add my own use case to the above list: In XF1, our Security product allowed users to (effectively) kill other sessions belonging to their account.

For instance, if you logged in at your friend's computer, and the forum enforced the "stay logged in" box or you ticked it out of reflex, and you forgot to logout. You could then kill the session from your own computer by going to a list of currently saved sessions (which would also show the user agent string).

This does not appear to be possible in XF2, at least not from what I could find.

Here's the session extension class I used in XF1:

PHP:
<?php
class DBTech_Security_XenForo_Session extends XFCP_DBTech_Security_XenForo_Session
{
    /**
     * Constructor.
     *
     * @param array $config Config elements to override default.
     * @param Zend_Cache_Core|null $cache
     * @param Zend_Db_Adapter_Abstract|null $db
     */
    public function __construct(array $config = array(), Zend_Cache_Core $cache = null, Zend_Db_Adapter_Abstract $db = null)
    {
        // Bubble up the tree
        $previous = parent::__construct($config, $cache, $db);

        // Shorthand
        $xenOptions = XenForo_Application::getOptions();

        if (
            $xenOptions->dbtech_security_active
            AND $xenOptions->dbtech_security_spiders
        )
        {
            $spiders = include 'src/addons/DBTech/Security/3rdParty/spiders.php';

            foreach ($spiders as $spider)
            {
                // Normalisation
                $spider['ident'] = strtolower($spider['ident']);

                if (isset($this->_knownRobots[$spider['ident']]))
                {
                    // Skip this spider
                    continue;
                }

                // Set the "known robots"
                $this->_knownRobots[$spider['ident']] = $spider['ident'];

                // Set the extended info
                $this->_robotMap[$spider['ident']] = [
                    'title' => $spider['name'],
                    'link' => isset($spider['info']) ? $spider['info'] : ''
                ];
            }
        }

        return $previous;
    }

    /**
     * Sets up the session.
     *
     * @param string $sessionId Session ID to look up, if one exists
     * @param string|false $ipAddress IP address in binary format or false, for access limiting.
     * @param array|null $defaultSession If no session can be found, uses this as the default session value
     */
    protected function _setup($sessionId = '', $ipAddress = false, array $defaultSession = null)
    {
        $sessionId = strval($sessionId);

        if ($sessionId)
        {
            /** @var DBTech_Security_XenForo_Model_Security $securityModel */
            $securityModel = XenForo_Model::create('DBTech_Security_XenForo_Model_Security');

            // Remove old sessions
            $securityModel->deleteOldSessions();

            // Get cookie
            $_sessionId = $securityModel->getSessionCookie();
            if ($_sessionId)
            {
                // Check to see if we can get our session
                if (!$securityModel->getSessionBySessionId($_sessionId))
                {
                    // Delete session cookie
                    $securityModel->deleteSessionCookie();

                    // Create a new session for us
                    return parent::_setup('', $ipAddress, $defaultSession);
                }
            }
        }

        return parent::_setup($sessionId, $ipAddress, $defaultSession);
    }

    /**
     * Indicates a login as a user and sets up a password date in the session
     * for an extra layer of security (invalidates the session when the password changes).
     *
     * @param integer $userId
     * @param integer $passwordDate
     */
    public function userLogin($userId, $passwordDate)
    {
        $previous = parent::userLogin($userId, $passwordDate);

        /** @var DBTech_Security_XenForo_Model_Security $securityModel */
        $securityModel = XenForo_Model::create('DBTech_Security_XenForo_Model_Security');

        // Remove old sessions
        $securityModel->deleteOldSessions();

        // Get cookie
        $sessionId = $securityModel->getSessionCookie();
        if (!$sessionId)
        {
            // Create a new session
            $sessionId = $securityModel->createSession($userId);

            // Set our cookie
            $securityModel->setSessionCookie($sessionId);

            // And do what we did before
            return $previous;
        }
        else
        {
            // Get the session if we have it
            $session = $securityModel->getSessionBySessionId($sessionId);

            if ($session)
            {
                // The session was real, this user should be logged in
                $securityModel->updateSessionLastActive($sessionId);

                // Set our cookie
                $securityModel->setSessionCookie($sessionId);

                // And do what we did before
                return $previous;
            }
            else
            {
                // Create a new session
                $sessionId = $securityModel->createSession($userId);

                // Set our cookie
                $securityModel->setSessionCookie($sessionId);

                // And do what we did before
                return $previous;
            }
        }

        return $previous;
    }

    public function saveSessionToSource($sessionId, $isUpdate)
    {
        /** @var DBTech_Security_XenForo_Model_Security $securityModel */
        $securityModel = XenForo_Model::create('DBTech_Security_XenForo_Model_Security');

        // Remove old sessions
        $securityModel->deleteOldSessions();

        // Get cookie
        $_sessionId = $securityModel->getSessionCookie();
        if ($_sessionId)
        {
            // Check to see if we can get our session
            $securityModel->updateSessionLastActive($_sessionId);
        }

        return parent::saveSessionToSource($sessionId, $isUpdate);
    }
}
Basically, it captures the session ID and ties it to a user ID in a separate DB record (maintained by the $securityModel, and ensures that any sessions missing from those records are prevented from accessing the member's account.


Fillip
 

Chris D

XenForo developer
Staff member
#5
From the next release, XF\Session can be extended, but we're only allowing the public session to be extended.

Similarly, there's a new code event that allows overriding the public storage object (though you could also do this using container extensions, so it's mostly just a shortcut).