Skip to content

[Security] Make statefull pages NOT turn responses private when the token is not used #28089

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace Symfony\Bridge\Monolog\Processor;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface;

/**
* Adds the current security token to the log entry.
Expand All @@ -24,13 +26,13 @@ class TokenProcessor implements ProcessorInterface

public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
$this->tokenStorage = $tokenStorage instanceof UsageTrackingTokenStorageInterface ? $tokenStorage : new UsageTrackingTokenStorage($tokenStorage);
}

public function __invoke(array $records)
{
$records['extra']['token'] = null;
if (null !== $token = $this->tokenStorage->getToken()) {
if (null !== $token = $this->tokenStorage->getToken(false)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the feeling this is a step backwards in terms of code quality 😢

When I see this and then look at the interface I'm confused:

https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorageInterface.php#L28

So here we kind of code against the implementation UsageTrackingTokenStorage and not against the abstraction/interface anymore.

Copy link
Contributor

@dmaicher dmaicher Jul 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if someone decorated the original security.token_storage service this will not work properly, right? (as the argument will be missing in any parent::getToken() call because its simply not part of the contract).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we kind of code against the implementation UsageTrackingTokenStorage

fixed by adding a new UsageTrackingTokenStorageInterface

if someone decorated the original security.token_storage service this will not work properly, right?

The behavior will be the same as currently. That's the typical downside with service decoration: you're required to adapt to changes done at the decorated level. It's not very different from being required to write new code to leverage any new features. There is no way around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue here is that your new interface adds a new argument to the same method, making it really weird for decorators

Copy link
Member Author

@nicolas-grekas nicolas-grekas Aug 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what/why this is weird. This works fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof I think that's OK. We don't want ppl to have to use two different methods for fetching the token. And this is in a separate interface (UsageTrackingTokenStorageInterface) so that's documented. We don't want ppl to decorate blindly btw.

$records['extra']['token'] = array(
'username' => $token->getUsername(),
'authenticated' => $token->isAuthenticated(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Monolog\Processor\TokenProcessor;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

/**
Expand All @@ -26,8 +26,8 @@ class TokenProcessorTest extends TestCase
public function testProcessor()
{
$token = new UsernamePasswordToken('user', 'password', 'provider', array('ROLE_USER'));
$tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
$tokenStorage->method('getToken')->willReturn($token);
$tokenStorage = $this->getMockBuilder(UsageTrackingTokenStorageInterface::class)->getMock();
$tokenStorage->method('getToken')->with(false)->willReturn($token);

$processor = new TokenProcessor($tokenStorage);
$record = array('extra' => array());
Expand Down
5 changes: 3 additions & 2 deletions src/Symfony/Bridge/Monolog/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
"require-dev": {
"symfony/console": "~3.4|~4.0",
"symfony/event-dispatcher": "~3.4|~4.0",
"symfony/security-core": "~3.4|~4.0",
"symfony/security-core": "~4.2",
"symfony/var-dumper": "~3.4|~4.0"
},
"conflict": {
"symfony/http-foundation": "<3.4"
"symfony/http-foundation": "<3.4",
"symfony/security-core": "<4.2"
},
"suggest": {
"symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
use Symfony\Component\Security\Core\Role\Role;
Expand All @@ -44,7 +46,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn

public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null)
{
$this->tokenStorage = $tokenStorage;
$this->tokenStorage = null === $tokenStorage || $tokenStorage instanceof UsageTrackingTokenStorageInterface ? $tokenStorage : new UsageTrackingTokenStorage($tokenStorage);
$this->roleHierarchy = $roleHierarchy;
$this->logoutUrlGenerator = $logoutUrlGenerator;
$this->accessDecisionManager = $accessDecisionManager;
Expand Down Expand Up @@ -73,7 +75,7 @@ public function collect(Request $request, Response $response, \Exception $except
'inherited_roles' => array(),
'supports_role_hierarchy' => null !== $this->roleHierarchy,
);
} elseif (null === $token = $this->tokenStorage->getToken()) {
} elseif (null === $token = $this->tokenStorage->getToken(false)) {
$this->data = array(
'enabled' => true,
'authenticated' => false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
</service>
<service id="Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface" alias="security.authorization_checker" />

<service id="security.token_storage" class="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage" public="true">
<service id="security.token_storage" class="Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage" public="true">
<tag name="kernel.reset" method="setToken" />
</service>
<service id="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" alias="security.token_storage" />
<service id="Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface" alias="security.token_storage" />

<service id="security.helper" class="Symfony\Component\Security\Core\Security">
<argument type="service_locator">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
Expand Down Expand Up @@ -51,7 +51,8 @@ public function testCollectWhenSecurityIsDisabled()

public function testCollectWhenAuthenticationTokenIsNull()
{
$tokenStorage = new TokenStorage();
$tokenStorage = new UsageTrackingTokenStorage();
$tokenStorage->setToken(null, \Closure::fromCallable(array($this, 'fail')));
$collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy());
$collector->collect($this->getRequest(), $this->getResponse());

Expand All @@ -71,8 +72,8 @@ public function testCollectWhenAuthenticationTokenIsNull()
/** @dataProvider provideRoles */
public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles, array $inheritedRoles)
{
$tokenStorage = new TokenStorage();
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles));
$tokenStorage = new UsageTrackingTokenStorage();
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles), \Closure::fromCallable(array($this, 'fail')));

$collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy());
$collector->collect($this->getRequest(), $this->getResponse());
Expand All @@ -99,8 +100,8 @@ public function testCollectImpersonatedToken()
new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $adminToken),
);

$tokenStorage = new TokenStorage();
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles));
$tokenStorage = new UsageTrackingTokenStorage();
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles), \Closure::fromCallable(array($this, 'fail')));

$collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy());
$collector->collect($this->getRequest(), $this->getResponse());
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/HttpFoundation/Session/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function count()
*
* @internal
*/
public function getUsageIndex()
public function &getUsageIndex()
{
return $this->usageIndex;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authentication\Token\Storage;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

/**
* A token storage that optionally calls a closure when the token is accessed.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class UsageTrackingTokenStorage implements UsageTrackingTokenStorageInterface
{
private $storage;
private $usageTracker;

public function __construct(TokenStorageInterface $storage = null)
{
$this->storage = $storage ?? new TokenStorage();
}

/**
* {@inheritdoc}
*/
public function getToken(bool $trackUsage = true): ?TokenInterface
{
if ($trackUsage && $usageTracker = $this->usageTracker) {
$usageTracker();
}

return $this->storage->getToken();
}

/**
* {@inheritdoc}
*/
public function setToken(TokenInterface $token = null, \Closure $usageTracker = null): void
{
$this->usageTracker = $usageTracker;
$this->storage->setToken($token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authentication\Token\Storage;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

/**
* A token storage that optionally calls a closure when the token is accessed.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface UsageTrackingTokenStorageInterface extends TokenStorageInterface
{
/**
* {@inheritdoc}
*/
public function getToken(bool $trackUsage = true): ?TokenInterface;

/**
* {@inheritdoc}
*/
public function setToken(TokenInterface $token = null, \Closure $usageTracker = null): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Tests\Authentication\Token\Storage;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;

class UsageTrackingTokenStorageTest extends TestCase
{
public function testGetSetToken()
{
$counter = 0;
$usageTracker = function () use (&$counter) { ++$counter; };

$tokenStorage = new UsageTrackingTokenStorage();
$this->assertNull($tokenStorage->getToken());
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock();

$tokenStorage->setToken($token);
$this->assertSame($token, $tokenStorage->getToken());

$tokenStorage->setToken($token, $usageTracker);
$this->assertSame($token, $tokenStorage->getToken(false));
$this->assertSame(0, $counter);
$this->assertSame($token, $tokenStorage->getToken());
$this->assertSame(1, $counter);

$tokenStorage->setToken(null, $usageTracker);
$this->assertNull($tokenStorage->getToken(false));
$this->assertSame(1, $counter);
$this->assertNull($tokenStorage->getToken());
$this->assertSame(2, $counter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
Expand All @@ -33,7 +35,7 @@ class AccessListener implements ListenerInterface

public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager)
{
$this->tokenStorage = $tokenStorage;
$this->tokenStorage = $tokenStorage instanceof UsageTrackingTokenStorageInterface ? $tokenStorage : new UsageTrackingTokenStorage($tokenStorage);
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
$this->authManager = $authManager;
Expand All @@ -47,18 +49,18 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM
*/
public function handle(GetResponseEvent $event)
{
if (null === $token = $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
}

$request = $event->getRequest();

list($attributes) = $this->map->getPatterns($request);

if (null === $attributes) {
if (!$attributes) {
return;
}

if (null === $token = $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
}

if (!$token->isAuthenticated()) {
$token = $this->authManager->authenticate($token);
$this->tokenStorage->setToken($token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

/**
Expand All @@ -33,7 +35,7 @@ class AnonymousAuthenticationListener implements ListenerInterface

public function __construct(TokenStorageInterface $tokenStorage, string $secret, LoggerInterface $logger = null, AuthenticationManagerInterface $authenticationManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->tokenStorage = $tokenStorage instanceof UsageTrackingTokenStorageInterface ? $tokenStorage : new UsageTrackingTokenStorage($tokenStorage);
$this->secret = $secret;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
Expand All @@ -44,7 +46,7 @@ public function __construct(TokenStorageInterface $tokenStorage, string $secret,
*/
public function handle(GetResponseEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
if (null !== $this->tokenStorage->getToken(false)) {
return;
}

Expand Down
Loading