1

I know that it's impossible for the sessions IDs to be the same across multiple browsers, but Drupal is acting really weird with sessions and I've been stuck with this issue for over a week now. Let me explain what the issue is.

I'm building a custom login page (A custom form that connects to an external database for authentication). If the credentials are correct, I'm setting a session variable and redirecting the user to a landing page that only successfully authenticated users can see. I'm setting the session variable to the user's id and checking that to see if the user is logged in or not.

So: 1. If the user tries to access the login page and a session already exists, user is redirected to the logged in page. If it doesn't, log in form is shown. 2. If the user tries to access the logged in page and a session doesn't exist, user is redirected back to the log in page. If session exists, the landing page is shown.

ISSUE #1: Now for some weird reason, after I login, the session ID generated on the landing page (after logging in) seems to be the same across all my browsers (I've tried Firefox, Chrome and even a live remote website testing service). Even if a new session ID is generated on the login page, whenever I'm redirected to the landing page, it has the same old session ID from before when I logged in on another device.

ISSUE #2: Sessions were acting pretty weird earlier too. For example, the session ID generated even on the login page would sometimes be the same across browsers. But this is not as consistent as the issue with the landing page. Also, this session ID is never carried over to the landing page. The session IDs of the login form and the landing page are always different.

ISSUE #3: Earlier, I was also facing the issue where I couldn't read the session variable on the login page to check if the user is already logged in. Even after a successful log in, whenever I came back to the form, it would still display the form. The session variable did not have the value I stored earlier. But if I go the landing page, the session would exist there and not redirect me back to the form.

This is very strange for what should be a straightforward implementation. For my implementation, I've created a controller which handles 2 routes - login and landing page. The login controller embeds the custom form. I've handled redirection in the submitForm handler and in the controllers. Here's my code:

Controller code:

namespace Drupal\igshpa_members\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Session\SessionManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\user\PrivateTempStoreFactory;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionManager;

class MembersController extends ControllerBase {

  /**
   * Drupal\user\PrivateTempStoreFactory definition.
   *
   * @var Drupal\user\PrivateTempStoreFactory
   */
  protected $userPrivateTempstore;

  /**
   * Drupal\Core\Session\SessionManager definition.
   *
   * @var Drupal\Core\Session\SessionManager
   */
  protected $sessionManager;

  /**
   * Drupal\Core\Session\AccountInterface definition.
   *
   * @var Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  // {@inheritdoc}
  public function __construct(
      PrivateTempStoreFactory $user_private_tempstore,
      SessionManager $session_manager,
      AccountInterface $current_user) {
    $this->sessionManager = $session_manager;
    $this->currentUser = $current_user;
    $this->userPrivateTempstore = $user_private_tempstore->get('igshpa_members');
  }

  // {@inheritdoc}
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('user.private_tempstore'),
      $container->get('session_manager'),
      $container->get('current_user')
    );
  }

  public function content() {

    if (!isset($_SESSION['session_started'])) {
      $_SESSION['session_started'] = TRUE;
      $this->sessionManager->start();
    }
    $userId = $this->userPrivateTempstore->get('igshpaId');

    if(!is_null($userId)) {
      $sessId = session_id();
      return array(
        '#theme' => 'members_portal',
        '#test_var' => $this->t($sessId),
      );
    } else {
      return $this->redirect('igshpa_members.igshpa_login');
    }
  }

  public function login() {

    if (!isset($_SESSION['session_started'])) {
      $_SESSION['session_started'] = TRUE;
      $this->sessionManager->start();
    }
    $userId = $this->userPrivateTempstore->get('igshpaId');

    if(!is_null($userId)) {
      return $this->redirect('igshpa_members.members_portal');
    } else {
      $sessId = session_id();
      $form_class = '\Drupal\igshpa_members\Form\IGSHPALogin';
      $form = \Drupal::formBuilder()->getForm($form_class);
      return array(
        '#theme' => 'igshpa_login',
        '#form' => $form,
        '#debug' => $sessId
      );
    }
  }
}

Form code:

namespace Drupal\igshpa_members\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Database\Database;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\user\PrivateTempStoreFactory;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionManager;

/**
 * Implements an example form.
 */
class IGSHPALogin extends FormBase {

