Skip to content

[Security] Add a method in the security helper to ease programmatic logout #41406

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

Merged
merged 3 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Make CSRF validation opt-in
  • Loading branch information
chalasr committed Jul 20, 2022
commit e5e7d5ece408af6df4dc31f7f7891759679e702f
15 changes: 11 additions & 4 deletions src/Symfony/Bundle/SecurityBundle/Security/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ public function login(UserInterface $user, string $authenticatorName = null, str
/**
* Logout the current user by dispatching the LogoutEvent.
*
* @param bool $validateCsrfToken Whether to look for a valid CSRF token based on the `logout` listener configuration
*
* @return Response|null The LogoutEvent's Response if any
*
* @throws LogoutException When $validateCsrfToken is true and the CSRF token is not found or invalid
*/
public function logout(): ?Response
public function logout(bool $validateCsrfToken = true): ?Response
{
/** @var TokenStorageInterface $tokenStorage */
$tokenStorage = $this->container->get('security.token_storage');
Expand All @@ -86,17 +90,20 @@ public function logout(): ?Response
throw new LogicException('Unable to logout as the request is not behind a firewall.');
}

if ($this->container->has('security.csrf.token_manager') && $logoutConfig = $firewallConfig->getLogout()) {
if ($validateCsrfToken) {
if (!$this->container->has('security.csrf.token_manager') || !$logoutConfig = $firewallConfig->getLogout()) {
throw new LogicException(sprintf('Unable to logout with CSRF token validation. Either make sure that CSRF protection is enabled and "logout" is configured on the "%s" firewall, or bypass CSRF token validation explicitly by passing false to the $validateCsrfToken argument of this method.', $firewallConfig->getName()));
}
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $logoutConfig['csrf_parameter']);
if (!\is_string($csrfToken) || false === $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) {
if (!\is_string($csrfToken) || !$this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($logoutConfig['csrf_token_id'], $csrfToken))) {
throw new LogoutException('Invalid CSRF token.');
}
}

$logoutEvent = new LogoutEvent($request, $token);
$this->container->get('security.firewall.event_dispatcher_locator')->get($firewallConfig->getName())->dispatch($logoutEvent);

$tokenStorage->setToken();
$tokenStorage->setToken(null);

return $logoutEvent->getResponse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
use Symfony\Bundle\SecurityBundle\Security\Security;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class SecurityTest extends AbstractWebTestCase
{
Expand All @@ -38,8 +41,10 @@ public function testServiceIsFunctional()
$security = $container->get('functional_test.security.helper');
$this->assertTrue($security->isGranted('ROLE_USER'));
$this->assertSame($token, $security->getToken());
$this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig(new Request()));
$this->assertSame('default', $firewallConfig->getName());
$request = new Request();
$request->server->set('REQUEST_URI', '/main/foo');
$this->assertInstanceOf(FirewallConfig::class, $firewallConfig = $security->getFirewallConfig($request));
$this->assertSame('main', $firewallConfig->getName());
}

