Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Security] Add a method in the security helper to ease programmatic login (#40662) #41274

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 2 commits into from
Jul 5, 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
Add functional test & fix reviews
  • Loading branch information
chalasr committed Jul 5, 2022
commit 37efa7291a5dd127cde06cf14407bb5e48308155
41 changes: 23 additions & 18 deletions 41 src/Symfony/Bundle/SecurityBundle/Security/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
/**
* Helper class for commonly-needed security tasks.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Arnaud Frézet <arnaud@larriereguichet.fr>
*
* @final
*/
class Security extends LegacySecurity
{
public function __construct(private ContainerInterface $container, private array $authenticators = [])
public function __construct(private readonly ContainerInterface $container, private readonly array $authenticators = [])
{
parent::__construct($container, false);
}
Expand All @@ -36,14 +40,21 @@ public function getFirewallConfig(Request $request): ?FirewallConfig
return $this->container->get('security.firewall.map')->getFirewallConfig($request);
}

/**
* @param UserInterface $user The user to authenticate
* @param string|null $authenticatorName The authenticator name (e.g. "form_login") or service id (e.g. SomeApiKeyAuthenticator::class) - required only if multiple authenticators are configured
* @param string|null $firewallName The firewall name - required only if multiple firewalls are configured
*/
public function login(UserInterface $user, string $authenticatorName = null, string $firewallName = null): void
{
$request = $this->container->get('request_stack')->getCurrentRequest();
$firewallName ??= $this->getFirewallConfig($request)?->getName();

if (!class_exists(AuthenticatorInterface::class)) {
throw new \LogicException('Security HTTP is missing. Try running "composer require symfony/security-http".');
if (!$firewallName) {
throw new LogicException('Unable to login as the current route is not covered by any firewall.');
}
$authenticator = $this->getAuthenticator($authenticatorName, $firewallName ?? $this->getFirewallName($request));

$authenticator = $this->getAuthenticator($authenticatorName, $firewallName);

$this->container->get('security.user_checker')->checkPreAuth($user);
$this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request);
Expand All @@ -54,39 +65,33 @@ private function getAuthenticator(?string $authenticatorName, string $firewallNa
if (!\array_key_exists($firewallName, $this->authenticators)) {
throw new LogicException(sprintf('No authenticators found for firewall "%s".', $firewallName));
}

/** @var ServiceProviderInterface $firewallAuthenticatorLocator */
$firewallAuthenticatorLocator = $this->authenticators[$firewallName];

if (!$authenticatorName) {
$authenticatorIds = array_keys($firewallAuthenticatorLocator->getProvidedServices());

if (!$authenticatorIds) {
throw new LogicException('No authenticator was found for the firewall "%s".');
throw new LogicException(sprintf('No authenticator was found for the firewall "%s".', $firewallName));
}

if (1 < \count($authenticatorIds)) {
throw new LogicException(sprintf('Too much authenticators were found for the current firewall "%s". You must provide an instance of "%s" to login programmatically. The available authenticators for the firewall "%s" are "%s".', $firewallName, AuthenticatorInterface::class, $firewallName, implode('" ,"', $authenticatorIds)));
}

return $firewallAuthenticatorLocator->get($authenticatorIds[0]);
}
$authenticatorId = 'security.authenticator.'.$authenticatorName.'.'.$firewallName;

if (!$firewallAuthenticatorLocator->has($authenticatorId)) {
throw new LogicException(sprintf('Unable to find an authenticator named "%s" for the firewall "%s". Try to pass a firewall name in the Security::login() method.', $authenticatorName, $firewallName));
if ($firewallAuthenticatorLocator->has($authenticatorName)) {
return $firewallAuthenticatorLocator->get($authenticatorName);
}

return $firewallAuthenticatorLocator->get($authenticatorId);
}

private function getFirewallName(Request $request): string
{
$firewall = $this->container->get('security.firewall.map')->getFirewallConfig($request);
$authenticatorId = 'security.authenticator.'.$authenticatorName.'.'.$firewallName;

if (null === $firewall) {
throw new LogicException('No firewall found as the current route is not covered by any firewall.');
if (!$firewallAuthenticatorLocator->has($authenticatorId)) {
throw new LogicException(sprintf('Unable to find an authenticator named "%s" for the firewall "%s". Available authenticators: "%s".', $authenticatorName, implode('", "', $firewallAuthenticatorLocator->getProvidedServices())));
}

return $firewall->getName();
return $firewallAuthenticatorLocator->get($authenticatorId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;

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\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\InMemoryUser;
Expand Down Expand Up @@ -81,6 +83,22 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
],
];
}

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

$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true));
}
}

final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface
Expand Down Expand Up @@ -189,3 +207,20 @@ public function eraseCredentials(): void
{
}
}

class WelcomeController
{
public $authenticator = 'json_login';

public function __construct(private Security $security)
{
}

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

return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ services:
alias: security.token_storage
public: true

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

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

security:
enable_authenticator_manager: true
providers:
Expand All @@ -20,3 +26,11 @@ security:

firewalls:
default:
json_login:
username_path: user.login
password_path: user.password
custom_authenticators:
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'

access_control:
- { path: ^/foo, roles: PUBLIC_ACCESS }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
welcome:
path: /welcome
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome }
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
use Symfony\Bundle\SecurityBundle\Security\Security;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Contracts\Service\ServiceProviderInterface;

class SecurityTest extends TestCase
{
Expand Down Expand Up @@ -111,6 +117,52 @@ public function getFirewallConfigTests()
yield [$request, new FirewallConfig('main', 'acme_user_checker')];
}

public function testAutoLogin()
{
$request = new Request();
$authenticator = $this->createMock(AuthenticatorInterface::class);
$requestStack = $this->createMock(RequestStack::class);
$firewallMap = $this->createMock(FirewallMap::class);
$firewall = new FirewallConfig('main', 'main');
$userAuthenticator = $this->createMock(UserAuthenticatorInterface::class);
$user = $this->createMock(UserInterface::class);
$userChecker = $this->createMock(UserCheckerInterface::class);

$container = $this->createMock(ContainerInterface::class);
$container
->expects($this->atLeastOnce())
->method('get')
->willReturnMap([
['request_stack', $requestStack],
['security.firewall.map', $firewallMap],
['security.user_authenticator', $userAuthenticator],
['security.user_checker', $userChecker],
])
;

$requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request);
$firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall);
$userAuthenticator->expects($this->once())->method('authenticateUser')->with($user, $authenticator, $request);
$userChecker->expects($this->once())->method('checkPreAuth')->with($user);

$firewallAuthenticatorLocator = $this->createMock(ServiceProviderInterface::class);
$firewallAuthenticatorLocator
->expects($this->once())
->method('getProvidedServices')
->willReturn(['security.authenticator.custom.dev' => $authenticator])
;
$firewallAuthenticatorLocator
->expects($this->once())
->method('get')
->with('security.authenticator.custom.dev')
->willReturn($authenticator)
;

$security = new Security($container, ['main' => $firewallAuthenticatorLocator]);

$security->login($user);
}

private function createContainer(string $serviceId, object $serviceObject): ContainerInterface
{
return new ServiceLocator([$serviceId => fn () => $serviceObject]);
Expand Down
6 changes: 0 additions & 6 deletions 6 src/Symfony/Component/Security/Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ CHANGELOG

* Deprecate the `Security` class, use `Symfony\Bundle\SecurityBundle\Security\Security` instead

6.1
---

* Add `Security::login()` to login programmatically


6.0
---

Expand Down
82 changes: 3 additions & 79 deletions 82 src/Symfony/Component/Security/Core/Tests/SecurityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,12 @@

use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Contracts\Service\ServiceProviderInterface;

/**
* @group legacy
Expand All @@ -45,7 +36,7 @@ public function testGetToken()

$container = $this->createContainer('security.token_storage', $tokenStorage);

$security = new Security($container, []);
$security = new Security($container);
$this->assertSame($token, $security->getToken());
}

Expand All @@ -66,7 +57,7 @@ public function testGetUser($userInToken, $expectedUser)

$container = $this->createContainer('security.token_storage', $tokenStorage);

$security = new Security($container, []);
$security = new Security($container);
$this->assertSame($expectedUser, $security->getUser());
}

Expand All @@ -89,77 +80,10 @@ public function testIsGranted()

$container = $this->createContainer('security.authorization_checker', $authorizationChecker);

$security = new Security($container, []);
$security = new Security($container);
$this->assertTrue($security->isGranted('SOME_ATTRIBUTE', 'SOME_SUBJECT'));
}

public function testAutoLogin()
{
$request = new Request();
$authenticator = $this->createMock(AuthenticatorInterface::class);
$requestStack = $this->createMock(RequestStack::class);
$firewallMap = $this->createMock(FirewallMap::class);
$firewall = new FirewallConfig('main', 'main');
$userAuthenticator = $this->createMock(UserAuthenticatorInterface::class);
$user = $this->createMock(UserInterface::class);
$userChecker = $this->createMock(UserCheckerInterface::class);

$container = $this->createMock(ContainerInterface::class);
$container
->expects($this->atLeastOnce())
->method('get')
->willReturnMap([
['request_stack', $requestStack],
['security.firewall.map', $firewallMap],
['security.user_authenticator', $userAuthenticator],
['security.user_checker', $userChecker],
])
;

$requestStack
->expects($this->once())
->method('getCurrentRequest')
->willReturn($request)
;

$firewallMap
->expects($this->once())
->method('getFirewallConfig')
->willReturn($firewall)
;
$userAuthenticator
->expects($this->once())
->method('authenticateUser')
->with($user, $authenticator, $request)
;
$userChecker
->expects($this->once())
->method('checkPreAuth')
->with($user)
;

$firewallAuthenticatorLocator = $this->createMock(ServiceProviderInterface::class);
$firewallAuthenticatorLocator
->expects($this->once())
->method('getProvidedServices')
->willReturn([
'security.authenticator.custom.dev' => $authenticator,
])
;
$firewallAuthenticatorLocator
->expects($this->once())
->method('get')
->with('security.authenticator.custom.dev')
->willReturn($authenticator)
;

$security = new Security($container, [
'main' => $firewallAuthenticatorLocator,
]);

$security->login($user);
}

private function createContainer($serviceId, $serviceObject)
{
$container = $this->createMock(ContainerInterface::class);
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.