Skip to content

Commit c283524

Browse files
committed
feature #986 [make:auth] Add RememberMeBadge (bechir)
This PR was squashed before being merged into the 1.0-dev branch. Discussion ---------- [make:auth] Add `RememberMeBadge` Fix #848 `@weaverryan` in #848: > EDIT: And I wonder if we should even ask "Do you want to support remember me?" during this process? We could then ask "Do you want remember me to be activated via a checkbox or always activated"? We could use this to determine how the template is generated AND to automatically add the correct remember_me config to security.yaml. - [x] Ask "Do you want to support remember me?" and "Do you want remember me to be activated via a checkbox or always activated"? - [x] Add RememberMeBadge in `LoginFormAuthenticator` when remember me is supported - [x] Update generated `security.yaml` - [x] Update `security/login.html.twig` Commits ------- 7e89801 [make:auth] Add `RememberMeBadge`
2 parents 96047f5 + 7e89801 commit c283524

File tree

8 files changed

+253
-25
lines changed

8 files changed

+253
-25
lines changed

src/Maker/MakeAuthenticator.php

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
5050
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
5151
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
52+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
5253
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
5354
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
5455
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
@@ -66,6 +67,9 @@ final class MakeAuthenticator extends AbstractMaker
6667
private const AUTH_TYPE_EMPTY_AUTHENTICATOR = 'empty-authenticator';
6768
private const AUTH_TYPE_FORM_LOGIN = 'form-login';
6869

70+
private const REMEMBER_ME_TYPE_ALWAYS = 'always';
71+
private const REMEMBER_ME_TYPE_CHECKBOX = 'checkbox';
72+
6973
public function __construct(
7074
private FileManager $fileManager,
7175
private SecurityConfigUpdater $configUpdater,
@@ -184,6 +188,34 @@ function ($answer) {
184188
true
185189
)
186190
);
191+
192+
$command->addArgument('support-remember-me', InputArgument::REQUIRED);
193+
$input->setArgument(
194+
'support-remember-me',
195+
$io->confirm(
196+
'Do you want to support remember me?',
197+
true
198+
)
199+
);
200+
201+
if ($input->getArgument('support-remember-me')) {
202+
$supportRememberMeValues = [
203+
'Activate when the user checks a box' => self::REMEMBER_ME_TYPE_CHECKBOX,
204+
'Always activate remember me' => self::REMEMBER_ME_TYPE_ALWAYS,
205+
];
206+
$command->addArgument('always-remember-me', InputArgument::REQUIRED);
207+
208+
$supportRememberMeType = $io->choice(
209+
'When activate the remember me?',
210+
array_keys($supportRememberMeValues),
211+
key($supportRememberMeValues)
212+
);
213+
214+
$input->setArgument(
215+
'always-remember-me',
216+
$supportRememberMeValues[$supportRememberMeType]
217+
);
218+
}
187219
}
188220
}
189221

@@ -192,12 +224,16 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
192224
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml'));
193225
$securityData = $manipulator->getData();
194226

227+
$supportRememberMe = $input->hasArgument('support-remember-me') ? $input->getArgument('support-remember-me') : false;
228+
$alwaysRememberMe = $input->hasArgument('always-remember-me') ? $input->getArgument('always-remember-me') : false;
229+
195230
$this->generateAuthenticatorClass(
196231
$securityData,
197232
$input->getArgument('authenticator-type'),
198233
$input->getArgument('authenticator-class'),
199234
$input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
200-
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null
235+
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null,
236+
$supportRememberMe,
201237
);
202238

203239
// update security.yaml with guard config
@@ -215,7 +251,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
215251
$input->getOption('firewall-name'),
216252
$entryPoint,
217253
$input->getArgument('authenticator-class'),
218-
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
254+
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
255+
$supportRememberMe,
256+
$alwaysRememberMe
219257
);
220258
$generator->dumpFile($path, $newYaml);
221259
$securityYamlUpdated = true;
@@ -226,7 +264,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
226264
$this->generateFormLoginFiles(
227265
$input->getArgument('controller-class'),
228266
$input->getArgument('username-field'),
229-
$input->getArgument('logout-setup')
267+
$input->getArgument('logout-setup'),
268+
$supportRememberMe,
269+
$alwaysRememberMe,
230270
);
231271
}
232272

@@ -241,12 +281,14 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
241281
$input->getArgument('authenticator-class'),
242282
$securityData,
243283
$input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
244-
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
284+
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
285+
$supportRememberMe,
286+
$alwaysRememberMe
245287
)
246288
);
247289
}
248290

