Log-in-as via Incognito window
tool_muloginas
An alternative "Log in as" plugin that uses incognito/private browser windows to create isolated sessions for logging in as another user. It generates short-lived, single-use tokens via AJAX, which are opened in a new incognito window to establish a separate Moodle session as the target user. The plugin includes proper session lifecycle management, cleanup cron tasks, and event handlers for user logout and deletion.
The plugin demonstrates strong security practices throughout. The authentication mechanism uses short-lived (15-second), single-use, cryptographically random tokens with user-agent binding. Capability checks (tool/muloginas:loginas) are enforced at both the web service and helper layers. The web services are restricted to AJAX-only calls and cannot be invoked via external WS tokens. Admin users are explicitly protected from being impersonated. Session lifecycle management is thorough with proper cleanup on logout, user deletion, and via scheduled cron tasks.
The only findings are low-severity code quality and compliance issues:
- The Privacy API implementation is contradictory — implementing both
null_provider(no personal data) andmetadata\provider(describes stored personal data) simultaneously - Direct
$_SERVER['HTTP_USER_AGENT']superglobal access wherecore_useragent::get_user_agent_string()should be used
No security vulnerabilities, no deprecated API usage, no SQL injection risks, no XSS vectors. The plugin includes comprehensive PHPUnit and Behat tests. Code quality is high overall.
Plugin Overview
tool_muloginas provides an alternative "Log in as" mechanism for Moodle administrators and managers. Instead of using Moodle's built-in log-in-as feature (which shares the browser session), this plugin generates a short-lived token that must be opened in an incognito/private browser window, creating a completely isolated session.
Architecture
The flow is:
- A user with
tool/muloginas:loginascapability visits a target user's profile - Clicking the button triggers an AJAX call (
token_create) that generates a 40-character random token with a 15-second lifetime - A modal displays a link that must be right-clicked and opened in a new incognito window
- The
loginas.phppage validates the token (checking lifetime, single-use, user-agent match, and that the target isn't an admin) and establishes the session - A polling AJAX call (
token_check) auto-closes the modal when the token is consumed
Security Assessment
The security model is well-designed:
- Token security: 40-char cryptographically random tokens, 15-second TTL, single-use, user-agent bound
- Access control:
tool/muloginas:loginascapability required (manager archetype), admin impersonation explicitly blocked - Web service restrictions: AJAX-only, cannot be called from external WS clients
- Session isolation: Uses core
\core\session\manager::loginas()properly - Lifecycle management: Sessions destroyed on logout, user deletion handled, stale data cleaned by cron
- Incognito detection: Pre-config.php cookie/header checks to verify fresh browser window
Code Quality
The codebase is clean, well-structured, and follows Moodle conventions. All DB queries use parameterized $DB methods. Templates use Mustache auto-escaping. The plugin includes PHPUnit tests for all core classes and a Behat test. The only issues are minor: a Privacy API interface contradiction and direct superglobal access where Moodle APIs exist.
Findings
The privacy provider implements both \core_privacy\local\metadata\null_provider and \core_privacy\local\metadata\provider simultaneously. These are contradictory declarations:
null_providerdeclares: "this plugin stores no personal user data"metadata\providerviaget_metadata()declares: "this plugin storesuseridandtargetuseridintool_muloginas_request"
The plugin does store personal data — user IDs, session IDs, and user agent strings — in the tool_muloginas_request table for up to 3 days. While the data is transient, implementing null_provider prevents the privacy subsystem from processing data export or deletion requests for this plugin's data.
Low risk. The practical impact is minimal because:
- Data retention is very short (maximum 3 days via cron, most records expire much sooner)
- The
user_deletedevent handler properly cleans up records for deleted users - The stored data is operational metadata (session tokens), not user-generated content
However, during the retention window, the data would be invisible to Moodle's privacy subsystem for export/erasure requests, which is technically a GDPR compliance gap.
The plugin stores log-in-as request records containing userid, targetuserid, sid (session ID), useragent, targetsid, and timestamps. Records are cleaned up by a cron task within 3 days, and the user_deleted event handler also removes relevant records. However, during the retention window, a GDPR data subject access request processed through Moodle's privacy API would not include or delete this plugin's data because null_provider signals the privacy system to skip this plugin entirely.
class provider implements
\core_privacy\local\metadata\null_provider,
\core_privacy\local\metadata\provider {
Remove the null_provider interface. If the data is truly transient and does not require export/deletion support, implement only metadata\provider and document the rationale. If GDPR compliance is desired, additionally implement \core_privacy\local\request\plugin\provider with export and delete methods.
class provider implements
\core_privacy\local\metadata\provider {
The plugin accesses $_SERVER['HTTP_USER_AGENT'] directly in two methods where Moodle's core_useragent::get_user_agent_string() should be used instead. Direct superglobal access bypasses Moodle's abstraction layer and is less robust (e.g., no graceful handling if the header is absent).
Note: The $_COOKIE, $_SERVER['HTTP_COOKIE'], and $_SERVER['HTTP_REFERER'] accesses in loginas.php lines 41-46 occur before config.php is included, where Moodle APIs are unavailable. These are intentional and necessary for the incognito window detection logic.
Low risk. This is a code quality issue, not a security vulnerability. The direct superglobal access works correctly in practice. Using the Moodle API would improve robustness (e.g., consistent null handling) and follow Moodle coding standards.
The user agent string is stored when a token is created (create_request()) and compared when the token is validated (validate_request()). This is a security measure to ensure the same browser is used for both the token creation and consumption. The comparison happens in a web request context where HTTP_USER_AGENT is virtually always present.
$record->useragent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$record->useragent = \core_useragent::get_user_agent_string();
if ($request->useragent !== $_SERVER['HTTP_USER_AGENT']) {
if ($request->useragent !== \core_useragent::get_user_agent_string()) {
The plugin includes a comprehensive test suite: PHPUnit tests for all core classes (loginas, token_create, token_check, cron) and a Behat test for the user profile integration. The tests cover both positive and negative cases including capability checks, token lifecycle, and cleanup logic.
The security architecture is notably well-designed for this type of plugin. The 15-second token lifetime, single-use enforcement, user-agent binding, admin impersonation protection, and AJAX-only web service restriction create multiple layers of defense. The incognito window detection (pre-config.php cookie checks) is a creative approach to session isolation.
The loginas.php page intentionally does not call require_login() because it establishes a new session in a fresh incognito window where no prior authentication exists. Security is instead enforced through the token mechanism, which is created by an authenticated user with the appropriate capability.
The event handler for user_loggedout (priority -1000) calls die() after rendering a custom logout page for incognito sessions. This is documented in the event observer configuration and is necessary to prevent the normal logout redirect from interfering with the incognito session closure flow.