Skip to content

Commit

Permalink
Merge pull request #410 from FriendsOfSymfony/user_identifier_headers
Browse files Browse the repository at this point in the history
Expand AnonymousRequestMatcher by user_identifier_headers option
  • Loading branch information
dbu committed Mar 17, 2018
2 parents 6276652 + e60ad47 commit c60c5e9
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 13 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Changelog
=========

1.4.5
-----

* Symfony user context: You can now also specify which headers are used for
authentication to detect anonymous requests. By default, the headers are the
previously hardcoded `Authorization`, `HTTP_AUTHORIZATION` and
`PHP_AUTH_USER`.

1.4.4
-----

* Avoid problem with [http_method_override](http://symfony.com/doc/current/reference/configuration/framework.html#configuration-framework-http-method-override).

1.4.3
-----

Expand Down
6 changes: 6 additions & 0 deletions doc/symfony-cache-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ options through the constructor:

**default**: ``GET``

* **user_identifier_headers**: List of request headers that authenticate a non-anonymous request.

**default**: ``['Authorization', 'HTTP_AUTHORIZATION', 'PHP_AUTH_USER']``

* **session_name_prefix**: Prefix for session cookies. Must match your PHP session configuration.
If cookies are not relevant in your application, you can set this to ``false`` to ignore any
cookies. (**Only set this to ``false`` if you do not use sessions at all.**)

**default**: ``PHPSESSID``

Expand Down
27 changes: 26 additions & 1 deletion src/SymfonyCache/UserContextSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand Down Expand Up @@ -51,7 +52,10 @@ class UserContextSubscriber implements EventSubscriberInterface
* 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.
* - user_identifier_headers: List of request headers that authenticate a non-anonymous request.
* - session_name_prefix: Prefix for session cookies. Must match your PHP session configuration.
* To completely ignore the cookies header and consider requests with cookies
* anonymous, pass false for this option.
*
* @param array $options Options to overwrite the default options
*
Expand All @@ -66,8 +70,21 @@ public function __construct(array $options = array())
'user_hash_header' => 'X-User-Context-Hash',
'user_hash_uri' => '/_fos_user_context_hash',
'user_hash_method' => 'GET',
'user_identifier_headers' => array('Authorization', 'HTTP_AUTHORIZATION', 'PHP_AUTH_USER'),
'session_name_prefix' => 'PHPSESSID',
));
if (class_exists('Symfony\Component\HttpKernel\Kernel')
&& (Kernel::MAJOR_VERSION > 2 || Kernel::MINOR_VERSION > 5)
) {
$resolver->setAllowedTypes('anonymous_hash', array('string'));
$resolver->setAllowedTypes('user_hash_accept_header', array('string'));
$resolver->setAllowedTypes('user_hash_header', array('string'));
$resolver->setAllowedTypes('user_hash_uri', array('string'));
$resolver->setAllowedTypes('user_hash_method', array('string'));
// actually string[] but that is not supported by symfony < 3.4
$resolver->setAllowedTypes('user_identifier_headers', array('array'));
$resolver->setAllowedTypes('session_name_prefix', array('string', 'boolean'));
}

$this->options = $resolver->resolve($options);
}
Expand Down Expand Up @@ -126,6 +143,11 @@ public function preHandle(CacheEvent $event)
*/
protected function cleanupHashLookupRequest(Request $hashLookupRequest, Request $originalRequest)
{
if (!$this->options['session_name_prefix']) {
$hashLookupRequest->headers->remove('Cookie');

return;
}
$sessionIds = array();
foreach ($originalRequest->cookies as $name => $value) {
if ($this->isSessionName($name)) {
Expand Down Expand Up @@ -186,7 +208,10 @@ private function getUserHash(HttpKernelInterface $kernel, Request $request)
*/
private function isAnonymous(Request $request)
{
$anonymousRequestMatcher = new AnonymousRequestMatcher($this->options['session_name_prefix']);
$anonymousRequestMatcher = new AnonymousRequestMatcher(array(
'user_identifier_headers' => $this->options['user_identifier_headers'],
'session_name_prefix' => $this->options['session_name_prefix'],
));

return $anonymousRequestMatcher->matches($request);
}
Expand Down
43 changes: 31 additions & 12 deletions src/UserContext/AnonymousRequestMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,56 @@

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Matches anonymous requests using a list of identification headers.
*/
class AnonymousRequestMatcher implements RequestMatcherInterface
{
private $sessionNamePrefix;
/**
* @var array
*/
private $options;

/**
* @param string $sessionNamePrefix Prefix for session cookies. Must match your PHP session configuration
* @param array $options Configuration for the matcher. All options are required because this matcher is usually
* created by the UserContextSubscriber which provides the default values.
*
* @throws \InvalidArgumentException if unknown keys are found in $options
*/
public function __construct($sessionNamePrefix)
public function __construct(array $options = array())
{
$this->sessionNamePrefix = $sessionNamePrefix;
$resolver = new OptionsResolver();
$resolver->setRequired(array('user_identifier_headers', 'session_name_prefix'));
if (class_exists('Symfony\Component\HttpKernel\Kernel')
&& (Kernel::MAJOR_VERSION > 2 || Kernel::MINOR_VERSION > 5)
) {
// actually string[] but that is not supported by symfony < 3.4
$resolver->setAllowedTypes('user_identifier_headers', array('array'));
$resolver->setAllowedTypes('session_name_prefix', array('string', 'boolean'));
}

$this->options = $resolver->resolve($options);
}

public function matches(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;
foreach ($this->options['user_identifier_headers'] as $header) {
if ($request->headers->has($header)) {
return false;
}
}

foreach ($request->cookies as $name => $value) {
if (0 === strpos($name, $this->sessionNamePrefix)) {
return false;
if ($this->options['session_name_prefix']) {
foreach ($request->cookies as $name => $value) {
if (0 === strpos($name, $this->options['session_name_prefix'])) {
return false;
}
}
}

Expand Down
28 changes: 28 additions & 0 deletions tests/Unit/SymfonyCache/UserContextSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@ public function testUserHashUserWithSession($arg, $options)
$this->assertSame($expectedContextHash, $request->headers->get($options['user_hash_header']));
}

/**
* When the session_name_prefix is set to false, the cookie header is completely ignored.
*
* This test does not have authentication headers and thus considers the request anonymous.
*/
public function testUserHashUserIgnoreCookies()
{
$userContextSubscriber = new UserContextSubscriber([
'session_name_prefix' => false,
]);

$sessionId1 = 'my_session_id';
$cookies = array(
'PHPSESSID' => $sessionId1,
);
$cookieString = "PHPSESSID=$sessionId1";
$request = Request::create('/foo', 'GET', array(), $cookies, array(), array('Cookie' => $cookieString));

$event = new CacheEvent($this->kernel, $request);

$userContextSubscriber->preHandle($event);
$response = $event->getResponse();

$this->assertNull($response);
$this->assertTrue($request->headers->has('X-User-Context-Hash'));
$this->assertSame('38015b703d82206ebc01d17a39c727e5', $request->headers->get('X-User-Context-Hash'));
}

/**
* @dataProvider provideConfigOptions
*/
Expand Down

0 comments on commit c60c5e9

Please sign in to comment.