249-
private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField): void
291+
private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField, bool $supportRememberMe): void
250292
{
251293
$useStatements = new UseStatementGenerator([
252294
Request::class,
@@ -288,6 +330,10 @@ private function generateAuthenticatorClass(array $securityData, string $authent
288330
$useStatements->addUseStatement(LegacySecurity::class);
289331
}
290332

333+
if ($supportRememberMe) {
334+
$useStatements->addUseStatement(RememberMeBadge::class);
335+
}
336+
291337
$userClassNameDetails = $this->generator->createClassNameDetails(
292338
'\\'.$userClass,
293339
'Entity\\'
@@ -305,11 +351,12 @@ private function generateAuthenticatorClass(array $securityData, string $authent
305351
'username_field_var' => Str::asLowerCamelCase($userNameField),
306352
'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass),
307353
'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
354+
'remember_me_badge' => $supportRememberMe,
308355
]
309356
);
310357
}
311358

312-
private function generateFormLoginFiles(string $controllerClass, string $userNameField, bool $logoutSetup): void
359+
private function generateFormLoginFiles(string $controllerClass, string $userNameField, bool $logoutSetup, bool $supportRememberMe, bool $alwaysRememberMe): void
313360
{
314361
$controllerClassNameDetails = $this->generator->createClassNameDetails(
315362
$controllerClass,
@@ -362,11 +409,13 @@ private function generateFormLoginFiles(string $controllerClass, string $userNam
362409
'username_is_email' => false !== stripos($userNameField, 'email'),
363410
'username_label' => ucfirst(Str::asHumanWords($userNameField)),
364411
'logout_setup' => $logoutSetup,
412+
'support_remember_me' => $supportRememberMe,
413+
'always_remember_me' => $alwaysRememberMe,
365414
]
366415
);
367416
}
368417

369-
private function generateNextMessage(bool $securityYamlUpdated, string $authenticatorType, string $authenticatorClass, array $securityData, $userClass, bool $logoutSetup): array
418+
private function generateNextMessage(bool $securityYamlUpdated, string $authenticatorType, string $authenticatorClass, array $securityData, $userClass, bool $logoutSetup, bool $supportRememberMe, bool $alwaysRememberMe): array
370419
{
371420
$nextTexts = ['Next:'];
372421
$nextTexts[] = '- Customize your new authenticator.';
@@ -377,7 +426,9 @@ private function generateNextMessage(bool $securityYamlUpdated, string $authenti
377426
'main',
378427
null,
379428
$authenticatorClass,
380-
$logoutSetup
429+
$logoutSetup,
430+
$supportRememberMe,
431+
$alwaysRememberMe
381432
);
382433
$nextTexts[] = "- Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
383434
}

src/Resources/skeleton/authenticator/LoginFormAuthenticator.tpl.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public function authenticate(Request $request): Passport
2424
new UserBadge($<?= $username_field_var ?>),
2525
new PasswordCredentials($request->request->get('password', '')),
2626
[
27-
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
27+
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),<?= $remember_me_badge ? "
28+
new RememberMeBadge(),\n" : "" ?>
2829
]
2930
);
3031
}

src/Resources/skeleton/authenticator/login_form.tpl.php

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,16 @@
2525
<input type="hidden" name="_csrf_token"
2626
value="{{ csrf_token('authenticate') }}"
2727
>
28-
29-
{#
30-
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
31-
See https://symfony.com/doc/current/security/remember_me.html
32-
33-
<div class="checkbox mb-3">
34-
<label>
35-
<input type="checkbox" name="_remember_me"> Remember me
36-
</label>
37-
</div>
38-
#}
28+
<?php if($support_remember_me): ?>
29+
<?php if(!$always_remember_me): ?>
30+
31+
<div class="checkbox mb-3">
32+
<label>
33+
<input type="checkbox" name="_remember_me"> Remember me
34+
</label>
35+
</div>
36+
<?php endif; ?>
37+
<?php endif; ?>
3938

4039
<button class="btn btn-lg btn-primary" type="submit">
4140
Sign in

src/Security/SecurityConfigUpdater.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
6969
return $contents;
7070
}
7171

72-
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup): string
72+
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup, bool $supportRememberMe, bool $alwaysRememberMe): string
7373
{
7474
$this->createYamlSourceManipulator($yamlSource);
7575

@@ -110,6 +110,41 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName,
110110
$firewall['entry_point'] = $authenticatorClass;
111111
}
112112

