Skip to content

Commit fba06d8

Browse files
committed
feature #736 keeping aliens at bay with maker + new security 5.2 features (jrushlow)
This PR was squashed before being merged into the 1.0-dev branch. Discussion ---------- keeping aliens at bay with maker + new security 5.2 features Hello Security51! Allows `make:auth` to take advantage of the new security features that were introduced. Starting in Symfony `5.2` when you run `make:auth` MakerBundle will automatically check if you have set: ``` security: enable_authenticator_manager: true ``` If so, MakerBundle will generate the required classes to authenticate users leveraging the new Authenticators. A positive side effect of this PR, all templates can check `$use_typed_properties` to determine if the host is capable of utilizing typed properties that were introduced in PHP 7.4. e.g. - ``` private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator; ``` Internally, we've added the `PhpCompatUtil::canUseTypedProperties()` method that is called anytime a Maker needs to generate a twig template. We then inject `$use_typed_properties` into all templates so the developer can add typed properties to a generated template without having to worry about a bunch of behind the scenes logic. Commits ------- 820ee25 keeping aliens at bay with maker + new security 5.2 features
2 parents dd34e35 + 820ee25 commit fba06d8

File tree

35 files changed

+828
-53
lines changed

35 files changed

+828
-53
lines changed

src/Generator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ private function addOperation(string $targetPath, string $templateName, array $v
168168

169169
$variables['relative_path'] = $this->fileManager->relativizePath($targetPath);
170170
$variables['use_attributes'] = $this->phpCompatUtil->canUseAttributes();
171+
$variables['use_typed_properties'] = $this->phpCompatUtil->canUseTypedProperties();
171172

172173
$templatePath = $templateName;
173174
if (!file_exists($templatePath)) {

src/Maker/MakeAuthenticator.php

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
use Symfony\Component\Form\Form;
3737
use Symfony\Component\HttpKernel\Kernel;
3838
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
39+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
3940
use Symfony\Component\Yaml\Yaml;
4041

4142
/**
42-
* @author Ryan Weaver <[email protected]>
43+
* @author Ryan Weaver <[email protected]>
44+
* @author Jesse Rushlow <[email protected]>
4345
*
4446
* @internal
4547
*/
@@ -56,6 +58,8 @@ final class MakeAuthenticator extends AbstractMaker
5658

5759
private $doctrineHelper;
5860

61+
private $useSecurity52 = false;
62+
5963
public function __construct(FileManager $fileManager, SecurityConfigUpdater $configUpdater, Generator $generator, DoctrineHelper $doctrineHelper)
6064
{
6165
$this->fileManager = $fileManager;
@@ -84,6 +88,15 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
8488
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
8589
$securityData = $manipulator->getData();
8690

91+
// Determine if we should use new security features introduced in Symfony 5.2
92+
if ($securityData['security']['enable_authenticator_manager'] ?? false) {
93+
$this->useSecurity52 = true;
94+
}
95+
96+
if ($this->useSecurity52 && !class_exists(UserBadge::class)) {
97+
throw new RuntimeCommandException('MakerBundle does not support generating authenticators using the new authenticator system before symfony/security-bundle 5.2. Please upgrade to 5.2 and try again.');
98+
}
99+
87100
// authenticator type
88101
$authenticatorTypeValues = [
89102
'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR,
@@ -138,10 +151,13 @@ function ($answer) {
138151
$input->setOption('firewall-name', $firewallName = $interactiveSecurityHelper->guessFirewallName($io, $securityData));
139152

140153
$command->addOption('entry-point', null, InputOption::VALUE_OPTIONAL);
141-
$input->setOption(
142-
'entry-point',
143-
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $input->getArgument('authenticator-class'), $firewallName)
144-
);
154+
155+
if (!$this->useSecurity52) {
156+
$input->setOption(
157+
'entry-point',
158+
$interactiveSecurityHelper->guessEntryPoint($io, $securityData, $input->getArgument('authenticator-class'), $firewallName)
159+
);
160+
}
145161

146162
if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
147163
$command->addArgument('controller-class', InputArgument::REQUIRED);
@@ -192,13 +208,21 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
192208

193209
// update security.yaml with guard config
194210
$securityYamlUpdated = false;
211+
212+
$entryPoint = $input->getOption('entry-point');
213+
214+
if ($this->useSecurity52 && self::AUTH_TYPE_FORM_LOGIN !== $input->getArgument('authenticator-type')) {
215+
$entryPoint = false;
216+
}
217+
195218
try {
196219
$newYaml = $this->configUpdater->updateForAuthenticator(
197220
$this->fileManager->getFileContents($path = 'config/packages/security.yaml'),
198221
$input->getOption('firewall-name'),
199-
$input->getOption('entry-point'),
222+
$entryPoint,
200223
$input->getArgument('authenticator-class'),
201-
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
224+
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
225+
$this->useSecurity52
202226
);
203227
$generator->dumpFile($path, $newYaml);
204228
$securityYamlUpdated = true;
@@ -235,10 +259,8 @@ private function generateAuthenticatorClass(array $securityData, string $authent
235259
if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $authenticatorType) {
236260
$this->generator->generateClass(
237261
$authenticatorClass,
238-
'authenticator/EmptyAuthenticator.tpl.php',
239-
[
240-
'provider_key_type_hint' => $this->providerKeyTypeHint(),
241-
]
262+
sprintf('authenticator/%sEmptyAuthenticator.tpl.php', $this->useSecurity52 ? 'Security52' : ''),
263+
['provider_key_type_hint' => $this->providerKeyTypeHint()]
242264
);
243265

244266
return;
@@ -251,12 +273,13 @@ private function generateAuthenticatorClass(array $securityData, string $authent
251273

252274
$this->generator->generateClass(
253275
$authenticatorClass,
254-
'authenticator/LoginFormAuthenticator.tpl.php',
276+
sprintf('authenticator/%sLoginFormAuthenticator.tpl.php', $this->useSecurity52 ? 'Security52' : ''),
255277
[
256278
'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
257279
'user_class_name' => $userClassNameDetails->getShortName(),
258280
'username_field' => $userNameField,
259281
'username_field_label' => Str::asHumanWords($userNameField),
282+
'username_field_var' => Str::asCamelCase($userNameField),
260283
'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass),
261284
'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
262285
'provider_key_type_hint' => $this->providerKeyTypeHint(),
@@ -322,7 +345,8 @@ private function generateNextMessage(bool $securityYamlUpdated, string $authenti
322345
'main',
323346
null,
324347
$authenticatorClass,
325-
$logoutSetup
348+
$logoutSetup,
349+
$this->useSecurity52
326350
);
327351
$nextTexts[] = '- Your <info>security.yaml</info> could not be updated automatically. You\'ll need to add the following config manually:\n\n'.$yamlExample;
328352
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php echo "<?php\n" ?>
2+
3+
namespace <?php echo $namespace ?>;
4+
5+
use Symfony\Component\HttpFoundation\Request;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
8+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
9+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
10+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
11+
12+
class <?php echo $class_name ?> extends AbstractAuthenticator
13+
{
14+
public function supports(Request $request): ?bool
15+
{
16+
// TODO: Implement supports() method.
17+
}
18+
19+
public function authenticate(Request $request): PassportInterface
20+
{
21+
// TODO: Implement authenticate() method.
22+
}
23+
24+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
25+
{
26+
// TODO: Implement onAuthenticationSuccess() method.
27+
}
28+
29+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
30+
{
31+
// TODO: Implement onAuthenticationFailure() method.
32+
}
33+
34+
// public function start(Request $request, AuthenticationException $authException = null): Response
35+
// {
36+
// /*
37+
// * If you would like this class to control what happens when an anonymous user accesses a
38+
// * protected page (e.g. redirect to /login), uncomment this method and make this class
39+
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntrypointInterface.
40+
// *
41+
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
42+
// */
43+
// }
44+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?= "<?php\n" ?>
2+
3+
namespace <?= $namespace ?>;
4+
5+
use Symfony\Component\HttpFoundation\RedirectResponse;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
9+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
10+
use Symfony\Component\Security\Core\Security;
11+
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
12+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
13+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
14+
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
15+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
16+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
17+
use Symfony\Component\Security\Http\Util\TargetPathTrait;
18+
19+
class <?= $class_name; ?> extends AbstractLoginFormAuthenticator
20+
{
21+
use TargetPathTrait;
22+
23+
public const LOGIN_ROUTE = 'app_login';
24+
25+
private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator;
26+
27+
public function __construct(UrlGeneratorInterface $urlGenerator)
28+
{
29+
$this->urlGenerator = $urlGenerator;
30+
}
31+
32+
public function authenticate(Request $request): PassportInterface
33+
{
34+
$<?= $username_field_var ?> = $request->request->get('<?= $username_field ?>', '');
35+
36+
$request->getSession()->set(Security::LAST_USERNAME, $<?= $username_field_var ?>);
37+
38+
return new Passport(
39+
new UserBadge($<?= $username_field_var ?>),
40+
new PasswordCredentials($request->request->get('password', '')),
41+
[
42+
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
43+
]
44+
);
45+
}
46+
47+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
48+
{
49+
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
50+
return new RedirectResponse($targetPath);
51+
}
52+
53+
// For example:
54+
//return new RedirectResponse($this->urlGenerator->generate('some_route'));
55+
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
56+
}
57+
58+
protected function getLoginUrl(Request $request): string
59+
{
60+
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
61+
}
62+
}

src/Security/SecurityConfigUpdater.php

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,39 @@
1212
namespace Symfony\Bundle\MakerBundle\Security;
1313

1414
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
15+
use Symfony\Component\HttpKernel\Log\Logger;
1516
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
1617

1718
/**
19+
* @author Ryan Weaver <[email protected]>
20+
* @author Jesse Rushlow <[email protected]>
21+
*
1822
* @internal
1923
*/
2024
final class SecurityConfigUpdater
2125
{
2226
/** @var YamlSourceManipulator */
2327
private $manipulator;
2428

29+
/** @var Logger|null */
30+
private $ysmLogger;
31+
32+
public function __construct(Logger $ysmLogger = null)
33+
{
34+
$this->ysmLogger = $ysmLogger;
35+
}
36+
2537
/**
2638
* Updates security.yaml contents based on a new User class.
2739
*/
2840
public function updateForUserClass(string $yamlSource, UserClassConfiguration $userConfig, string $userClass): string
2941
{
3042
$this->manipulator = new YamlSourceManipulator($yamlSource);
3143

44+
if (null !== $this->ysmLogger) {
45+
$this->manipulator->setLogger($this->ysmLogger);
46+
}
47+
3248
$this->normalizeSecurityYamlFile();
3349

3450
$this->updateProviders($userConfig, $userClass);
@@ -43,10 +59,14 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
4359
return $contents;
4460
}
4561

46-
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup): string
62+
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup, bool $useSecurity52): string
4763
{
4864
$this->manipulator = new YamlSourceManipulator($yamlSource);
4965

66+
if (null !== $this->ysmLogger) {
67+
$this->manipulator->setLogger($this->ysmLogger);
68+
}
69+
5070
$this->normalizeSecurityYamlFile();
5171

5272
$newData = $this->manipulator->getData();
@@ -56,23 +76,50 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName,
5676
}
5777

5878
if (!isset($newData['security']['firewalls'][$firewallName])) {
59-
$newData['security']['firewalls'][$firewallName] = ['anonymous' => true];
79+
if ($useSecurity52) {
80+
$newData['security']['firewalls'][$firewallName] = ['lazy' => true];
81+
} else {
82+
$newData['security']['firewalls'][$firewallName] = ['anonymous' => 'lazy'];
83+
}
6084
}
6185

6286
$firewall = $newData['security']['firewalls'][$firewallName];
6387

64-
if (!isset($firewall['guard'])) {
65-
$firewall['guard'] = [];
66-
}
88+
if ($useSecurity52) {
89+
if (isset($firewall['custom_authenticator'])) {
90+
if (\is_array($firewall['custom_authenticator'])) {
91+
$firewall['custom_authenticator'][] = $authenticatorClass;
92+
} else {
93+
$stringValue = $firewall['custom_authenticator'];
94+
$firewall['custom_authenticator'] = [];
95+
$firewall['custom_authenticator'][] = $stringValue;
96+
$firewall['custom_authenticator'][] = $authenticatorClass;
97+
}
98+
} else {
99+
$firewall['custom_authenticator'] = $authenticatorClass;
100+
}
67101

68-
if (!isset($firewall['guard']['authenticators'])) {
69-
$firewall['guard']['authenticators'] = [];
70-
}
102+
if (!isset($firewall['entry_point']) && $chosenEntryPoint) {
103+
$firewall['entry_point_empty_line'] = $this->manipulator->createEmptyLine();
104+
$firewall['entry_point_comment'] = $this->manipulator->createCommentLine(
105+
' the entry_point start() method determines what happens when an anonymous user accesses a protected page'
106+
);
107+
$firewall['entry_point'] = $authenticatorClass;
108+
}
109+
} else {
110+
if (!isset($firewall['guard'])) {
111+
$firewall['guard'] = [];
112+
}
71113

72-
$firewall['guard']['authenticators'][] = $authenticatorClass;
114+
if (!isset($firewall['guard']['authenticators'])) {
115+
$firewall['guard']['authenticators'] = [];
116+
}
73117

74-
if (\count($firewall['guard']['authenticators']) > 1) {
75-
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
118+
$firewall['guard']['authenticators'][] = $authenticatorClass;
119+
120+
if (\count($firewall['guard']['authenticators']) > 1) {
121+
$firewall['guard']['entry_point'] = $chosenEntryPoint ?? current($firewall['guard']['authenticators']);
122+
}
76123
}
77124

78125
if (!isset($firewall['logout']) && $logoutSetup) {
@@ -86,10 +133,10 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName,
86133
}
87134

88135
$newData['security']['firewalls'][$firewallName] = $firewall;
136+
89137
$this->manipulator->setData($newData);
90-
$contents = $this->manipulator->getContents();
91138

92-
return $contents;
139+
return $this->manipulator->getContents();
93140
}
94141

95142
private function normalizeSecurityYamlFile()

src/Util/PhpCompatUtil.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ public function canUseAttributes(): bool
3636
return version_compare($version, '8alpha', '>=') && Kernel::VERSION_ID >= 50200;
3737
}
3838

39+
public function canUseTypedProperties(): bool
40+
{
41+
$version = $this->getPhpVersion();
42+
43+
return version_compare($version, '7.4', '>=');
44+
}
45+
3946
protected function getPhpVersion(): string
4047
{
4148
$rootDirectory = $this->fileManager->getRootDirectory();

0 commit comments

Comments
 (0)