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>
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