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

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