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

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