Skip to content

Commit 7714855

Browse files
committed
Add functional test & fix reviews
1 parent 80658d3 commit 7714855

File tree

5 files changed

+127
-18
lines changed

5 files changed

+127
-18
lines changed

Security/Security.php

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222
/**
2323
* Helper class for commonly-needed security tasks.
2424
*
25+
* @author Ryan Weaver <ryan@symfonycasts.com>
26+
* @author Robin Chalas <robin.chalas@gmail.com>
27+
* @author Arnaud Frézet <arnaud@larriereguichet.fr>
28+
*
2529
* @final
2630
*/
2731
class Security extends LegacySecurity
2832
{
29-
public function __construct(private ContainerInterface $container, private array $authenticators = [])
33+
public function __construct(private readonly ContainerInterface $container, private readonly array $authenticators = [])
3034
{
3135
parent::__construct($container, false);
3236
}
@@ -36,14 +40,21 @@ public function getFirewallConfig(Request $request): ?FirewallConfig
3640
return $this->container->get('security.firewall.map')->getFirewallConfig($request);
3741
}
3842

43+
/**
44+
* @param UserInterface $user The user to authenticate
45+
* @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
46+
* @param string|null $firewallName The firewall name - required only if multiple firewalls are configured
47+
*/
3948
public function login(UserInterface $user, string $authenticatorName = null, string $firewallName = null): void
4049
{
4150
$request = $this->container->get('request_stack')->getCurrentRequest();
51+
$firewallName ??= $this->getFirewallConfig($request)?->getName();
4252

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

4859
$this->container->get('security.user_checker')->checkPreAuth($user);
4960
$this->container->get('security.user_authenticator')->authenticateUser($user, $authenticator, $request);
@@ -54,39 +65,33 @@ private function getAuthenticator(?string $authenticatorName, string $firewallNa
5465
if (!\array_key_exists($firewallName, $this->authenticators)) {
5566
throw new LogicException(sprintf('No authenticators found for firewall "%s".', $firewallName));
5667
}
68+
5769
/** @var ServiceProviderInterface $firewallAuthenticatorLocator */
5870
$firewallAuthenticatorLocator = $this->authenticators[$firewallName];
5971

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

6375
if (!$authenticatorIds) {
64-
throw new LogicException('No authenticator was found for the firewall "%s".');
76+
throw new LogicException(sprintf('No authenticator was found for the firewall "%s".', $firewallName));
6577
}
66-
6778
if (1 < \count($authenticatorIds)) {
6879
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)));
6980
}
7081

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

75-
if (!$firewallAuthenticatorLocator->has($authenticatorId)) {
76-
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));
85+
if ($firewallAuthenticatorLocator->has($authenticatorName)) {
86+
return $firewallAuthenticatorLocator->get($authenticatorName);
7787
}
7888

79-
return $firewallAuthenticatorLocator->get($authenticatorId);
80-
}
81-
82-
private function getFirewallName(Request $request): string
83-
{
84-
$firewall = $this->container->get('security.firewall.map')->getFirewallConfig($request);
89+
$authenticatorId = 'security.authenticator.'.$authenticatorName.'.'.$firewallName;
8590

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

90-
return $firewall->getName();
95+
return $firewallAuthenticatorLocator->get($authenticatorId);
9196
}
9297
}

Tests/Functional/SecurityTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
1313

1414
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
15+
use Symfony\Bundle\SecurityBundle\Security\Security;
1516
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider;
17+
use Symfony\Component\HttpFoundation\JsonResponse;
1618
use Symfony\Component\HttpFoundation\Request;
1719
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1820
use Symfony\Component\Security\Core\User\InMemoryUser;
@@ -81,6 +83,22 @@ public function userWillBeMarkedAsChangedIfRolesHasChangedProvider()
8183
],
8284
];
8385
}
86+
87+
/**
88+
* @testWith ["json_login"]
89+
* ["Symfony\\Bundle\\SecurityBundle\\Tests\\Functional\\Bundle\\AuthenticatorBundle\\ApiAuthenticator"]
90+
*/
91+
public function testLoginWithBuiltInAuthenticator(string $authenticator)
92+
{
93+
$client = $this->createClient(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml', 'debug' => true]);
94+
static::getContainer()->get(WelcomeController::class)->authenticator = $authenticator;
95+
$client->request('GET', '/welcome');
96+
$response = $client->getResponse();
97+
98+
$this->assertInstanceOf(JsonResponse::class, $response);
99+
$this->assertSame(200, $response->getStatusCode());
100+
$this->assertSame(['message' => 'Welcome @chalasr!'], json_decode($response->getContent(), true));
101+
}
84102
}
85103

86104
final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface
@@ -189,3 +207,20 @@ public function eraseCredentials(): void
189207
{
190208
}
191209
}
210+
211+
class WelcomeController
212+
{
213+
public $authenticator = 'json_login';
214+
215+
public function __construct(private Security $security)
216+
{
217+
}
218+
219+
public function welcome()
220+
{
221+
$user = new InMemoryUser('chalasr', '', ['ROLE_USER']);
222+
$this->security->login($user, $this->authenticator);
223+
224+
return new JsonResponse(['message' => sprintf('Welcome @%s!', $this->security->getUser()->getUserIdentifier())]);
225+
}
226+
}

Tests/Functional/app/SecurityHelper/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ services:
1111
alias: security.token_storage
1212
public: true
1313

14+
Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController:
15+
arguments: ['@security.helper']
16+
public: true
17+
18+
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~
19+
1420
security:
1521
enable_authenticator_manager: true
1622
providers:
@@ -20,3 +26,11 @@ security:
2026

2127
firewalls:
2228
default:
29+
json_login:
30+
username_path: user.login
31+
password_path: user.password
32+
custom_authenticators:
33+
- 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator'
34+
35+
access_control:
36+
- { path: ^/foo, roles: PUBLIC_ACCESS }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
welcome:
2+
path: /welcome
3+
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\WelcomeController::welcome }

Tests/Security/SecurityTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
use Symfony\Bundle\SecurityBundle\Security\Security;
1919
use Symfony\Component\DependencyInjection\ServiceLocator;
2020
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpFoundation\RequestStack;
2122
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2223
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2324
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2425
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
2526
use Symfony\Component\Security\Core\User\InMemoryUser;
27+
use Symfony\Component\Security\Core\User\UserCheckerInterface;
28+
use Symfony\Component\Security\Core\User\UserInterface;
29+
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
30+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
31+
use Symfony\Contracts\Service\ServiceProviderInterface;
2632

2733
class SecurityTest extends TestCase
2834
{
@@ -111,6 +117,52 @@ public function getFirewallConfigTests()
111117
yield [$request, new FirewallConfig('main', 'acme_user_checker')];
112118
}
113119

120+
public function testAutoLogin()
121+
{
122+
$request = new Request();
123+
$authenticator = $this->createMock(AuthenticatorInterface::class);
124+
$requestStack = $this->createMock(RequestStack::class);
125+
$firewallMap = $this->createMock(FirewallMap::class);
126+
$firewall = new FirewallConfig('main', 'main');
127+
$userAuthenticator = $this->createMock(UserAuthenticatorInterface::class);
128+
$user = $this->createMock(UserInterface::class);
129+
$userChecker = $this->createMock(UserCheckerInterface::class);
130+
131+
$container = $this->createMock(ContainerInterface::class);
132+
$container
133+
->expects($this->atLeastOnce())
134+
->method('get')
135+
->willReturnMap([
136+
['request_stack', $requestStack],
137+
['security.firewall.map', $firewallMap],
138+
['security.user_authenticator', $userAuthenticator],
139+
['security.user_checker', $userChecker],
140+
])
141+
;
142+
143+
$requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request);
144+
$firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall);
145+
$userAuthenticator->expects($this->once())->method('authenticateUser')->with($user, $authenticator, $request);
146+
$userChecker->expects($this->once())->method('checkPreAuth')->with($user);
147+
148+
$firewallAuthenticatorLocator = $this->createMock(ServiceProviderInterface::class);
149+
$firewallAuthenticatorLocator
150+
->expects($this->once())
151+
->method('getProvidedServices')
152+
->willReturn(['security.authenticator.custom.dev' => $authenticator])
153+
;
154+
$firewallAuthenticatorLocator
155+
->expects($this->once())
156+
->method('get')
157+
->with('security.authenticator.custom.dev')
158+
->willReturn($authenticator)
159+
;
160+
161+
$security = new Security($container, ['main' => $firewallAuthenticatorLocator]);
162+
163+
$security->login($user);
164+
}
165+
114166
private function createContainer(string $serviceId, object $serviceObject): ContainerInterface
115167
{
116168
return new ServiceLocator([$serviceId => fn () => $serviceObject]);

0 commit comments

Comments
 (0)