113+
if (!isset($firewall['logout']) && $logoutSetup) {
114+
$firewall['logout'] = ['path' => 'app_logout'];
115+
$firewall['logout'][] = $this->manipulator->createCommentLine(
116+
' where to redirect after logout'
117+
);
118+
$firewall['logout'][] = $this->manipulator->createCommentLine(
119+
' target: app_any_route'
120+
);
121+
}
122+
123+
if ($supportRememberMe) {
124+
if (!isset($firewall['remember_me'])) {
125+
$firewall['remember_me_empty_line'] = $this->manipulator->createEmptyLine();
126+
$firewall['remember_me'] = [
127+
'secret' => '%kernel.secret%',
128+
'lifetime' => 604800,
129+
'path' => '/',
130+
];
131+
if (!$alwaysRememberMe) {
132+
$firewall['remember_me'][] = $this->manipulator->createCommentLine(' by default, the feature is enabled by checking a checkbox in the');
133+
$firewall['remember_me'][] = $this->manipulator->createCommentLine(' login form, uncomment the following line to always enable it.');
134+
}
135+
} else {
136+
$firewall['remember_me']['secret'] ??= '%kernel.secret%';
137+
$firewall['remember_me']['lifetime'] ??= 604800;
138+
$firewall['remember_me']['path'] ??= '/';
139+
}
140+
141+
if ($alwaysRememberMe) {
142+
$firewall['remember_me']['always_remember_me'] = true;
143+
} else {
144+
$firewall['remember_me'][] = $this->manipulator->createCommentLine('always_remember_me: true');
145+
}
146+
}
147+
113148
$newData['security']['firewalls'][$firewallName] = $firewall;
114149

115150
if (!isset($firewall['logout']) && $logoutSetup) {

tests/Maker/MakeAuthenticatorTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ public function getTestDetails(): \Generator
151151
// field name
152152
'userEmail',
153153
'no',
154+
// remember me support => no
155+
'no',
154156
]);
155157

156158
$this->runLoginTest($runner, 'userEmail');
@@ -180,6 +182,8 @@ public function getTestDetails(): \Generator
180182
// username field => userEmail
181183
0,
182184
'no',
185+
// remember me support => no
186+
'no',
183187
]);
184188

185189
$runner->runTests();
@@ -207,6 +211,8 @@ public function getTestDetails(): \Generator
207211
// user class
208212
'App\Security\User',
209213
'no',
214+
// remember me support => no
215+
'no',
210216
]);
211217
}),
212218
];
@@ -229,6 +235,8 @@ public function getTestDetails(): \Generator
229235
// controller name
230236
'SecurityController',
231237
'no',
238+
// remember me support => no
239+
'no',
232240
]);
233241

234242
$this->runLoginTest($runner, 'email');
@@ -249,6 +257,8 @@ public function getTestDetails(): \Generator
249257
'SecurityController',
250258
// logout support
251259
'yes',
260+
// remember me support => no
261+
'no',
252262
]);
253263

254264
$this->runLoginTest($runner, 'userEmail', true, 'App\\Entity\\User', true);
@@ -266,6 +276,68 @@ public function getTestDetails(): \Generator
266276
);
267277
}),
268278
];
279+
280+
yield 'auth_login_form_remember_me_via_checkbox' => [$this->createMakerTest()
281+
->addExtraDependencies('doctrine', 'twig', 'symfony/form')
282+
->run(function (MakerTestRunner $runner) {
283+
$this->makeUser($runner, 'userEmail');
284+
285+
$output = $runner->runMaker([
286+
// authenticator type => login-form
287+
1,
288+
// class name
289+
'AppCustomAuthenticator',
290+
// controller name
291+
'SecurityController',
292+
// logout support
293+
'yes',
294+
// remember me support => yes
295+
'yes',
296+
// remember me type => checkbox
297+
0,
298+
]);
299+
300+
$this->runLoginTest($runner, 'userEmail');
301+
302+
$this->assertStringContainsString('Success', $output);
303+
$seucrityConfig = $runner->readYaml('config/packages/security.yaml');
304+
$firewallMain = $seucrityConfig['security']['firewalls']['main'];
305+
306+
$this->assertEquals('%kernel.secret%', $firewallMain['remember_me']['secret']);
307+
$this->assertEquals('604800', $firewallMain['remember_me']['lifetime']);
308+
}),
309+
];
310+
311+
yield 'auth_login_form_always_remember_me' => [$this->createMakerTest()
312+
->addExtraDependencies('doctrine', 'twig', 'symfony/form')
313+
->run(function (MakerTestRunner $runner) {
314+
$this->makeUser($runner, 'userEmail');
315+
316+
$output = $runner->runMaker([
317+
// authenticator type => login-form
318+
1,
319+
// class name
320+
'AppCustomAuthenticator',
321+
// controller name
322+
'SecurityController',
323+
// logout support
324+
'yes',
325+
// remember me support => yes
326+
'yes',
327+
// remember me type => always
328+
1,
329+
]);
330+
331+
$this->runLoginTest($runner, 'userEmail');
332+
333+
$this->assertStringContainsString('Success', $output);
334+
$seucrityConfig = $runner->readYaml('config/packages/security.yaml');
335+
$firewallMain = $seucrityConfig['security']['firewalls']['main'];
336+
337+
$this->assertEquals('%kernel.secret%', $firewallMain['remember_me']['secret']);
338+
$this->assertTrue($firewallMain['remember_me']['always_remember_me']);
339+
}),
340+
];
269341
}
270342

271343
private function runLoginTest(MakerTestRunner $runner, string $userIdentifier, bool $isEntity = true, string $userClass = 'App\\Entity\\User', bool $testLogin = false): void

0 commit comments

Comments
 (0)