Yii2 Multi-Tenant Auth: How to Support Concurrent Multi-User Sessions in the Same Browser
A Comprehensive Guide to Multi-Context Authentication, Real-Time Context Switching, and Session Isolation in Yii2 Framework.
1. Introduction & The Engineering Challenge
In modern web applications built on Yii2 Advanced Framework—such as E-Commerce platforms with a Main Admin Panel and a Merchant/Seller Portal—developers often face a critical requirement:
How can a user log into the Main Admin panel AND a Merchant portal concurrently using two different accounts in the exact same browser without session collisions?
The Core Problem:
Web browsers attach the same session cookie (PHP Session ID) to every HTTP request sent to the same domain. Out of the box, Yii2 relies on a single session key ($_SESSION['__id']) to track the authenticated identity.
When a user logs into the Merchant portal in Tab 2, PHP overwrites $_SESSION['__id'] with the Merchant User ID. The moment the user refreshes Tab 1 (Admin Panel), Yii2 loads the Merchant User identity instead of the Admin, leading to broken authorization, UI glitches, or unexpected logouts.
2. Architecture Overview: Real-Time Dynamic Context Switching
Rather than attempting to hack runtime session names or creating fragile beforeRequest event hooks, the optimal solution introduces a Zero Code Mutation Architecture.
By subclassing yii\web\User and yii\web\Session, we intercept authentication and session access points to perform Real-Time Dynamic Context Switching based on the request URI:
┌──────────────────────────────────────────────┐
│ HTTP Request │
└──────────────────────┬───────────────────────┘
│
┌───────────────┴───────────────┐
│ URL Routing Context Check │
└───────────────┬───────────────┘
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
[ Main Admin Route ] [ Merchant Module Route ]
Context = 'admin' Context = 'merchant'
------------------ ---------------------
idParam = '__id' idParam = '__id_merchant'
Cookie = '_identity_admin' Cookie = '_identity_merchant'
Session = $_SESSION['key'] Session = $_SESSION['merchant_key']
Flashes = $_SESSION['__flash'] Flashes = $_SESSION['merchant___flash']
3. Step 1: The Custom User Component (common\components\User.php)
We extend yii\web\User to dynamically evaluate the active URL route. Depending on the route context, the component updates $idParam and $identityCookie on the fly and invalidates cached identity objects when switching between tabs.
<?php
namespace common\components;
use Yii;
use yii\web\User as BaseUser;
class User extends BaseUser
{
private $_lastContext = null;
public function init()
{
$this->applyContext();
parent::init();
}
/**
* Determines the active application context based on URL path.
*/
protected function getCurrentContext()
{
try {
if (Yii::$app->has('request')) {
$path = Yii::$app->request->getPathInfo();
$merchantPrefix = 'merchant'; // Prefix for merchant routes
if ($path === $merchantPrefix || strpos($path, $merchantPrefix . '/') === 0) {
return 'merchant';
}
}
} catch (\Exception $e) {
}
return 'admin';
}
/**
* Applies context-specific idParam and identityCookie configurations.
*/
public function applyContext()
{
$context = $this->getCurrentContext();
if ($this->_lastContext !== $context) {
$env = defined('YII_ENV') ? '_' . YII_ENV : '';
if ($context === 'merchant') {
$this->idParam = '__id_merchant';
$this->identityCookie = [
'name' => '_identity_merchant' . $env,
'httpOnly' => true,
];
} else {
$this->idParam = '__id';
$this->identityCookie = [
'name' => '_identity_admin' . $env,
'httpOnly' => true,
];
}
// Invalidate identity cache when switching context
if ($this->_lastContext !== null) {
$this->setIdentity(null);
}
$this->_lastContext = $context;
}
}
// Intercept entry points to ensure context is applied before execution
public function getIdentity($autoRenew = true)
{
$this->applyContext();
return parent::getIdentity($autoRenew);
}
public function login(\yii\web\IdentityInterface $identity, $duration = 0)
{
$this->applyContext();
return parent::login($identity, $duration);
}
public function logout($destroySession = true)
{
$this->applyContext();
return parent::logout($destroySession);
}
protected function renewAuthStatus()
{
$this->applyContext();
parent::renewAuthStatus();
}
}
4. Step 2: The Custom Session Component (common\components\Session.php)
To prevent generic session data collisions (Yii::$app->session->set('key', $val)) and flash alert mismatches (setFlash), we subclass yii\web\Session.
This component automatically prefixes session keys and isolates flash arrays based on the active context:
<?php
namespace common\components;
use Yii;
use yii\web\Session as BaseSession;
class Session extends BaseSession
{
/**
* Returns key prefix based on current route context.
*/
protected function getContextPrefix()
{
try {
if (Yii::$app->has('request')) {
$path = Yii::$app->request->getPathInfo();
$merchantPrefix = 'merchant';
if ($path === $merchantPrefix || strpos($path, $merchantPrefix . '/') === 0) {
return 'merchant_';
}
}
} catch (\Exception $e) {
}
return '';
}
/**
* Ensures session status is active before reading/writing.
*/
protected function ensureSessionActive()
{
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
}
// --- Session Key Getters & Setters ---
public function get($key, $defaultValue = null)
{
$this->ensureSessionActive();
return parent::get($this->getContextPrefix() . $key, $defaultValue);
}
public function set($key, $value)
{
$this->ensureSessionActive();
parent::set($this->getContextPrefix() . $key, $value);
}
public function remove($key)
{
$this->ensureSessionActive();
return parent::remove($this->getContextPrefix() . $key);
}
public function has($key)
{
$this->ensureSessionActive();
return parent::has($this->getContextPrefix() . $key);
}
// --- Flash Messages Isolation ---
public function setFlash($key, $value = true, $removeAfterAccess = true)
{
$this->ensureSessionActive();
$param = $this->getContextPrefix() . $this->flashParam;
$counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
$counters[$key] = $removeAfterAccess ? -1 : 0;
$_SESSION[$param] = $counters;
$_SESSION[$param][$key] = $value;
}
public function getFlash($key, $defaultValue = null, $delete = false)
{
$this->ensureSessionActive();
$param = $this->getContextPrefix() . $this->flashParam;
$counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
if (!isset($counters[$key])) {
return $defaultValue;
}
$value = isset($_SESSION[$param][$key]) ? $_SESSION[$param][$key] : $defaultValue;
if ($delete) {
$this->removeFlash($key);
}
return $value;
}
public function getAllFlashes($delete = false)
{
$this->ensureSessionActive();
$param = $this->getContextPrefix() . $this->flashParam;
$counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
$flashes = [];
foreach ($counters as $key => $count) {
if (isset($_SESSION[$param][$key])) {
$flashes[$key] = $_SESSION[$param][$key];
if ($delete) {
unset($counters[$key], $_SESSION[$param][$key]);
}
}
}
if ($delete) {
$_SESSION[$param] = $counters;
}
return $flashes;
}
public function removeFlash($key)
{
$this->ensureSessionActive();
$param = $this->getContextPrefix() . $this->flashParam;
$counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
$value = isset($_SESSION[$param][$key]) ? $_SESSION[$param][$key] : null;
unset($counters[$key], $_SESSION[$param][$key]);
$_SESSION[$param] = $counters;
return $value;
}
public function hasFlash($key)
{
return $this->getFlash($key) !== null;
}
}
5. Step 3: Application Configuration (config/main.php)
Configure your custom components in backend/config/main.php (or your relevant configuration file):
return [
'components' => [
// 1. Custom Session Component
'session' => [
'class' => 'common\components\Session',
'timeout' => 86400 * 30,
'name' => 'advanced_backend_session',
'cookieParams' => [
'httpOnly' => true,
'secure' => false,
'lifetime' => 3600 * 24 * 30,
],
],
// 2. Custom User Component
'user' => [
'class' => 'common\components\User',
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
'identityCookie' => [
'name' => '_identity_admin',
'httpOnly' => true,
],
],
// ... rest of your application components
],
];
6. Key Benefits & Takeaways
-
Zero Code Mutation Across Existing Project Code:
- Developers continue calling
Yii::$app->user->identity,Yii::$app->user->id,Yii::$app->session->set(), andYii::$app->session->setFlash()standardly without modifying views, controllers, or widgets.
- Developers continue calling
-
True Concurrent Multi-User Authentication:
- Users can be signed into an Admin account in Tab 1 and a Merchant account in Tab 2 simultaneously within the same browser.
-
100% RBAC & Cache Compatibility:
- Standard RBAC managers (
yii\rbac\DbManageror custom cached managers) key permission queries by$userId(userAccessCheck:userId:permission), preserving full isolation.
- Standard RBAC managers (
Conclusion
By leveraging object-oriented extension points in Yii2's core architecture (yii\web\User and yii\web\Session), you achieve robust multi-tenant authentication and session isolation elegantly. Happy coding!
Top comments (0)