Commit 32ee1634 authored by Tino Goratsch's avatar Tino Goratsch

reworked the user context hash handling

parent cb33a160
......@@ -10,6 +10,7 @@ use ACP3\Core\Application\BootstrapCache\Event\Listener\UserContextListener;
use ACP3\Core\Session\SessionHandlerInterface;
use ACP3\Core\View\Renderer\Smarty\Filters\MoveToBottom;
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
......@@ -38,8 +39,8 @@ class BootstrapCache extends HttpCache implements CacheInvalidation
HttpKernelInterface $kernel,
StoreInterface $store,
SurrogateInterface $surrogate = null,
array $options = [])
{
array $options = []
) {
parent::__construct($kernel, $store, $surrogate, $options);
$this->addSubscriber(new UserContextListener([
......@@ -48,6 +49,9 @@ class BootstrapCache extends HttpCache implements CacheInvalidation
]));
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
if (isset($options['debug']) && $options['debug']) {
$this->addSubscriber(new DebugListener());
}
}
/**
......
......@@ -7,107 +7,15 @@
namespace ACP3\Core\Application\BootstrapCache\Event\Listener;
use ACP3\Modules\ACP3\Users\Model\AuthenticationModel;
use FOS\HttpCache\SymfonyCache\CacheEvent;
use FOS\HttpCache\SymfonyCache\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Caching proxy side of the user context handling for the symfony built-in HttpCache.
*
* @see \FOS\HttpCache\SymfonyCache\UserContextSubscriber for the original file as we had to override some logic...
*/
class UserContextListener implements EventSubscriberInterface
class UserContextListener extends \FOS\HttpCache\SymfonyCache\UserContextListener
{
/**
* The options configured in the constructor argument or default values.
*
* @var array
*/
private $options;
/**
* Generated user hash.
*
* @var string
*/
private $userHash;
/**
* When creating this listener, you can configure a number of options.
*
* - anonymous_hash: Hash used for anonymous user. Hash lookup skipped for anonymous if this is set.
* - user_hash_accept_header: Accept header value to be used to request the user hash to the
* backend application. Must match the setup of the backend application.
* - user_hash_header: Name of the header the user context hash will be stored into. Must
* match the setup for the Vary header in the backend application.
* - user_hash_uri: Target URI used in the request for user context hash generation.
* - user_hash_method: HTTP Method used with the hash lookup request for user context hash generation.
* - session_name_prefix: Prefix for session cookies. Must match your PHP session configuration.
*
* @param array $options Options to overwrite the default options
*
* @throws \InvalidArgumentException if unknown keys are found in $options
*/
public function __construct(array $options = [])
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'anonymous_hash' => null,
'user_hash_accept_header' => 'application/vnd.fos.user-context-hash',
'user_hash_header' => 'X-User-Context-Hash',
'user_hash_uri' => '/_fos_user_context_hash',
'user_hash_method' => 'GET',
'session_name_prefix' => 'PHPSESSID',
]);
$this->options = $resolver->resolve($options);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
Events::PRE_HANDLE => 'preHandle',
];
}
/**
* Look at the request before it is handled by the kernel.
*
* Adds the user hash header to the request.
*
* Checks if an external request tries tampering with the use context hash mechanism
* to prevent attacks.
*
* @param CacheEvent $event
*/
public function preHandle(CacheEvent $event)
{
$request = $event->getRequest();
if (!$this->isInternalRequest($request)) {
// Prevent tampering attacks on the hash mechanism
if ($request->headers->get('accept') === $this->options['user_hash_accept_header']
|| $request->headers->get($this->options['user_hash_header']) !== null
) {
$event->setResponse(new Response('', 400));
return;
}
if ($request->isMethodSafe() && $hash = $this->getUserHash($event->getKernel(), $request)) {
$request->headers->set($this->options['user_hash_header'], $hash);
}
}
// let the kernel handle this request.
}
/**
* Remove unneeded things from the request for user hash generation.
*
......@@ -123,116 +31,6 @@ class UserContextListener implements EventSubscriberInterface
$hashLookupRequest->cookies->set(AuthenticationModel::AUTH_NAME, $authCookie);
}
$sessionIds = [];
foreach ($originalRequest->cookies as $name => $value) {
if ($this->isSessionName($name)) {
$sessionIds[$name] = $value;
$hashLookupRequest->cookies->set($name, $value);
}
}
if (count($sessionIds) > 0) {
$hashLookupRequest->headers->set('Cookie', http_build_query($sessionIds, '', '; '));
}
}
/**
* Checks if passed request object is to be considered internal (e.g. for user hash lookup).
*
* @param Request $request
*
* @return bool
*/
private function isInternalRequest(Request $request)
{
return $request->attributes->get('internalRequest', false) === true;
}
/**
* Returns the user context hash for $request.
*
* @param HttpKernelInterface $kernel
* @param Request $request
*
* @return string
*/
private function getUserHash(HttpKernelInterface $kernel, Request $request)
{
if (isset($this->userHash)) {
return $this->userHash;
}
if ($this->options['anonymous_hash'] && $this->isAnonymous($request)) {
return $this->userHash = $this->options['anonymous_hash'];
}
// Hash lookup request to let the backend generate the user hash
$hashLookupRequest = $this->generateHashLookupRequest($request);
$resp = $kernel->handle($hashLookupRequest);
// Store the user hash in memory for sub-requests (processed in the same thread).
$this->userHash = $resp->headers->get($this->options['user_hash_header']);
return $this->userHash;
}
/**
* Checks if current request is considered anonymous.
*
* @param Request $request
*
* @return bool
*/
private function isAnonymous(Request $request)
{
// You might have to enable rewriting of the Authorization header in your server config or .htaccess:
// RewriteEngine On
// RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
if ($request->server->has('AUTHORIZATION') ||
$request->server->has('HTTP_AUTHORIZATION') ||
$request->server->has('PHP_AUTH_USER')
) {
return false;
}
if ($request->cookies->has(AuthenticationModel::AUTH_NAME)) {
return false;
}
foreach ($request->cookies as $name => $value) {
if ($this->isSessionName($name)) {
return false;
}
}
return true;
}
/**
* Checks if passed string can be considered as a session name, such as would be used in cookies.
*
* @param string $name
*
* @return bool
*/
private function isSessionName($name)
{
return strpos($name, $this->options['session_name_prefix']) === 0;
}
/**
* Generates the request object that will be forwarded to get the user context hash.
*
* @param Request $request
*
* @return Request The request that will return the user context hash value
*/
private function generateHashLookupRequest(Request $request)
{
$hashLookupRequest = Request::create($this->options['user_hash_uri'], $this->options['user_hash_method'], [], [], [], $request->server->all());
$hashLookupRequest->attributes->set('internalRequest', true);
$hashLookupRequest->headers->set('Accept', $this->options['user_hash_accept_header']);
$this->cleanupHashLookupRequest($hashLookupRequest, $request);
return $hashLookupRequest;
parent::cleanupHashLookupRequest($hashLookupRequest, $originalRequest);
}
}
......@@ -47,7 +47,7 @@ trait CacheResponseTrait
}
$response
->setVary('X-User-Context-Hash')
->setVary('Cookie')
->setMaxAge($lifetime)
->setSharedMaxAge($lifetime);
}
......
......@@ -10,6 +10,8 @@ use ACP3\Core\ACL\ACLInterface;
use ACP3\Core\Controller\AbstractWidgetAction;
use ACP3\Core\Controller\Context\WidgetContext;
use FOS\HttpCache\UserContext\DefaultHashGenerator;
use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Response;
class Hash extends AbstractWidgetAction
{
......@@ -33,15 +35,20 @@ class Hash extends AbstractWidgetAction
/**
* @return \Symfony\Component\HttpFoundation\Response
*/
public function execute()
public function execute(): Response
{
$this->response->setVary('Cookie');
$this->response->setPublic();
$this->response->setMaxAge(60);
$this->response->headers->add([
'Content-type' => 'application/vnd.fos.user-context-hash',
'X-User-Context-Hash' => $this->hashGenerator->generateHash()
]);
$accept = AcceptHeader::fromString($this->request->getSymfonyRequest()->headers->get('Accept'));
if ($accept->has('application/vnd.fos.user-context-hash')) {
$this->response->setVary('Cookie');
$this->response->setPublic();
$this->response->setMaxAge(3600);
$this->response->headers->add([
'Content-type' => 'application/vnd.fos.user-context-hash',
'X-User-Context-Hash' => $this->hashGenerator->generateHash()
]);
} else {
$this->response->setStatusCode(Response::HTTP_NOT_ACCEPTABLE);
}
return $this->response;
}
......
......@@ -24,9 +24,7 @@ class Login extends Core\Controller\AbstractWidgetAction
*/
public function execute()
{
$this->setCacheResponseCacheable(
$this->config->getSettings(\ACP3\Modules\ACP3\System\Installer\Schema::MODULE_NAME)['cache_lifetime']
);
$this->setCacheResponseCacheable();
if ($this->user->isAuthenticated() === false) {
$prefix = $this->request->getArea() === Core\Controller\AreaEnum::AREA_ADMIN ? 'acp/' : '';
......
......@@ -51,10 +51,7 @@ class IsAuthenticatedProvider implements ContextProvider
$context->addParameter('security_secret', $settings['security_secret']);
$context->addParameter('authenticated', $this->userModel->isAuthenticated());
$context->addParameter('user_id', $this->userModel->getUserId());
$context->addParameter('roles', $this->acl->getUserRoleIds($this->userModel->getUserId()));
if (intval($settings['cache_vary_user']) === 1) {
$context->addParameter('user_id', $this->userModel->getUserId());
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment