Skip to content

Commit 388df31

Browse files
committed
form login : guess if encoder is needed
1 parent c44f878 commit 388df31

File tree

16 files changed

+712
-51
lines changed

16 files changed

+712
-51
lines changed

src/Maker/MakeAuthenticator.php

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -154,59 +154,26 @@ function ($answer) {
154154

155155
$command->addArgument('user-class', InputArgument::OPTIONAL);
156156
$userClass = $interactiveSecurityHelper->guessUserClass($io, $securityData['security']['providers']);
157-
if (0 !== strpos($userClass, '\\')) {
158-
$userClass = '\\'.$userClass;
159-
}
160157
$input->setArgument('user-class', $userClass);
161158
}
162159
}
163160

164161
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
165162
{
166-
// generate authenticator class
167-
if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $input->getArgument('authenticator-type')) {
168-
$generator->generateClass(
169-
$input->getArgument('authenticator-class'),
170-
'authenticator/EmptyAuthenticator.tpl.php',
171-
[]
172-
);
173-
} elseif ($this->doctrineHelper->isClassAMappedEntity($input->getArgument('user-class'))) {
174-
$userClassNameDetails = $generator->createClassNameDetails(
175-
$input->getArgument('user-class'),
176-
'Entity\\'
177-
);
178-
179-
$generator->generateClass(
180-
$input->getArgument('authenticator-class'),
181-
'authenticator/LoginFormEntityAuthenticator.tpl.php',
182-
[
183-
'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
184-
'user_class_name' => $userClassNameDetails->getShortName(),
185-
]
186-
);
187-
} else {
188-
$generator->generateClass(
189-
$input->getArgument('authenticator-class'),
190-
'authenticator/LoginFormNotEntityAuthenticator.tpl.php',
191-
[]
192-
);
193-
}
163+
$this->generateAuthenticatorClass($input);
194164

195165
// update security.yaml with guard config
196166
$securityYamlUpdated = false;
197-
$path = 'config/packages/security.yaml';
198-
if ($this->fileManager->fileExists($path)) {
199-
try {
200-
$newYaml = $this->configUpdater->updateForAuthenticator(
201-
$this->fileManager->getFileContents($path),
202-
$input->getOption('firewall-name'),
203-
$input->getOption('entry-point'),
204-
$input->getArgument('authenticator-class')
205-
);
206-
$generator->dumpFile($path, $newYaml);
207-
$securityYamlUpdated = true;
208-
} catch (YamlManipulationFailedException $e) {
209-
}
167+
try {
168+
$newYaml = $this->configUpdater->updateForAuthenticator(
169+
$this->fileManager->getFileContents($path = 'config/packages/security.yaml'),
170+
$input->getOption('firewall-name'),
171+
$input->getOption('entry-point'),
172+
$input->getArgument('authenticator-class')
173+
);
174+
$generator->dumpFile($path, $newYaml);
175+
$securityYamlUpdated = true;
176+
} catch (YamlManipulationFailedException $e) {
210177
}
211178

212179
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
@@ -230,6 +197,52 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
230197
$io->text($text);
231198
}
232199

200+
private function generateAuthenticatorClass(InputInterface $input)
201+
{
202+
// generate authenticator class
203+
if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $input->getArgument('authenticator-type')) {
204+
$this->generator->generateClass(
205+
$input->getArgument('authenticator-class'),
206+
'authenticator/EmptyAuthenticator.tpl.php',
207+
[]
208+
);
209+
} else {
210+
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml'));
211+
$securityData = $manipulator->getData();
212+
213+
$userNeedsEncoder = false;
214+
if (isset($securityData['security']['encoders']) && $securityData['security']['encoders']) {
215+
foreach ($securityData['security']['encoders'] as $userClassWithEncoder => $encoder) {
216+
if ($input->getArgument('user-class') === $userClassWithEncoder || is_subclass_of($input->getArgument('user-class'), $userClassWithEncoder)) {
217+
$userNeedsEncoder = true;
218+
}
219+
}
220+
}
221+
222+
if ($this->doctrineHelper->isClassAMappedEntity($input->getArgument('user-class'))) {
223+
$userClassNameDetails = $this->generator->createClassNameDetails(
224+
'\\'.$input->getArgument('user-class'),
225+
'Entity\\'
226+
);
227+
228+
$this->generator->generateClass(
229+
$input->getArgument('authenticator-class'),
230+
$userNeedsEncoder ? 'authenticator/LoginFormEntityAuthenticator.tpl.php' : 'authenticator/LoginFormEntityAuthenticatorNoEncoder.tpl.php',
231+
[
232+
'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
233+
'user_class_name' => $userClassNameDetails->getShortName(),
234+
]
235+
);
236+
} else {
237+
$this->generator->generateClass(
238+
$input->getArgument('authenticator-class'),
239+
$userNeedsEncoder ? 'authenticator/LoginFormNotEntityAuthenticator.tpl.php' : 'authenticator/LoginFormNotEntityAuthenticatorNoEncoder.tpl.php',
240+
[]
241+
);
242+
}
243+
}
244+
}
245+
233246
private function generateFormLoginFiles(InputInterface $input)
234247
{
235248
$controllerClassNameDetails = $this->generator->createClassNameDetails(
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace ?>;
4+
5+
use <?= $user_fully_qualified_class_name ?>;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Symfony\Component\HttpFoundation\RedirectResponse;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\Routing\RouterInterface;
10+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
11+
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
12+
use Symfony\Component\Security\Core\Security;
13+
use Symfony\Component\Security\Core\User\UserInterface;
14+
use Symfony\Component\Security\Core\User\UserProviderInterface;
15+
use Symfony\Component\Security\Csrf\CsrfToken;
16+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
17+
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
18+
use Symfony\Component\Security\Http\Util\TargetPathTrait;
19+
20+
class <?= $class_name; ?> extends AbstractFormLoginAuthenticator
21+
{
22+
use TargetPathTrait;
23+
24+
private $entityManager;
25+
private $router;
26+
private $csrfTokenManager;
27+
28+
public function __construct(EntityManagerInterface $entityManager, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager)
29+
{
30+
$this->entityManager = $entityManager;
31+
$this->router = $router;
32+
$this->csrfTokenManager = $csrfTokenManager;
33+
}
34+
35+
public function supports(Request $request)
36+
{
37+
return 'app_login' === $request->attributes->get('_route')
38+
&& $request->isMethod('POST');
39+
}
40+
41+
public function getCredentials(Request $request)
42+
{
43+
$credentials = [
44+
'email' => $request->request->get('email'),
45+
'password' => $request->request->get('password'),
46+
'csrf_token' => $request->request->get('_csrf_token'),
47+
];
48+
$request->getSession()->set(
49+
Security::LAST_USERNAME,
50+
$credentials['email']
51+
);
52+
53+
return $credentials;
54+
}
55+
56+
public function getUser($credentials, UserProviderInterface $userProvider)
57+
{
58+
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
59+
if (!$this->csrfTokenManager->isTokenValid($token)) {
60+
throw new InvalidCsrfTokenException();
61+
}
62+
63+
return $this->entityManager->getRepository(<?= $user_class_name ?>::class)->findOneBy(['email' => $credentials['email']]);
64+
}
65+
66+
public function checkCredentials($credentials, UserInterface $user)
67+
{
68+
return true;
69+
}
70+
71+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
72+
{
73+
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
74+
return new RedirectResponse($targetPath);
75+
}
76+
77+
throw new \Exception('TODO: provide a valid redirection inside '.__FILE__);
78+
}
79+
80+
protected function getLoginUrl()
81+
{
82+
return $this->router->generate('app_login');
83+
}
84+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace ?>;
4+
5+
use Symfony\Component\HttpFoundation\RedirectResponse;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\Routing\RouterInterface;
8+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
9+
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
10+
use Symfony\Component\Security\Core\Security;
11+
use Symfony\Component\Security\Core\User\UserInterface;
12+
use Symfony\Component\Security\Core\User\UserProviderInterface;
13+
use Symfony\Component\Security\Csrf\CsrfToken;
14+
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
15+
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
16+
use Symfony\Component\Security\Http\Util\TargetPathTrait;
17+
18+
class <?= $class_name; ?> extends AbstractFormLoginAuthenticator
19+
{
20+
use TargetPathTrait;
21+
22+
private $router;
23+
private $csrfTokenManager;
24+
25+
public function __construct(RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager)
26+
{
27+
$this->router = $router;
28+
$this->csrfTokenManager = $csrfTokenManager;
29+
}
30+
31+
public function supports(Request $request)
32+
{
33+
return 'app_login' === $request->attributes->get('_route')
34+
&& $request->isMethod('POST');
35+
}
36+
37+
public function getCredentials(Request $request)
38+
{
39+
$credentials = [
40+
'email' => $request->request->get('email'),
41+
'password' => $request->request->get('password'),
42+
'csrf_token' => $request->request->get('_csrf_token'),
43+
];
44+
$request->getSession()->set(
45+
Security::LAST_USERNAME,
46+
$credentials['email']
47+
);
48+
49+
return $credentials;
50+
}
51+
52+
public function getUser($credentials, UserProviderInterface $userProvider)
53+
{
54+
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
55+
if (!$this->csrfTokenManager->isTokenValid($token)) {
56+
throw new InvalidCsrfTokenException();
57+
}
58+
59+
// Load / create our user however you need.
60+
// You can do this by calling the user provider, or with custom logic here.
61+
return $userProvider->loadUserByUsername($credentials['email']);
62+
}
63+
64+
public function checkCredentials($credentials, UserInterface $user)
65+
{
66+
return true;
67+
}
68+
69+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
70+
{
71+
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
72+
return new RedirectResponse($targetPath);
73+
}
74+
75+
throw new \Exception('TODO: provide a valid redirection inside '.__FILE__);
76+
}
77+
78+
protected function getLoginUrl()
79+
{
80+
return $this->router->generate('app_login');
81+
}
82+
}

tests/Maker/FunctionalTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,25 @@ function (string $output, string $directory) {
420420
),
421421
];
422422

