vendor/symfony/maker-bundle/src/Maker/MakeAuthenticator.php line 463

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony MakerBundle package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bundle\MakerBundle\Maker;
  11. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  12. use Symfony\Bundle\MakerBundle\ConsoleStyle;
  13. use Symfony\Bundle\MakerBundle\DependencyBuilder;
  14. use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
  15. use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
  16. use Symfony\Bundle\MakerBundle\FileManager;
  17. use Symfony\Bundle\MakerBundle\Generator;
  18. use Symfony\Bundle\MakerBundle\InputConfiguration;
  19. use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
  20. use Symfony\Bundle\MakerBundle\Security\SecurityConfigUpdater;
  21. use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder;
  22. use Symfony\Bundle\MakerBundle\Str;
  23. use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
  24. use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
  25. use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
  26. use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
  27. use Symfony\Bundle\MakerBundle\Validator;
  28. use Symfony\Bundle\SecurityBundle\Security;
  29. use Symfony\Bundle\SecurityBundle\SecurityBundle;
  30. use Symfony\Bundle\TwigBundle\TwigBundle;
  31. use Symfony\Component\Console\Command\Command;
  32. use Symfony\Component\Console\Input\InputArgument;
  33. use Symfony\Component\Console\Input\InputInterface;
  34. use Symfony\Component\Console\Input\InputOption;
  35. use Symfony\Component\Console\Question\Question;
  36. use Symfony\Component\HttpFoundation\RedirectResponse;
  37. use Symfony\Component\HttpFoundation\Request;
  38. use Symfony\Component\HttpFoundation\Response;
  39. use Symfony\Component\Routing\Annotation\Route;
  40. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  41. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  42. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  43. use Symfony\Component\Security\Core\Security as LegacySecurity;
  44. use Symfony\Component\Security\Guard\AuthenticatorInterface as GuardAuthenticatorInterface;
  45. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  46. use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
  47. use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
  48. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
  49. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
  50. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  51. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  52. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  53. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  54. use Symfony\Component\Yaml\Yaml;
  55. /**
  56.  * @author Ryan Weaver   <ryan@symfonycasts.com>
  57.  * @author Jesse Rushlow <jr@rushlow.dev>
  58.  *
  59.  * @internal
  60.  */
  61. final class MakeAuthenticator extends AbstractMaker
  62. {
  63.     private const AUTH_TYPE_EMPTY_AUTHENTICATOR 'empty-authenticator';
  64.     private const AUTH_TYPE_FORM_LOGIN 'form-login';
  65.     private const REMEMBER_ME_TYPE_ALWAYS 'always';
  66.     private const REMEMBER_ME_TYPE_CHECKBOX 'checkbox';
  67.     public function __construct(
  68.         private FileManager $fileManager,
  69.         private SecurityConfigUpdater $configUpdater,
  70.         private Generator $generator,
  71.         private DoctrineHelper $doctrineHelper,
  72.         private SecurityControllerBuilder $securityControllerBuilder,
  73.     ) {
  74.     }
  75.     public static function getCommandName(): string
  76.     {
  77.         return 'make:auth';
  78.     }
  79.     public static function getCommandDescription(): string
  80.     {
  81.         return 'Creates a Guard authenticator of different flavors';
  82.     }
  83.     public function configureCommand(Command $commandInputConfiguration $inputConfig): void
  84.     {
  85.         $command
  86.             ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeAuth.txt'));
  87.     }
  88.     public function interact(InputInterface $inputConsoleStyle $ioCommand $command): void
  89.     {
  90.         if (!$this->fileManager->fileExists($path 'config/packages/security.yaml')) {
  91.             throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. PHP & XML configuration formats are currently not supported.');
  92.         }
  93.         $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
  94.         $securityData $manipulator->getData();
  95.         // @legacy - Can be removed when Symfony 5.4 support is dropped
  96.         if (interface_exists(GuardAuthenticatorInterface::class) && !($securityData['security']['enable_authenticator_manager'] ?? false)) {
  97.             throw new RuntimeCommandException('MakerBundle only supports the new authenticator based security system. See https://symfony.com/doc/current/security.html');
  98.         }
  99.         // authenticator type
  100.         $authenticatorTypeValues = [
  101.             'Empty authenticator' => self::AUTH_TYPE_EMPTY_AUTHENTICATOR,
  102.             'Login form authenticator' => self::AUTH_TYPE_FORM_LOGIN,
  103.         ];
  104.         $command->addArgument('authenticator-type'InputArgument::REQUIRED);
  105.         $authenticatorType $io->choice(
  106.             'What style of authentication do you want?',
  107.             array_keys($authenticatorTypeValues),
  108.             key($authenticatorTypeValues)
  109.         );
  110.         $input->setArgument(
  111.             'authenticator-type',
  112.             $authenticatorTypeValues[$authenticatorType]
  113.         );
  114.         if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
  115.             $neededDependencies = [TwigBundle::class => 'twig'];
  116.             $missingPackagesMessage $this->addDependencies($neededDependencies'Twig must be installed to display the login form.');
  117.             if ($missingPackagesMessage) {
  118.                 throw new RuntimeCommandException($missingPackagesMessage);
  119.             }
  120.             if (!isset($securityData['security']['providers']) || !$securityData['security']['providers']) {
  121.                 throw new RuntimeCommandException('To generate a form login authentication, you must configure at least one entry under "providers" in "security.yaml".');
  122.             }
  123.         }
  124.         // authenticator class
  125.         $command->addArgument('authenticator-class'InputArgument::REQUIRED);
  126.         $questionAuthenticatorClass = new Question('The class name of the authenticator to create (e.g. <fg=yellow>AppCustomAuthenticator</>)');
  127.         $questionAuthenticatorClass->setValidator(
  128.             function ($answer) {
  129.                 Validator::notBlank($answer);
  130.                 return Validator::classDoesNotExist(
  131.                     $this->generator->createClassNameDetails($answer'Security\\''Authenticator')->getFullName()
  132.                 );
  133.             }
  134.         );
  135.         $input->setArgument('authenticator-class'$io->askQuestion($questionAuthenticatorClass));
  136.         $interactiveSecurityHelper = new InteractiveSecurityHelper();
  137.         $command->addOption('firewall-name'nullInputOption::VALUE_OPTIONAL);
  138.         $input->setOption('firewall-name'$firewallName $interactiveSecurityHelper->guessFirewallName($io$securityData));
  139.         $command->addOption('entry-point'nullInputOption::VALUE_OPTIONAL);
  140.         if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
  141.             $command->addArgument('controller-class'InputArgument::REQUIRED);
  142.             $input->setArgument(
  143.                 'controller-class',
  144.                 $io->ask(
  145.                     'Choose a name for the controller class (e.g. <fg=yellow>SecurityController</>)',
  146.                     'SecurityController',
  147.                     [Validator::class, 'validateClassName']
  148.                 )
  149.             );
  150.             $command->addArgument('user-class'InputArgument::REQUIRED);
  151.             $input->setArgument(
  152.                 'user-class',
  153.                 $userClass $interactiveSecurityHelper->guessUserClass($io$securityData['security']['providers'])
  154.             );
  155.             $command->addArgument('username-field'InputArgument::REQUIRED);
  156.             $input->setArgument(
  157.                 'username-field',
  158.                 $interactiveSecurityHelper->guessUserNameField($io$userClass$securityData['security']['providers'])
  159.             );
  160.             $command->addArgument('logout-setup'InputArgument::REQUIRED);
  161.             $input->setArgument(
  162.                 'logout-setup',
  163.                 $io->confirm(
  164.                     'Do you want to generate a \'/logout\' URL?',
  165.                     true
  166.                 )
  167.             );
  168.             $command->addArgument('support-remember-me'InputArgument::REQUIRED);
  169.             $input->setArgument(
  170.                 'support-remember-me',
  171.                 $io->confirm(
  172.                     'Do you want to support remember me?',
  173.                     true
  174.                 )
  175.             );
  176.             if ($input->getArgument('support-remember-me')) {
  177.                 $supportRememberMeValues = [
  178.                     'Activate when the user checks a box' => self::REMEMBER_ME_TYPE_CHECKBOX,
  179.                     'Always activate remember me' => self::REMEMBER_ME_TYPE_ALWAYS,
  180.                 ];
  181.                 $command->addArgument('always-remember-me'InputArgument::REQUIRED);
  182.                 $supportRememberMeType $io->choice(
  183.                     'How should remember me be activated?',
  184.                     array_keys($supportRememberMeValues),
  185.                     key($supportRememberMeValues)
  186.                 );
  187.                 $input->setArgument(
  188.                     'always-remember-me',
  189.                     $supportRememberMeValues[$supportRememberMeType]
  190.                 );
  191.             }
  192.         }
  193.     }
  194.     public function generate(InputInterface $inputConsoleStyle $ioGenerator $generator): void
  195.     {
  196.         $manipulator = new YamlSourceManipulator($this->fileManager->getFileContents('config/packages/security.yaml'));
  197.         $securityData $manipulator->getData();
  198.         $supportRememberMe $input->hasArgument('support-remember-me') ? $input->getArgument('support-remember-me') : false;
  199.         $alwaysRememberMe $input->hasArgument('always-remember-me') ? $input->getArgument('always-remember-me') : false;
  200.         $this->generateAuthenticatorClass(
  201.             $securityData,
  202.             $input->getArgument('authenticator-type'),
  203.             $input->getArgument('authenticator-class'),
  204.             $input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
  205.             $input->hasArgument('username-field') ? $input->getArgument('username-field') : null,
  206.             $supportRememberMe,
  207.         );
  208.         // update security.yaml with guard config
  209.         $securityYamlUpdated false;
  210.         $entryPoint $input->getOption('entry-point');
  211.         if (self::AUTH_TYPE_FORM_LOGIN !== $input->getArgument('authenticator-type')) {
  212.             $entryPoint false;
  213.         }
  214.         try {
  215.             $newYaml $this->configUpdater->updateForAuthenticator(
  216.                 $this->fileManager->getFileContents($path 'config/packages/security.yaml'),
  217.                 $input->getOption('firewall-name'),
  218.                 $entryPoint,
  219.                 $input->getArgument('authenticator-class'),
  220.                 $input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
  221.                 $supportRememberMe,
  222.                 $alwaysRememberMe
  223.             );
  224.             $generator->dumpFile($path$newYaml);
  225.             $securityYamlUpdated true;
  226.         } catch (YamlManipulationFailedException) {
  227.         }
  228.         if (self::AUTH_TYPE_FORM_LOGIN === $input->getArgument('authenticator-type')) {
  229.             $this->generateFormLoginFiles(
  230.                 $input->getArgument('controller-class'),
  231.                 $input->getArgument('username-field'),
  232.                 $input->getArgument('logout-setup'),
  233.                 $supportRememberMe,
  234.                 $alwaysRememberMe,
  235.             );
  236.         }
  237.         $generator->writeChanges();
  238.         $this->writeSuccessMessage($io);
  239.         $io->text(
  240.             $this->generateNextMessage(
  241.                 $securityYamlUpdated,
  242.                 $input->getArgument('authenticator-type'),
  243.                 $input->getArgument('authenticator-class'),
  244.                 $securityData,
  245.                 $input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
  246.                 $input->hasArgument('logout-setup') ? $input->getArgument('logout-setup') : false,
  247.                 $supportRememberMe,
  248.                 $alwaysRememberMe
  249.             )
  250.         );
  251.     }
  252.     private function generateAuthenticatorClass(array $securityDatastring $authenticatorTypestring $authenticatorClass$userClass$userNameFieldbool $supportRememberMe): void
  253.     {
  254.         $useStatements = new UseStatementGenerator([
  255.             Request::class,
  256.             Response::class,
  257.             TokenInterface::class,
  258.             Passport::class,
  259.         ]);
  260.         // generate authenticator class
  261.         if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $authenticatorType) {
  262.             $useStatements->addUseStatement([
  263.                 AuthenticationException::class,
  264.                 AbstractAuthenticator::class,
  265.             ]);
  266.             $this->generator->generateClass(
  267.                 $authenticatorClass,
  268.                 'authenticator/EmptyAuthenticator.tpl.php',
  269.                 ['use_statements' => $useStatements]
  270.             );
  271.             return;
  272.         }
  273.         $useStatements->addUseStatement([
  274.             RedirectResponse::class,
  275.             UrlGeneratorInterface::class,
  276.             AbstractLoginFormAuthenticator::class,
  277.             CsrfTokenBadge::class,
  278.             UserBadge::class,
  279.             PasswordCredentials::class,
  280.             TargetPathTrait::class,
  281.         ]);
  282.         // @legacy - Can be removed when Symfony 5.4 support is dropped
  283.         if (class_exists(Security::class)) {
  284.             $useStatements->addUseStatement(Security::class);
  285.         } else {
  286.             $useStatements->addUseStatement(LegacySecurity::class);
  287.         }
  288.         if ($supportRememberMe) {
  289.             $useStatements->addUseStatement(RememberMeBadge::class);
  290.         }
  291.         $userClassNameDetails $this->generator->createClassNameDetails(
  292.             '\\'.$userClass,
  293.             'Entity\\'
  294.         );
  295.         $this->generator->generateClass(
  296.             $authenticatorClass,
  297.             'authenticator/LoginFormAuthenticator.tpl.php',
  298.             [
  299.                 'use_statements' => $useStatements,
  300.                 'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
  301.                 'user_class_name' => $userClassNameDetails->getShortName(),
  302.                 'username_field' => $userNameField,
  303.                 'username_field_label' => Str::asHumanWords($userNameField),
  304.                 'username_field_var' => Str::asLowerCamelCase($userNameField),
  305.                 'user_needs_encoder' => $this->userClassHasEncoder($securityData$userClass),
  306.                 'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
  307.                 'remember_me_badge' => $supportRememberMe,
  308.             ]
  309.         );
  310.     }
  311.     private function generateFormLoginFiles(string $controllerClassstring $userNameFieldbool $logoutSetupbool $supportRememberMebool $alwaysRememberMe): void
  312.     {
  313.         $controllerClassNameDetails $this->generator->createClassNameDetails(
  314.             $controllerClass,
  315.             'Controller\\',
  316.             'Controller'
  317.         );
  318.         if (!class_exists($controllerClassNameDetails->getFullName())) {
  319.             $useStatements = new UseStatementGenerator([
  320.                 AbstractController::class,
  321.                 Route::class,
  322.                 AuthenticationUtils::class,
  323.             ]);
  324.             $controllerPath $this->generator->generateController(
  325.                 $controllerClassNameDetails->getFullName(),
  326.                 'authenticator/EmptySecurityController.tpl.php',
  327.                 ['use_statements' => $useStatements]
  328.             );
  329.             $controllerSourceCode $this->generator->getFileContentsForPendingOperation($controllerPath);
  330.         } else {
  331.             $controllerPath $this->fileManager->getRelativePathForFutureClass($controllerClassNameDetails->getFullName());
  332.             $controllerSourceCode $this->fileManager->getFileContents($controllerPath);
  333.         }
  334.         if (method_exists($controllerClassNameDetails->getFullName(), 'login')) {
  335.             throw new RuntimeCommandException(sprintf('Method "login" already exists on class %s'$controllerClassNameDetails->getFullName()));
  336.         }
  337.         $manipulator = new ClassSourceManipulator(
  338.             sourceCode$controllerSourceCode,
  339.             overwritetrue
  340.         );
  341.         $this->securityControllerBuilder->addLoginMethod($manipulator);
  342.         if ($logoutSetup) {
  343.             $this->securityControllerBuilder->addLogoutMethod($manipulator);
  344.         }
  345.         $this->generator->dumpFile($controllerPath$manipulator->getSourceCode());
  346.         // create login form template
  347.         $this->generator->generateTemplate(
  348.             'security/login.html.twig',
  349.             'authenticator/login_form.tpl.php',
  350.             [
  351.                 'username_field' => $userNameField,
  352.                 'username_is_email' => false !== stripos($userNameField'email'),
  353.                 'username_label' => ucfirst(Str::asHumanWords($userNameField)),
  354.                 'logout_setup' => $logoutSetup,
  355.                 'support_remember_me' => $supportRememberMe,
  356.                 'always_remember_me' => $alwaysRememberMe,
  357.             ]
  358.         );
  359.     }
  360.     private function generateNextMessage(bool $securityYamlUpdatedstring $authenticatorTypestring $authenticatorClass, array $securityData$userClassbool $logoutSetupbool $supportRememberMebool $alwaysRememberMe): array
  361.     {
  362.         $nextTexts = ['Next:'];
  363.         $nextTexts[] = '- Customize your new authenticator.';
  364.         if (!$securityYamlUpdated) {
  365.             $yamlExample $this->configUpdater->updateForAuthenticator(
  366.                 'security: {}',
  367.                 'main',
  368.                 null,
  369.                 $authenticatorClass,
  370.                 $logoutSetup,
  371.                 $supportRememberMe,
  372.                 $alwaysRememberMe
  373.             );
  374.             $nextTexts[] = "- Your <info>security.yaml</info> could not be updated automatically. You'll need to add the following config manually:\n\n".$yamlExample;
  375.         }
  376.         if (self::AUTH_TYPE_FORM_LOGIN === $authenticatorType) {
  377.             $nextTexts[] = sprintf('- Finish the redirect "TODO" in the <info>%s::onAuthenticationSuccess()</info> method.'$authenticatorClass);
  378.             if (!$this->doctrineHelper->isClassAMappedEntity($userClass)) {
  379.                 $nextTexts[] = sprintf('- Review <info>%s::getUser()</info> to make sure it matches your needs.'$authenticatorClass);
  380.             }
  381.             $nextTexts[] = '- Review & adapt the login template: <info>'.$this->fileManager->getPathForTemplate('security/login.html.twig').'</info>.';
  382.         }
  383.         return $nextTexts;
  384.     }
  385.     private function userClassHasEncoder(array $securityDatastring $userClass): bool
  386.     {
  387.         $userNeedsEncoder false;
  388.         $hashersData $securityData['security']['encoders'] ?? $securityData['security']['encoders'] ?? [];
  389.         foreach ($hashersData as $userClassWithEncoder => $encoder) {
  390.             if ($userClass === $userClassWithEncoder || is_subclass_of($userClass$userClassWithEncoder) || class_implements($userClass$userClassWithEncoder)) {
  391.                 $userNeedsEncoder true;
  392.             }
  393.         }
  394.         return $userNeedsEncoder;
  395.     }
  396.     public function configureDependencies(DependencyBuilder $dependenciesInputInterface $input null): void
  397.     {
  398.         $dependencies->addClassDependency(
  399.             SecurityBundle::class,
  400.             'security'
  401.         );
  402.         // needed to update the YAML files
  403.         $dependencies->addClassDependency(
  404.             Yaml::class,
  405.             'yaml'
  406.         );
  407.     }
  408. }