symfonysulu

When multiple users access the Sulu CMF site, the user context suddenly changes


In order to create a restricted area on a website, I set up User Context Caching following this guide. https://docs.sulu.io/en/2.5/cookbook/user-context-caching.html

The login, logout works, but when multiple users access the website, the user context suddenly changes. E.g. I log on with user A, after some clicks I am suddenly user B. The username is displayed in the page header template with {{ app.user.username }}).

In live environment the page is hosted using nginx (for static content) and apache (for dynamic content). Do I need some special configuration there?

Here are the configuration files.

config/routes/fos_http_cache.yaml

user_context_hash:
    path: /_fos_user_context_hash

config/packages/fos_http_cache.yaml

fos_http_cache:
    proxy_client:
        symfony:
            use_kernel_dispatcher: true
        user_context:
            enabled: true
            role_provider: true
            hash_cache_ttl: 0

src/Kernel.php

<?php

declare(strict_types=1);

namespace App;

/*
 * This file is part of Sulu.
 *
 * (c) Sulu GmbH
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

use FOS\HttpCache\SymfonyCache\HttpCacheProvider;
use Sulu\Bundle\HttpCacheBundle\Cache\SuluHttpCache;
use Sulu\Component\HttpKernel\SuluKernel;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class Kernel extends SuluKernel implements HttpCacheProvider
{
    private ?HttpKernelInterface $httpCache = null;

    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
    {
        $container->setParameter('container.dumper.inline_class_loader', true);

        parent::configureContainer($container, $loader);
    }

    public function getHttpCache(): HttpKernelInterface
    {
        if (!$this->httpCache instanceof HttpKernelInterface) {
            $this->httpCache = new SuluHttpCache($this);
            // Activate the following for user based caching see also:
            // https://foshttpcachebundle.readthedocs.io/en/latest/features/user-context.html
            $this->httpCache->addSubscriber(
                new \FOS\HttpCache\SymfonyCache\UserContextListener([
                    'session_name_prefix' => 'SULUSESSID',
                ])
            );
        }

        return $this->httpCache;
    }
}

config/packages/security.yaml

security:
    enable_authenticator_manager: true

    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: true

    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Sulu\Bundle\SecurityBundle\Entity\User: bcrypt

    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        sulu:
            id: sulu_security.user_provider

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/admin/reset, roles: PUBLIC_ACCESS }
        - { path: ^/admin/security/reset, roles: PUBLIC_ACCESS }
        - { path: ^/admin/login$, roles: PUBLIC_ACCESS }
        - { path: ^/admin/2fa, roles: PUBLIC_ACCESS }
        - { path: ^/admin/_wdt, roles: PUBLIC_ACCESS }
        - { path: ^/admin/_profiler, roles: PUBLIC_ACCESS }
        - { path: ^/admin/translations, roles: PUBLIC_ACCESS }
        - { path: ^/admin$, roles: PUBLIC_ACCESS }
        - { path: ^/admin/$, roles: PUBLIC_ACCESS }
        - { path: ^/admin/p/, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_USER }
        - { path: ^/_fos_user_context_hash, roles: [PUBLIC_ACCESS] }
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern: ^/admin(\/|$)
            lazy: true
            provider: sulu
            entry_point: sulu_security.authentication_entry_point
            json_login:
                check_path: sulu_admin.login_check
                success_handler: sulu_security.authentication_handler
                failure_handler: sulu_security.authentication_handler
            logout:
                path: sulu_admin.logout
            two_factor:
                prepare_on_login: true
                prepare_on_access_denied: true
                check_path: 2fa_login_check_admin
                authentication_required_handler: sulu_security.two_factor_authentication_required_handler
                success_handler: sulu_security.two_factor_authentication_success_handler
                failure_handler: sulu_security.two_factor_authentication_failure_handler

        oilxcoin:
            pattern: ^/
            lazy: true
            provider: sulu
            # The login and logout routes need to be created.
            # For an advanced user management with registration and opt-in emails have a look at the:
            # https://github.com/sulu/SuluCommunityBundle
            # Also have a look at the user context based caching when you output user role specific data
            # https://docs.sulu.io/en/2.2/cookbook/user-context-caching.html
            form_login:
                login_path: login
                check_path: login
                default_target_path: /
            logout:
                path: logout
                target: /login
            remember_me:
                secret:   "%kernel.secret%"
                lifetime: 604800 # 1 week in seconds
                path:     /
        
            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#the-firewall
        
            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

sulu_security:
    checker:
        enabled: true
    password_policy:
        enabled: true
        # Sulu uses the simple password_policy pattern ".{8,}" by default
        # You can change it to a more complex pattern with the following lines:
        #pattern: '(?=^.{8,}$)(?=.*\d)(?=.*[^a-zA-Z0-9]+)(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$'
        #info_translation_key: app.password_information

when@test:
    security:
        password_hashers:
            # By default, password hashers are resource intensive and take time. This is
            # important to generate secure password hashes. In tests however, secure hashes
            # are not important, waste resources and increase test times. The following
            # reduces the work factor to the lowest possible values.
            Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
                algorithm: bcrypt
                cost: 4 # Lowest possible value for bcrypt
                time_cost: 3 # Lowest possible value for argon
                memory_cost: 10 # Lowest possible value for argon

        access_decision_manager:
            strategy: affirmative

        providers:
            sulu:
                id: test_user_provider

        firewalls:
            admin:
                http_basic:
                    provider: sulu

    sulu_test:
        enable_test_user_provider: true

config/webspaces/webspace.xml

<?xml version="1.0" encoding="utf-8"?>
<webspace xmlns="http://schemas.sulu.io/webspace/webspace"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://schemas.sulu.io/webspace/webspace http://schemas.sulu.io/webspace/webspace-1.1.xsd">
    <!-- See: http://docs.sulu.io/en/latest/book/webspaces.html how to configure your webspace-->

    <name>example.io</name>
    <key>example</key>

    <localizations>
        <!-- See: http://docs.sulu.io/en/latest/book/localization.html how to add new localizations -->
        <localization language="en" default="true" />
        <localization language="es" />
    </localizations>

    <security permission-check="true">
        <system>private_sale</system>
    </security>

    <default-templates>
        <default-template type="page">default</default-template>
        <default-template type="home">homepage</default-template>
    </default-templates>

    <templates>
        <template type="search">search/search</template>
        <!-- See: http://docs.sulu.io/en/latest/cookbook/custom-error-page.html how to create a custom error page -->
        <template type="error">error/error</template>
        <template type="error-404">error/error-404</template>
    </templates>

    <navigation>
        <contexts>
            <context key="main">
                <meta>
                    <title lang="en">Main Navigation</title>
                    <title lang="de">Hauptnavigation</title>
                </meta>
            </context>
            <context key="footer_quicklinks">
                <meta>
                    <title lang="en">Footer Quicklinks</title>
                    <title lang="en">Footer Quicklinks</title>
                </meta>
            </context>
            <context key="footer_support">
                <meta>
                    <title lang="en">Footer Support</title>
                    <title lang="en">Footer Support</title>
                </meta>
            </context>
        </contexts>
    </navigation>

    <portals>
        <portal>
            <name>example.io</name>
            <key>example</key>

            <environments>
                <environment type="prod">
                    <urls>
                        <url>{host}/{localization}</url>
                    </urls>
                </environment>
                <environment type="stage">
                    <urls>
                        <url>{host}/{localization}</url>
                    </urls>
                </environment>
                <environment type="test">
                    <urls>
                        <url>{host}/{localization}</url>
                    </urls>
                </environment>
                <environment type="dev">
                    <urls>
                        <url>{host}/{localization}</url>
                    </urls>
                </environment>
            </environments>
        </portal>
    </portals>
</webspace>

Solution

  • You did misunderstand user context based caching. Pages in Sulu are cached, and the cache response is returned for every user having the same cache hash. The cache hash, in this scenario, is normally configured based on the roles a user has.

    So User A visit "Page X", that page gets cached, and if you render its username that username is also cached.

    User B visits "Page X", and get the same page returned, but as you rendered the username, you will see the username of the authenticated user that previously visited the page.

    All things which are user specific never ever should be directly rendered on pages itself.

    Something which is specific for a single user should be AJAX loaded, in this case the Username. You can also use ESI, but ESI doesn't make sense in case of something for a single specific user as it blocks the response time and would make it slow. Symfony already ships a render_hinclude where you can call a custom controller which allows you fast implement such things:https://symfony.com/doc/current/templates.html#templates-hinclude