Skip to content

Commit

Permalink
feature symfony#986 [make:auth] Add RememberMeBadge (bechir)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 1.0-dev branch.

Discussion
----------

[make:auth] Add `RememberMeBadge`

Fix symfony#848

`@weaverryan` in symfony#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`
  • Loading branch information
weaverryan committed Jul 10, 2023
2 parents 96047f5 + 7e89801 commit c283524
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 25 deletions.
67 changes: 59 additions & 8 deletions src/Maker/MakeAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
Expand All @@ -66,6 +67,9 @@ final class MakeAuthenticator extends AbstractMaker
private const AUTH_TYPE_EMPTY_AUTHENTICATOR = 'empty-authenticator';
private const AUTH_TYPE_FORM_LOGIN = 'form-login';

private const REMEMBER_ME_TYPE_ALWAYS = 'always';
private const REMEMBER_ME_TYPE_CHECKBOX = 'checkbox';

public function __construct(
private FileManager $fileManager,
private SecurityConfigUpdater $configUpdater,
Expand Down Expand Up @@ -184,6 +188,34 @@ function ($answer) {
true
)
);

$command->addArgument('support-remember-me', InputArgument::REQUIRED);
$input->setArgument(
'support-remember-me',
$io->confirm(
'Do you want to support remember me?',
true
)
);

if ($input->getArgument('support-remember-me')) {
$supportRememberMeValues = [
'Activate when the user checks a box' => self::REMEMBER_ME_TYPE_CHECKBOX,
'Always activate remember me' => self::REMEMBER_ME_TYPE_ALWAYS,
];
$command->addArgument('always-remember-me', InputArgument::REQUIRED);

$supportRememberMeType = $io->choice(
'When activate the remember me?',
array_keys($supportRememberMeValues),
key($supportRememberMeValues)
);

$input->setArgument(
'always-remember-me',
$supportRememberMeValues[$supportRememberMeType]
);
}
}
}

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

$supportRememberMe = $input->hasArgument('support-remember-me') ? $input->getArgument('support-remember-me') : false;
$alwaysRememberMe = $input->hasArgument('always-remember-me') ? $input->getArgument('always-remember-me') : false;

$this->generateAuthenticatorClass(
$securityData,
$input->getArgument('authenticator-type'),
$input->getArgument('authenticator-class'),
$input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null,
$supportRememberMe,
);

// update security.yaml with guard config
Expand All @@ -215,7 +251,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$input->getOption('firewall-name'),
$entryPoint,
$input->getArgument('authenticator-class'),
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
$supportRememberMe,
$alwaysRememberMe
);
$generator->dumpFile($path, $newYaml);
$securityYamlUpdated = true;
Expand All @@ -226,7 +264,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$this->generateFormLoginFiles(
$input->getArgument('controller-class'),
$input->getArgument('username-field'),
$input->getArgument('logout-setup')
$input->getArgument('logout-setup'),
$supportRememberMe,
$alwaysRememberMe,
);
}

Expand All @@ -241,12 +281,14 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$input->getArgument('authenticator-class'),
$securityData,
$input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false
$input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
$supportRememberMe,
$alwaysRememberMe
)
);
}

private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField): void
private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField, bool $supportRememberMe): void
{
$useStatements = new UseStatementGenerator([
Request::class,
Expand Down Expand Up @@ -288,6 +330,10 @@ private function generateAuthenticatorClass(array $securityData, string $authent
$useStatements->addUseStatement(LegacySecurity::class);
}

if ($supportRememberMe) {
$useStatements->addUseStatement(RememberMeBadge::class);
}

$userClassNameDetails = $this->generator->createClassNameDetails(
'\\'.$userClass,
'Entity\\'
Expand All @@ -305,11 +351,12 @@ private function generateAuthenticatorClass(array $securityData, string $authent
'username_field_var' => Str::asLowerCamelCase($userNameField),
'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass),
'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
'remember_me_badge' => $supportRememberMe,
]
);
}

private function generateFormLoginFiles(string $controllerClass, string $userNameField, bool $logoutSetup): void
private function generateFormLoginFiles(string $controllerClass, string $userNameField, bool $logoutSetup, bool $supportRememberMe, bool $alwaysRememberMe): void
{
$controllerClassNameDetails = $this->generator->createClassNameDetails(
$controllerClass,
Expand Down Expand Up @@ -362,11 +409,13 @@ private function generateFormLoginFiles(string $controllerClass, string $userNam
'username_is_email' => false !== stripos($userNameField, 'email'),
'username_label' => ucfirst(Str::asHumanWords($userNameField)),
'logout_setup' => $logoutSetup,
'support_remember_me' => $supportRememberMe,
'always_remember_me' => $alwaysRememberMe,
]
);
}

private function generateNextMessage(bool $securityYamlUpdated, string $authenticatorType, string $authenticatorClass, array $securityData, $userClass, bool $logoutSetup): array
private function generateNextMessage(bool $securityYamlUpdated, string $authenticatorType, string $authenticatorClass, array $securityData, $userClass, bool $logoutSetup, bool $supportRememberMe, bool $alwaysRememberMe): array
{
$nextTexts = ['Next:'];
$nextTexts[] = '- Customize your new authenticator.';
Expand All @@ -377,7 +426,9 @@ private function generateNextMessage(bool $securityYamlUpdated, string $authenti
'main',
null,
$authenticatorClass,
$logoutSetup
$logoutSetup,
$supportRememberMe,
$alwaysRememberMe
);
$nextTexts[] = "- Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public function authenticate(Request $request): Passport
new UserBadge($<?= $username_field_var ?>),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),<?= $remember_me_badge ? "
new RememberMeBadge(),\n" : "" ?>
]
);
}
Expand Down
21 changes: 10 additions & 11 deletions src/Resources/skeleton/authenticator/login_form.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>

