Alexander Steshenko | Technical Blog

BasicCRM Sessions and User Login

Sessions in web application

What is a session in a web application? It is a period of time with some kind of activity and data possibly associated with it. For example, a user opens an online store and starts adding goods into his cart. He isn’t a registered user, he is not authenticated. He may just as well close the page and forget about those goods he’s added. What is that thing the cart is associated to? The session. The session of them working with the site. When he’s gone - the session ends. It’s important that sessions are limited in time, they start and end. When a session ends (user decides not to buy anything from your store) all stuff associated with the session is gone along with it. That’s why sessions are convenient.

Implementing sessions for a DDD application

Even though frameworks and even programming languages often have things like Sessions built-in (also ACL, authentication etc), it is important to understand that all those things are actually part of Domain. How to use those built-in features in a DDD application?

My approach to this is to always implement Sessions for the purposes of a specific application instead of relying on built-in features. In my practice there have always been problems because of relying on PHP’s session either with performance or flexibility of the overall architecture.

Session for BasicCrm

I decided that we don’t really need sessions for not registered users (like in the online store example). That is, when a registered user logs in - only then a session starts. In other words, in response to the login action user gets new session started. I don’t remember in which book it was described so well about the ways of session implementation variations. There are three ways to go about it, but essentially two are important:

  1. clients send the session data every request so the system uses it
  2. clients only use a secure session id and all the session data is stored in the system

The one I used the most is the second one. When somebody logs in to the BasicCRM application we let them know the session identifier: a unique, secure id. Whenever they want to do anything within this session they started they need to provide this id. As as far as it is secure and associated with the user who started the session there’s no need to provide user credentials each request. Just session id.

Sessions are considered valid only for one day since they been used last time.

“Session” class of objects:

namespace Domain;

/**
 * @Entity(repositoryClass="Domain\SessionsRepository")
 * @Table(name="sessions")
 */
class Session
{
    const LIFETIME_DAYS = 1;

    /**
     * @Id @Column(type="string")
     * @GeneratedValue(strategy="NONE")
     */
    protected $id;

    /** @ManyToOne(targetEntity="User") */
    protected $user;

    /**
     * @Column(type="datetime")
     * @var \DateTime
     */
    protected $modified;

    public function __construct(User $user)
    {
        $this->id = md5(uniqid());
        $this->user = $user;
        $this->refresh();
    }

    public function getId()
    {
        return $this->id;
    }

    public function isValid()
    {
        $now = new \DateTime();
        // where '%a' is total amount of days
        return $now->diff($this->modified)->format('%a') <= self::LIFETIME_DAYS;
    }

    public function refresh()
    {
        $this->modified = new \DateTime();
    }

    public function getUser()
    {
        return $this->user;
    }
}

SessionsRepository

What would we do in every Service Layer function where we need to process a user-provided session id? It’s not enough to get it from the datastorage, we also need check whether it’s valid or not and mark it as “used” i.e. prolong it being valid. This is some kind of logic we’d like to avoid duplicating. Any logic being duplicated is a sign that you need to introduce a new domain term that would be responsible for that logic.

Here I put all that into the SessionRepository:

The code implementing this logic:

class SessionsRepository extends \Doctrine\ORM\EntityRepository
{
    public function getValid($id)
    {
        $session = $this->find($id);
        if (!$session) {
            throw new \DomainException('Session is not found');
        }
        if (!$session->isValid()) {
            throw new \DomainException('Session is no longer valid');
        }
        $session->refresh();
        $this->_em->persist($session);

        return $session;
    }
}

Here’s the list of Service Layer functions (the system’s public API) I’ve added for these new domain objects:  

MVC  

A few MVC additions here. I’ve added an action helper that attempts to view current session data and if it’s a registered user using the system now - it switches the layout:

class SessionActionHelper extends \Zend_Controller_Action_Helper_Abstract
{
    public function preDispatch()
    {
        $authService = \ServiceLocator::getAuthService();
        $sessionId = $this->getRequest()->getCookie('sessionid');
        if ($sessionId) {
            try {
                $session = $authService->viewSession($sessionId);
                // Switching layout
                $this->_actionController->view->layout()->setLayout('company');
                // Save session data for possible further use in the View:
                $this->_actionController->view->assign('session', $session);
            } catch (DomainException $error) {
                // An error catched during session retrieval, we remove the cookie
                $expires = new Zend_Date(0, Zend_Date::TIMESTAMP);
                $this->getResponse()->setHeader('Set-Cookie',
                    'sessionid=; '
                    . 'expires=' . $expires->get(Zend_Date::COOKIE) . '; '
                    . 'HttpOnly; '
                    . 'path=/'
                );
            }
        }
    }
}

This way if you log in you’ll see a different set of menus and current session data information (like the email):

It’s a de facto standard these days to save session ids in the browser’s cookies so it’s all transparent for the users. I decided to follow that best practice: when user logs in, the browser is requested to remember the id in the cookies, when they log out - the cookie is cleared:

class AuthController extends Zend_Controller_Action
{
    public function loginAction()
    {
        if ($this->_request->isPost()) {
            $authService = \ServiceLocator::getAuthService();
            $session = $authService->loginUser(
                $this->_getParam('email'),
                $this->_getParam('password')
            );

            // Setting sessionid cookie header
            $expires = new Zend_Date();
            $expires->addDay(7);
            $this->getResponse()->setHeader('Set-Cookie',
                'sessionid=' . $session->getId() . '; '
                . 'expires=' . $expires->get(Zend_Date::COOKIE) . '; '
                . 'HttpOnly; '
                . 'path=/'
            );
            $this->_redirect('/company/dashboard');
        }
    }

    public function logoutAction()
    {
        $authService = \ServiceLocator::getAuthService();
        $authService->logoutUser($this->getRequest()->getCookie('sessionid'));
        // And remove the cookie
        $expires = new Zend_Date(0, Zend_Date::TIMESTAMP);
        $this->getResponse()->setHeader('Set-Cookie',
            'sessionid=; '
            . 'expires=' . $expires->get(Zend_Date::COOKIE) . '; '
            . 'HttpOnly; '
            . 'path=/'
        );
        $this->_redirect('/');
    }
}

All the code has been published to GitHub, and made live in production.


Comments and feedback are much appreciated. See contact details here