-
-
Notifications
You must be signed in to change notification settings - Fork 421
[make:security:form-login] new maker to use built in FormLogin #1244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7831e74
f0ffdf9
1d89286
aefc340
1c5fa69
1884cdc
e894848
4639a8e
cb9fa2a
b76b846
a884066
7d4d248
de145ea
bc820f2
e8b41dc
542a1ac
312dba8
7b632ce
530d233
97ebd0e
7ff5796
baf2e96
73bd3b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony MakerBundle package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\MakerBundle\Maker\Security; | ||
|
||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; | ||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||
use Symfony\Bundle\MakerBundle\ConsoleStyle; | ||
use Symfony\Bundle\MakerBundle\DependencyBuilder; | ||
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; | ||
use Symfony\Bundle\MakerBundle\FileManager; | ||
use Symfony\Bundle\MakerBundle\Generator; | ||
use Symfony\Bundle\MakerBundle\InputConfiguration; | ||
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; | ||
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper; | ||
use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater; | ||
use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder; | ||
use Symfony\Bundle\MakerBundle\Str; | ||
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator; | ||
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator; | ||
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator; | ||
use Symfony\Bundle\MakerBundle\Validator; | ||
use Symfony\Bundle\SecurityBundle\SecurityBundle; | ||
use Symfony\Bundle\TwigBundle\TwigBundle; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Routing\Annotation\Route; | ||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; | ||
use Symfony\Component\Yaml\Yaml; | ||
|
||
/** | ||
* Generate Form Login Security using SecurityBundle's Authenticator. | ||
* | ||
* @see https://symfony.com/doc/current/security.html#form-login | ||
* | ||
* @author Jesse Rushlow <[email protected]> | ||
* | ||
* @internal | ||
*/ | ||
final class MakeFormLogin extends AbstractMaker | ||
{ | ||
private const SECURITY_CONFIG_PATH = 'config/packages/security.yaml'; | ||
private YamlSourceManipulator $ysm; | ||
private string $controllerName; | ||
private string $firewallToUpdate; | ||
private string $userNameField; | ||
private bool $willLogout; | ||
|
||
public function __construct( | ||
private FileManager $fileManager, | ||
private SecurityConfigUpdater $securityConfigUpdater, | ||
private SecurityControllerBuilder $securityControllerBuilder, | ||
) { | ||
} | ||
|
||
public static function getCommandName(): string | ||
{ | ||
return 'make:security:form-login'; | ||
} | ||
|
||
public function configureCommand(Command $command, InputConfiguration $inputConfig): void | ||
{ | ||
$command->setHelp(file_get_contents(\dirname(__DIR__, 2).'/Resources/help/security/MakeFormLogin.txt')); | ||
} | ||
|
||
public static function getCommandDescription(): string | ||
{ | ||
return 'Generate the code needed for the form_login authenticator'; | ||
} | ||
|
||
public function configureDependencies(DependencyBuilder $dependencies): void | ||
{ | ||
$dependencies->addClassDependency( | ||
SecurityBundle::class, | ||
'security' | ||
); | ||
|
||
$dependencies->addClassDependency(TwigBundle::class, 'twig'); | ||
|
||
// needed to update the YAML files | ||
$dependencies->addClassDependency( | ||
Yaml::class, | ||
'yaml' | ||
); | ||
|
||
$dependencies->addClassDependency(DoctrineBundle::class, 'orm'); | ||
} | ||
|
||
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void | ||
{ | ||
if (!$this->fileManager->fileExists(self::SECURITY_CONFIG_PATH)) { | ||
throw new RuntimeCommandException(sprintf('The file "%s" does not exist. PHP & XML configuration formats are currently not supported.', self::SECURITY_CONFIG_PATH)); | ||
} | ||
|
||
$this->ysm = new YamlSourceManipulator($this->fileManager->getFileContents(self::SECURITY_CONFIG_PATH)); | ||
$securityData = $this->ysm->getData(); | ||
|
||
if (!isset($securityData['security']['providers']) || !$securityData['security']['providers']) { | ||
throw new RuntimeCommandException('To generate a form login authentication, you must configure at least one entry under "providers" in "security.yaml".'); | ||
} | ||
|
||
$this->controllerName = $io->ask( | ||
'Choose a name for the controller class (e.g. <fg=yellow>SecurityController</>)', | ||
'SecurityController', | ||
[Validator::class, 'validateClassName'] | ||
); | ||
|
||
$securityHelper = new InteractiveSecurityHelper(); | ||
$this->firewallToUpdate = $securityHelper->guessFirewallName($io, $securityData); | ||
$userClass = $securityHelper->guessUserClass($io, $securityData['security']['providers']); | ||
$this->userNameField = $securityHelper->guessUserNameField($io, $userClass, $securityData['security']['providers']); | ||
$this->willLogout = $io->confirm('Do you want to generate a \'/logout\' URL?'); | ||
} | ||
|
||
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void | ||
{ | ||
$useStatements = new UseStatementGenerator([ | ||
AbstractController::class, | ||
Response::class, | ||
Route::class, | ||
AuthenticationUtils::class, | ||
]); | ||
|
||
$controllerNameDetails = $generator->createClassNameDetails($this->controllerName, 'Controller\\', 'Controller'); | ||
$templatePath = strtolower($controllerNameDetails->getRelativeNameWithoutSuffix()); | ||
|
||
$controllerPath = $generator->generateController( | ||
$controllerNameDetails->getFullName(), | ||
'security/formLogin/LoginController.tpl.php', | ||
[ | ||
'use_statements' => $useStatements, | ||
'controller_name' => $controllerNameDetails->getShortName(), | ||
'template_path' => $templatePath, | ||
] | ||
); | ||
|
||
if ($this->willLogout) { | ||
$manipulator = new ClassSourceManipulator($generator->getFileContentsForPendingOperation($controllerPath)); | ||
|
||
$this->securityControllerBuilder->addLogoutMethod($manipulator); | ||
|
||
$generator->dumpFile($controllerPath, $manipulator->getSourceCode()); | ||
} | ||
|
||
$generator->generateTemplate( | ||
sprintf('%s/login.html.twig', $templatePath), | ||
'security/formLogin/login_form.tpl.php', | ||
[ | ||
'logout_setup' => $this->willLogout, | ||
'username_label' => Str::asHumanWords($this->userNameField), | ||
'username_is_email' => false !== stripos($this->userNameField, 'email'), | ||
] | ||
); | ||
|
||
$securityData = $this->securityConfigUpdater->updateForFormLogin($this->ysm->getContents(), $this->firewallToUpdate, 'app_login', 'app_login'); | ||
|
||
if ($this->willLogout) { | ||
$securityData = $this->securityConfigUpdater->updateForLogout($securityData, $this->firewallToUpdate); | ||
} | ||
|
||
$generator->dumpFile(self::SECURITY_CONFIG_PATH, $securityData); | ||
|
||
$generator->writeChanges(); | ||
|
||
$this->writeSuccessMessage($io); | ||
|
||
$io->text([ | ||
sprintf('Next: Review and adapt the login template: <info>%s/login.html.twig</info> to suit your needs.', $templatePath), | ||
]); | ||
} | ||
jrushlow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
The <info>%command.name%</info> command generates a controller and twig template | ||
to allow users to login using the form_login authenticator. | ||
|
||
The controller name, and logout ability can be customized by answering the | ||
questions asked when running <info>%command.name%</info>. | ||
|
||
This will also update your <info>security.yaml</info> for the new authenticator. | ||
|
||
<info>php %command.full_name%</info> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?= "<?php\n" ?> | ||
|
||
namespace <?= $namespace; ?>; | ||
|
||
<?= $use_statements; ?> | ||
|
||
class <?= $controller_name ?> extends AbstractController | ||
{ | ||
#[Route(path: '/login', name: 'app_login')] | ||
public function login(AuthenticationUtils $authenticationUtils): Response | ||
{ | ||
// get the login error if there is one | ||
$error = $authenticationUtils->getLastAuthenticationError(); | ||
|
||
// last username entered by the user | ||
$lastUsername = $authenticationUtils->getLastUsername(); | ||
|
||
return $this->render('<?= $template_path ?>/login.html.twig', [ | ||
'last_username' => $lastUsername, | ||
'error' => $error, | ||
]); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{% extends 'base.html.twig' %} | ||
|
||
{% block title %}Log in!{% endblock %} | ||
|
||
{% block body %} | ||
<form method="post"> | ||
{% if error %} | ||
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> | ||
{% endif %} | ||
|
||
<?php if ($logout_setup): ?> | ||
{% if app.user %} | ||
<div class="mb-3"> | ||
You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Logout</a> | ||
</div> | ||
{% endif %} | ||
|
||
<?php endif; ?> | ||
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> | ||
<label for="username"><?= $username_label; ?></label> | ||
<input type="<?= $username_is_email ? 'email' : 'text'; ?>" value="{{ last_username }}" name="_username" id="username" class="form-control" autocomplete="<?= $username_is_email ? 'email' : 'username'; ?>" required autofocus> | ||
<label for="password">Password</label> | ||
<input type="password" name="_password" id="password" class="form-control" autocomplete="current-password" required> | ||
|
||
<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> | ||
#} | ||
|
||
<button class="btn btn-lg btn-primary" type="submit"> | ||
Sign in | ||
</button> | ||
</form> | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,10 +30,7 @@ public function __construct( | |
) { | ||
} | ||
|
||
/** | ||
* Updates security.yaml contents based on a new User class. | ||
*/ | ||
public function updateForUserClass(string $yamlSource, UserClassConfiguration $userConfig, string $userClass): string | ||
public function updateForFormLogin(string $yamlSource, string $firewallToUpdate, string $loginPath, string $checkPath): string | ||
{ | ||
$this->manipulator = new YamlSourceManipulator($yamlSource); | ||
|
||
|
@@ -43,6 +40,24 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u | |
|
||
$this->normalizeSecurityYamlFile(); | ||
|
||
$newData = $this->manipulator->getData(); | ||
|
||
$newData['security']['firewalls'][$firewallToUpdate]['form_login']['login_path'] = $loginPath; | ||
$newData['security']['firewalls'][$firewallToUpdate]['form_login']['check_path'] = $checkPath; | ||
$newData['security']['firewalls'][$firewallToUpdate]['form_login']['enable_csrf'] = true; | ||
|
||
$this->manipulator->setData($newData); | ||
|
||
return $this->manipulator->getContents(); | ||
} | ||
|
||
/** | ||
* Updates security.yaml contents based on a new User class. | ||
*/ | ||
public function updateForUserClass(string $yamlSource, UserClassConfiguration $userConfig, string $userClass): string | ||
{ | ||
$this->createYamlSourceManipulator($yamlSource); | ||
|
||
$this->updateProviders($userConfig, $userClass); | ||
|
||
if ($userConfig->hasPassword()) { | ||
|
@@ -57,13 +72,7 @@ public function updateForUserClass(string $yamlSource, UserClassConfiguration $u | |
|
||
public function updateForAuthenticator(string $yamlSource, string $firewallName, $chosenEntryPoint, string $authenticatorClass, bool $logoutSetup): string | ||
{ | ||
$this->manipulator = new YamlSourceManipulator($yamlSource); | ||
|
||
if (null !== $this->ysmLogger) { | ||
$this->manipulator->setLogger($this->ysmLogger); | ||
} | ||
|
||
$this->normalizeSecurityYamlFile(); | ||
$this->createYamlSourceManipulator($yamlSource); | ||
|
||
$newData = $this->manipulator->getData(); | ||
|
||
|
@@ -102,23 +111,55 @@ public function updateForAuthenticator(string $yamlSource, string $firewallName, | |
$firewall['entry_point'] = $authenticatorClass; | ||
} | ||
|
||
$newData['security']['firewalls'][$firewallName] = $firewall; | ||
|
||
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' | ||
); | ||
} | ||
$this->configureLogout($newData, $firewallName); | ||
|
||
$newData['security']['firewalls'][$firewallName] = $firewall; | ||
return $this->manipulator->getContents(); | ||
} | ||
|
||
$this->manipulator->setData($newData); | ||
|
||
return $this->manipulator->getContents(); | ||
} | ||
|
||
public function updateForLogout(string $yamlSource, string $firewallName): string | ||
{ | ||
$this->createYamlSourceManipulator($yamlSource); | ||
|
||
$this->configureLogout($this->manipulator->getData(), $firewallName); | ||
|
||
return $this->manipulator->getContents(); | ||
} | ||
|
||
/** | ||
* @legacy This can be removed once we deprecate/remove `make:auth` | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's not technically true. It's just that once we remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The private method shouldn't be needed anymore here. We have a public |
||
private function configureLogout(array $securityData, string $firewallName): void | ||
{ | ||
$securityData['security']['firewalls'][$firewallName]['logout'] = ['path' => 'app_logout']; | ||
$securityData['security']['firewalls'][$firewallName]['logout'][] = $this->manipulator->createCommentLine( | ||
' where to redirect after logout' | ||
); | ||
$securityData['security']['firewalls'][$firewallName]['logout'][] = $this->manipulator->createCommentLine( | ||
' target: app_any_route' | ||
); | ||
jrushlow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
$this->manipulator->setData($securityData); | ||
} | ||
|
||
private function createYamlSourceManipulator(string $yamlSource): void | ||
{ | ||
$this->manipulator = new YamlSourceManipulator($yamlSource); | ||
|
||
if (null !== $this->ysmLogger) { | ||
$this->manipulator->setLogger($this->ysmLogger); | ||
} | ||
|
||
$this->normalizeSecurityYamlFile(); | ||
} | ||
|
||
private function normalizeSecurityYamlFile(): void | ||
{ | ||
if (!isset($this->manipulator->getData()['security'])) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.