View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0007978 | OXID eShop (all versions) | 1.05. Users | public | 2026-07-16 13:55 | 2026-08-20 11:20 |
| Reporter | rene.gust | Assigned To | |||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | acknowledged | Resolution | open | ||
| Product Version | 7.4.1 | ||||
| Summary | 0007978: login token for admin and frontend user | ||||
| Description | - In our project we have an impersonation feature that lets an admin user load a frontend user and place orders on their behalf. This worked fine up to OXID 7.0. - OXID 7.4 introduced a login-token mechanism (in UserComponent and in User::loadActiveUser()): on login a hash derived from the user's password hash is stored in the session under a single key login-token, and on every loadActiveUser() call it is re-verified against the current password hash — if it doesn't match, the user is logged out. - This breaks impersonation, because the core uses one shared login-token key for both the admin (backend) and the frontend user, distinguished only at runtime via isAdmin(). With admin and frontend logged in in parallel within the same session, whichever context writes the token last overwrites the other, so the other context fails the check and gets logged out. - I've worked out a workaround that splits the token into two context-specific session slots (login-token-admin / login-token-frontend) and translates between them and the generic login-token key the core expects, leaving the actual verifyHash check untouched. - My concern: this relies on the internal login-token key name and the way the core reads/writes it. If a future OXID version renames the key, changes where it's set, or alters the verification flow, the workaround will silently break. - Question: is the login-token handling considered stable/public API, or is it an internal implementation detail that may change? And is there an officially supported extension point for impersonation-style parallel sessions that I should use instead? Thanks | ||||
| Additional Information | <?php namespace OxidProfessionalServices\Impersonation\Model; use OxidEsales\Eshop\Core\Registry; class UserLoginToken extends User_parent { private const LOGIN_TOKEN_CORE = 'login-token'; private const LOGIN_TOKEN_ADMIN = 'login-token-admin'; private const LOGIN_TOKEN_FRONTEND = 'login-token-frontend'; public function login($userName, $password, $setSessionCookie = false) { $result = parent::login($userName, $password, $setSessionCookie); // parent stored 'login-token' in the current isAdmin() context. // Mirror it into the context-specific slot and drop the generic one, // so admin and frontend impersonation don't overwrite each other. $session = Registry::getSession(); $token = $session->getVariable(self::LOGIN_TOKEN_CORE); if ($token !== null) { $session->setVariable($this->getLoginTokenName(), $token); $session->deleteVariable(self::LOGIN_TOKEN_CORE); } return $result; } public function loadActiveUser($blForceAdmin = false) { // Restore the context-specific token into the generic slot before the // parent call, so parent::loadActiveUser() finds it as usual. // Note: the same admin context as in the parent applies here, incl. $blForceAdmin. $session = Registry::getSession(); $token = $session->getVariable($this->getLoginTokenName($blForceAdmin)); if ($token !== null) { $session->setVariable(self::LOGIN_TOKEN_CORE, $token); } $result = parent::loadActiveUser($blForceAdmin); // Clean up: don't leave the generic slot lying around persistently. $session->deleteVariable(self::LOGIN_TOKEN_CORE); return $result; } public function logout() { $result = parent::logout(); // parent only deletes 'login-token'. Remove just the slot of the current // context, so a frontend logout doesn't tear down a parallel admin session // (and vice versa). Registry::getSession()->deleteVariable($this->getLoginTokenName()); return $result; } private function getLoginTokenName(bool $forceAdmin = false): string { return ($this->isAdmin() || $forceAdmin) ? self::LOGIN_TOKEN_ADMIN : self::LOGIN_TOKEN_FRONTEND; } } | ||||
| Tags | No tags attached. | ||||
| Theme | Not defined | ||||
| Browser | Not defined | ||||
| PHP Version | Not defined | ||||
| Database Version | Not defined | ||||
|
|
Environment: plain CE shop from the SDK, no modules installed, apex theme. Verified on `dev-b-7.4.x` (37d01d9) and `dev-b-7.5.x` (10b54a8), MySQL 8.4, PHP 8.3. 1. Create two accounts in the shop: one admin user (member of `oxidadmin`) and one regular frontend customer. Set both passwords through the user model so they are hashed the same way a real login expects. 2. Start a single session and log in as the **admin** in admin context. Read the session and note `auth`, `usr` and `login-token`. Observed: `auth` is set, `usr` is empty, `login-token` holds hash A. 3. On the **same session**, log in as the frontend customer in frontend context. Read the session again. Observed: `auth` still holds the admin id, `usr` now holds the customer id, and `login-token` has been replaced by hash B. There is only one token slot for both contexts. 4. Call `loadActiveUser()` in **admin** context and read the return value and the session. Observed: returns `false`, and afterwards `auth`, `usr` and `login-token` are all cleared. The admin is logged out although only the frontend user logged in after them. 5. Repeat with the order reversed on a fresh session: frontend customer first, then the admin, then call `loadActiveUser()` in **frontend** context. Observed: returns `false` and the session is cleared again, this time throwing out the frontend user. The failure is symmetric, whoever writes the token last evicts the other. 6. Control run A: same session, only the frontend customer logs in, then `loadActiveUser()` in frontend context. Observed: the token is unchanged after the call and the user stays logged in. 7. Control run B: same session, only the admin logs in, then `loadActiveUser()` in admin context. Observed: the token is unchanged after the call and the admin stays logged in. Controls 6 and 7 rule out a general problem with `loadActiveUser()`: the eviction only happens when both contexts are logged in on one session, which is the shared `login-token` key. -QA |