  /**
   * Drupal\user\PrivateTempStoreFactory definition.
   *
   * @var Drupal\user\PrivateTempStoreFactory
   */
  protected $userPrivateTempstore;

  /**
   * Drupal\Core\Session\SessionManager definition.
   *
   * @var Drupal\Core\Session\SessionManager
   */
  protected $sessionManager;

  /**
   * Drupal\Core\Session\AccountInterface definition.
   *
   * @var Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  // {@inheritdoc}
  public function __construct(
      PrivateTempStoreFactory $user_private_tempstore,
      SessionManager $session_manager,
      AccountInterface $current_user) {
    $this->sessionManager = $session_manager;
    $this->currentUser = $current_user;
    $this->userPrivateTempstore = $user_private_tempstore->get('igshpa_members');
  }

  // {@inheritdoc}
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('user.private_tempstore'),
      $container->get('session_manager'),
      $container->get('current_user')
    );
  }
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'igshpa_login_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['username'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Username'),
      '#required' => true,
    );
    $form['password'] = array(
      '#type' => 'password',
      '#title' => $this->t('Password'),
      '#required' => true,
    );
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Log in'),
      '#button_type' => 'primary',
    );
    $form['actions']['submit']['#attributes']['class'][] = 'btn-all';
    $form['#cache']['max-age'] = 0;
    $form['#cache']['contexts'][] = 'session';
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Validate the information
    // Remember to handle required, text length, format etc
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Receive username and password
    // search db for entry
    // If present, set session to user id | session!!!
    // redirect to user profile page | redirect!!!
    $username = $form_state->getValue('username');
    $password = $form_state->getValue('password');

    $con = Database::getConnection('default','igshpa');
    // I've removed the actual query for security purposes:
    $query = $con->query('SELECT ....');

    $result = $query->fetchAll();

    if (sizeof($result) == 1) {
      if (!isset($_SESSION['session_started'])) {
        $_SESSION['session_started'] = TRUE;
        $this->sessionManager->start();
      }
      $userId = $this->userPrivateTempstore->set('igshpaId',$result[0]->person_id);
      $form_state->setRedirect('igshpa_members.members_portal');
      return;
    } else {
      drupal_set_message($this->t('Wrong username or password. Try again.'), 'error');
      return;
    }
  }

}
1

The session id of a request from a browser with no session cookie is always the same, because the session you see in Drupal at this point is not real. Drupal waits until the response is finished to start a session when there is new session data to store. This lazy behavior is to prevent generating sessions that are never used. Usually you don't interfere with that by using the session manager. You simply store and retrieve data in the session and do so as if a session is started (and don't think about it that it is not real). That's the reason why the private tempstore is very difficult to use for anonymous traffic and you normally don't need it. All you need is to store the user id in the session $session->set('igshpaId', $result[0]->person_id);. So the form submit could be like this:

  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Receive username and password
    // search db for entry
    // If present, set session to user id | session!!!
    // redirect to user profile page | redirect!!!
    $username = $form_state->getValue('username');
    $password = $form_state->getValue('password');
    $con = Database::getConnection('default','igshpa');
    // I've removed the actual query for security purposes:
    $query = $con->query('SELECT ....');
    $result = $query->fetchAll();

    if (sizeof($result) == 1) {
      $session = $this->requestStack->getCurrentRequest()->getSession();
      $session->set('igshpaId', $result[0]->person_id);
      $form_state->setRedirect('igshpa_members.members_portal');
      return;
    }
    else {
      drupal_set_message($this->t('Wrong username or password. Try again.'), 'error');
      return;
    }
  }

And then in the controller

use Symfony\Component\HttpFoundation\Request;  

...

  public function content(Request $request) {
    $session = $request->getSession();
    $userId = $session-get('igshpaId');
    ...
  }

If you have a route callback like a controller you can get the request directly from the route arguments. So no need to inject any services.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.