{#
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
See https://symfony.com/doc/current/security/remember_me.html

<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
#}
<?php if($support_remember_me): ?>
<?php if(!$always_remember_me): ?>

<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
<?php endif; ?>
<?php endif; ?>

<button class="btn btn-lg btn-primary" type="submit">
Sign in
Expand Down
37 changes: 36 additions & 1 deletion src/Security/SecurityConfigUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u
return $contents;
}

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

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

if (!isset($firewall['logout']) && $logoutSetup) {
$firewall['logout'] = ['path' => 'app_logout'];
$firewall['logout'][] = $this->manipulator->createCommentLine(
' where to redirect after logout'
);
$firewall['logout'][] = $this->manipulator->createCommentLine(
' target: app_any_route'
);
}

if ($supportRememberMe) {
if (!isset($firewall['remember_me'])) {
$firewall['remember_me_empty_line'] = $this->manipulator->createEmptyLine();
$firewall['remember_me'] = [
'secret' => '%kernel.secret%',
'lifetime' => 604800,
'path' => '/',
];
if (!$alwaysRememberMe) {
$firewall['remember_me'][] = $this->manipulator->createCommentLine(' by default, the feature is enabled by checking a checkbox in the');
$firewall['remember_me'][] = $this->manipulator->createCommentLine(' login form, uncomment the following line to always enable it.');
}
} else {
$firewall['remember_me']['secret'] ??= '%kernel.secret%';
$firewall['remember_me']['lifetime'] ??= 604800;
$firewall['remember_me']['path'] ??= '/';
}

if ($alwaysRememberMe) {
$firewall['remember_me']['always_remember_me'] = true;
} else {
$firewall['remember_me'][] = $this->manipulator->createCommentLine('always_remember_me: true');
}
}

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

if (!isset($firewall['logout']) && $logoutSetup) {
Expand Down
72 changes: 72 additions & 0 deletions tests/Maker/MakeAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ public function getTestDetails(): \Generator
// field name
'userEmail',
'no',
// remember me support => no
'no',
]);

$this->runLoginTest($runner, 'userEmail');
Expand Down Expand Up @@ -180,6 +182,8 @@ public function getTestDetails(): \Generator
// username field => userEmail
0,
'no',
// remember me support => no
'no',
]);

$runner->runTests();
Expand Down Expand Up @@ -207,6 +211,8 @@ public function getTestDetails(): \Generator
// user class
'App\Security\User',
'no',
// remember me support => no
'no',
]);
}),
];
Expand All @@ -229,6 +235,8 @@ public function getTestDetails(): \Generator
// controller name
'SecurityController',
'no',
// remember me support => no
'no',
]);

$this->runLoginTest($runner, 'email');
Expand All @@ -249,6 +257,8 @@ public function getTestDetails(): \Generator
'SecurityController',
// logout support
'yes',
// remember me support => no
'no',
]);

$this->runLoginTest($runner, 'userEmail', true, 'App\\Entity\\User', true);
Expand All @@ -266,6 +276,68 @@ public function getTestDetails(): \Generator
);
}),
];

yield 'auth_login_form_remember_me_via_checkbox' => [$this->createMakerTest()
->addExtraDependencies('doctrine', 'twig', 'symfony/form')
->run(function (MakerTestRunner $runner) {
$this->makeUser($runner, 'userEmail');

$output = $runner->runMaker([
// authenticator type => login-form
1,
// class name
'AppCustomAuthenticator',
// controller name
'SecurityController',
// logout support
'yes',
// remember me support => yes
'yes',
// remember me type => checkbox
0,
]);

$this->runLoginTest($runner, 'userEmail');

$this->assertStringContainsString('Success', $output);
$seucrityConfig = $runner->readYaml('config/packages/security.yaml');
$firewallMain = $seucrityConfig['security']['firewalls']['main'];

$this->assertEquals('%kernel.secret%', $firewallMain['remember_me']['secret']);
$this->assertEquals('604800', $firewallMain['remember_me']['lifetime']);
}),
];

yield 'auth_login_form_always_remember_me' => [$this->createMakerTest()
->addExtraDependencies('doctrine', 'twig', 'symfony/form')
->run(function (MakerTestRunner $runner) {
$this->makeUser($runner, 'userEmail');

$output = $runner->runMaker([
// authenticator type => login-form
1,
// class name
'AppCustomAuthenticator',
// controller name
'SecurityController',
// logout support
'yes',
// remember me support => yes
'yes',
// remember me type => always
1,
]);

$this->runLoginTest($runner, 'userEmail');

$this->assertStringContainsString('Success', $output);
$seucrityConfig = $runner->readYaml('config/packages/security.yaml');
$firewallMain = $seucrityConfig['security']['firewalls']['main'];

$this->assertEquals('%kernel.secret%', $firewallMain['remember_me']['secret']);
$this->assertTrue($firewallMain['remember_me']['always_remember_me']);
}),
];
}

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

0 comments on commit c283524

Please sign in to comment.