423+
yield 'auth_login_form_user_entity_no_encoder' => [
424+
MakerTestDetails::createTest(
425+
$this->getMakerInstance(MakeAuthenticator::class),
426+
[
427+
// authenticator type => login-form
428+
1,
429+
// class name
430+
'AppCustomAuthenticator',
431+
// controller name
432+
'SecurityController',
433+
]
434+
)
435+
->addExtraDependencies('doctrine')
436+
->addExtraDependencies('twig')
437+
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeAuthenticatorLoginFormUserEntityNoEncoder')
438+
->configureDatabase()
439+
->updateSchemaAfterCommand(),
440+
];
441+
423442
yield 'auth_login_form_user_not_entity' => [
424443
MakerTestDetails::createTest(
425444
$this->getMakerInstance(MakeAuthenticator::class),
@@ -440,6 +459,26 @@ function (string $output, string $directory) {
440459
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeAuthenticatorLoginFormUserNotEntity'),
441460
];
442461

462+
yield 'auth_login_form_user_not_entity_no_encoder' => [
463+
MakerTestDetails::createTest(
464+
$this->getMakerInstance(MakeAuthenticator::class),
465+
[
466+
// authenticator type => login-form
467+
1,
468+
// class name
469+
'AppCustomAuthenticator',
470+
// controller name
471+
'SecurityController',
472+
// user class
473+
'App\Security\User',
474+
]
475+
)
476+
->addExtraDependencies('twig')
477+
->addExtraDependencies('doctrine/annotations')
478+
->addExtraDependencies('symfony/form')
479+
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeAuthenticatorLoginFormUserNotEntityNoEncoder'),
480+
];
481+
443482
yield 'auth_login_form_existing_controller' => [
444483
MakerTestDetails::createTest(
445484
$this->getMakerInstance(MakeAuthenticator::class),

tests/Security/InteractiveSecurityHelperTest.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,13 @@ public function getEntryPointTests()
124124
/**
125125
* @dataProvider getUserClassTests
126126
*/
127-
public function testGuessUserClass(array $securityData, string $expectedUserClass, bool $userClassAutomaticallyGuessed, string $providedClass = '')
127+
public function testGuessUserClass(array $securityData, string $expectedUserClass, bool $userClassAutomaticallyGuessed)
128128
{
129129
/** @var SymfonyStyle|\PHPUnit_Framework_MockObject_MockObject $io */
130130
$io = $this->createMock(SymfonyStyle::class);
131131
$io->expects($this->exactly(true === $userClassAutomaticallyGuessed ? 0 : 1))
132132
->method('ask')
133-
->willReturn($providedClass);
133+
->willReturn($expectedUserClass);
134134

135135
$helper = new InteractiveSecurityHelper();
136136
$this->assertEquals(
@@ -143,22 +143,20 @@ public function getUserClassTests()
143143
{
144144
yield 'user_from_provider' => [
145145
['app_provider' => ['entity' => ['class' => 'App\\Entity\\User']]],
146-
'\\App\\Entity\\User',
146+
'App\\Entity\\User',
147147
true,
148148
];
149149

150150
yield 'multiple_providers' => [
151151
['provider_1' => ['id' => 'app.provider_1'], 'provider_2' => ['id' => 'app.provider_2']],
152-
'\\App\\Entity\\User',
152+
'App\\Entity\\User',
153153
false,
154-
'\\App\\Entity\\User'
155154
];
156155

157156
yield 'no_provider' => [
158157
[[]],
159-
'\\App\\Entity\\User',
158+
'App\\Entity\\User',
160159
false,
161-
'App\\Entity\\User'
162160
];
163161
}
164162
}

0 commit comments

Comments
 (0)