/**
Expand Down Expand Up @@ -86,14 +91,14 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
}

/**
* @testWith ["json_login"]
* @testWith ["form_login"]
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
*/
public function testLogin(string $authenticator)
{
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
$client->request('GET', '/welcome');
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' > true]);
static::getContainer()->get(ForceLoginController::class)->authenticator = $authenticator;
$client->request('GET', '/main/force-login');
$response = $client->getResponse();

$this->assertInstanceOf(JsonResponse::class, $response);
Expand All @@ -104,8 +109,10 @@ public function testLogin(string $authenticator)

public function testLogout()
{
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
$client->request('GET', '/force-logout');
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');

$client->request('GET', '/main/force-logout');
$response = $client->getResponse();

$this->assertSame(200, $response->getStatusCode());
Expand All @@ -114,9 +121,39 @@ public function testLogout()
}

public function testLogoutWithCsrf()
{
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml', 'debug' => true]);
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');

// put a csrf token in the storage
/** @var EventDispatcherInterface $eventDispatcher */
$eventDispatcher = static::getContainer()->get(EventDispatcherInterface::class);
$setCsrfToken = function (RequestEvent $event) {
static::getContainer()->get('security.csrf.token_storage')->setToken('logout', 'bar');
$event->setResponse(new Response(''));
};
$eventDispatcher->addListener(KernelEvents::REQUEST, $setCsrfToken);
try {
$client->request('GET', '/'.uniqid('', true));
} finally {
$eventDispatcher->removeListener(KernelEvents::REQUEST, $setCsrfToken);
}

static::getContainer()->get(LogoutController::class)->checkCsrf = true;
$client->request('GET', '/main/force-logout', ['_csrf_token' => 'bar']);
$response = $client->getResponse();

$this->assertSame(200, $response->getStatusCode());
$this->assertNull(static::getContainer()->get('security.helper')->getUser());
$this->assertSame(['message' => 'Logout successful'], json_decode($response->getContent(), true));
}

public function testLogoutBypassCsrf()
{
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config_logout_csrf.yml']);
$client->request('GET', '/force-logout');
$client->loginUser(new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']), 'main');

$client->request('GET', '/main/force-logout');
$response = $client->getResponse();

$this->assertSame(200, $response->getStatusCode());
Expand Down Expand Up @@ -232,17 +269,17 @@ public function eraseCredentials(): void
}
}

class WelcomeController
class ForceLoginController
{
public $authenticator = 'json_login';
public $authenticator = 'form_login';

public function __construct(private Security $security)
{
}

public function welcome()
{
$user = new InMemoryUser('chalasr', '', ['ROLE_USER']);
$user = new InMemoryUser('chalasr', 'the-password', ['ROLE_FOO']);
$this->security->login($user, $this->authenticator);

return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
Expand All @@ -251,18 +288,24 @@ public function welcome()

class LogoutController
{
public function __construct(private Security $security, private ?CsrfTokenManagerInterface $csrfTokenManager = null)
public $checkCsrf = false;

public function __construct(private Security $security)
{
}

public function logout(Request $request)
public function logout(UserInterface $user)
{
$this->security->login(new InMemoryUser('chalasr', '', ['ROLE_USER']), 'json_login', 'default');
if ($this->csrfTokenManager) {
$request->query->set('_csrf_token', (string) $this->csrfTokenManager->getToken('logout'));
}
$this->security->logout();
$this->security->logout($this->checkCsrf);

return new JsonResponse(['message' => 'Logout successful']);
}
}

class LoggedInController
{
public function __invoke(UserInterface $user)
{
return new JsonResponse(['message' => sprintf('Welcome back @%s', $user->getUserIdentifier())]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ imports:
- { resource: ./../config/framework.yml }

services:
# alias the service so we can access it in the tests
functional_test.security.helper:
alias: security.helper
public: true
Expand All @@ -11,30 +10,39 @@ services:
alias: security.token_storage
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController:
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController:
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~

security:
enable_authenticator_manager: true

providers:
in_memory:
main:
memory:
users: []
users:
chalasr: { password: the-password, roles: ['ROLE_FOO'] }
no-role-username: { password: the-password, roles: [] }

firewalls:
default:
json_login:
username_path: user.login
password_path: user.password
main:
pattern: ^/main
form_login:
check_path: /main/login/check
custom_authenticators:
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
provider: main

access_control:
- { path: ^/foo, roles: PUBLIC_ACCESS }
- { path: '^/main/login/check$', roles: IS_AUTHENTICATED_FULLY }
- { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY }
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ imports:
- { resource: ./../config/framework.yml }

services:
# alias the service so we can access it in the tests
functional_test.security.helper:
alias: security.helper
public: true
Expand All @@ -11,32 +10,39 @@ services:
alias: security.token_storage
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController:
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController:
arguments: ['@security.helper', '@security.csrf.token_manager']
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController:
arguments: ['@security.helper']
public: true

Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~

security:
enable_authenticator_manager: true

providers:
in_memory:
main:
memory:
users: []
users:
chalasr: { password: the-password, roles: ['ROLE_FOO'] }
no-role-username: { password: the-password, roles: [] }

firewalls:
default:
json_login:
username_path: user.login
password_path: user.password
logout:
path: /regular-logout
main:
pattern: ^/main
form_login:
check_path: /main/login/check
custom_authenticators:
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
provider: main

access_control:
- { path: ^/foo, roles: PUBLIC_ACCESS }
- { path: '^/main/logged-in$', roles: IS_AUTHENTICATED_FULLY }
- { path: '^/main/force-logout$', roles: IS_AUTHENTICATED_FULLY }
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
welcome:
path: /welcome
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome
force-login:
path: /main/force-login
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\ForceLoginController::welcome

logged-in:
path: /main/logged-in
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LoggedInController

force-logout:
path: /force-logout
path: /main/force-logout
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\LogoutController::logout
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy