Skip to content

Commit

Permalink
[make:registration] drop guard authentication support (#1243)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan Weaver <weaverryan+github@gmail.com>
  • Loading branch information
jrushlow and weaverryan committed Mar 4, 2024
1 parent 744ad00 commit cbf2646
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 60 deletions.
63 changes: 30 additions & 33 deletions src/Maker/MakeRegistrationForm.php
Expand Up @@ -26,6 +26,8 @@
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Security\Model\Authenticator;
use Symfony\Bundle\MakerBundle\Security\Model\AuthenticatorType;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
Expand All @@ -34,6 +36,7 @@
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Console\Command\Command;
Expand All @@ -49,7 +52,6 @@
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
Expand All @@ -75,8 +77,7 @@ final class MakeRegistrationForm extends AbstractMaker
private $emailGetter;
private $fromEmailAddress;
private $fromEmailName;
private $autoLoginAuthenticator;
private $firewallName;
private ?Authenticator $autoLoginAuthenticator = null;
private $redirectRouteName;
private $addUniqueEntityConstraint;

Expand Down Expand Up @@ -110,7 +111,7 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
$interactiveSecurityHelper = new InteractiveSecurityHelper();

if (null === $this->router) {
throw new RuntimeCommandException('Router have been explicitely disabled in your configuration. This command needs to use the router.');
throw new RuntimeCommandException('Router have been explicitly disabled in your configuration. This command needs to use the router.');
}

if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
Expand Down Expand Up @@ -184,32 +185,20 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma

private function interactAuthenticatorQuestions(ConsoleStyle $io, InteractiveSecurityHelper $interactiveSecurityHelper, array $securityData): void
{
$firewallsData = $securityData['security']['firewalls'] ?? [];
$firewallName = $interactiveSecurityHelper->guessFirewallName(
$io,
$securityData,
'Which firewall key in security.yaml holds the authenticator you want to use for logging in?'
);
// get list of authenticators
$authenticators = $interactiveSecurityHelper->getAuthenticatorsFromConfig($securityData['security']['firewalls'] ?? []);

if (!isset($firewallsData[$firewallName])) {
$io->note('No firewalls found - skipping authentication after registration. You might want to configure your security before running this command.');
if (empty($authenticators)) {
$io->note('No authenticators found - so your user won\'t be automatically authenticated after registering.');

return;
}

$this->firewallName = $firewallName;

// get list of guard authenticators
$authenticatorClasses = $interactiveSecurityHelper->getAuthenticatorClasses($firewallsData[$firewallName]);
if (empty($authenticatorClasses)) {
$io->note('No Guard authenticators found - so your user won\'t be automatically authenticated after registering.');
} else {
$this->autoLoginAuthenticator =
1 === \count($authenticatorClasses) ? $authenticatorClasses[0] : $io->choice(
'Which authenticator\'s onAuthenticationSuccess() should be used after logging in?',
$authenticatorClasses
);
}
$this->autoLoginAuthenticator =
1 === \count($authenticators) ? $authenticators[0] : $io->choice(
'Which authenticator should be used to login the user?',
$authenticators
);
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
Expand Down Expand Up @@ -312,11 +301,22 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
}
}

if ($this->autoLoginAuthenticator) {
$autoLoginVars = [
'login_after_registration' => null !== $this->autoLoginAuthenticator,
];

if (null !== $this->autoLoginAuthenticator) {
$useStatements->addUseStatement([
$this->autoLoginAuthenticator,
UserAuthenticatorInterface::class,
Security::class,
]);

$autoLoginVars['firewall'] = $this->autoLoginAuthenticator->firewallName;
$autoLoginVars['authenticator'] = sprintf('\'%s\'', $this->autoLoginAuthenticator->type->value);

if (AuthenticatorType::CUSTOM === $this->autoLoginAuthenticator->type) {
$useStatements->addUseStatement($this->autoLoginAuthenticator->authenticatorClass);
$autoLoginVars['authenticator'] = sprintf('%s::class', Str::getShortClassName($this->autoLoginAuthenticator->authenticatorClass));
}
}

if ($isTranslatorAvailable = class_exists(Translator::class)) {
Expand All @@ -339,14 +339,11 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'from_email' => $this->fromEmailAddress,
'from_email_name' => addslashes($this->fromEmailName),
'email_getter' => $this->emailGetter,
'authenticator_class_name' => $this->autoLoginAuthenticator ? Str::getShortClassName($this->autoLoginAuthenticator) : null,
'authenticator_full_class_name' => $this->autoLoginAuthenticator,
'firewall_name' => $this->firewallName,
'redirect_route_name' => $this->redirectRouteName,
'password_hasher_class_details' => $generator->createClassNameDetails(UserPasswordHasherInterface::class, '\\'),
'translator_available' => $isTranslatorAvailable,
],
$userRepoVars
$userRepoVars,
$autoLoginVars,
)
);

Expand Down
Expand Up @@ -16,7 +16,7 @@ public function __construct(<?= $email_verifier_class_details->getShortName() ?>

<?php endif; ?>
<?= $generator->generateRouteForControllerMethod($route_path, $route_name) ?>
public function register(Request $request, <?= $password_hasher_class_details->getShortName() ?> $userPasswordHasher<?= $authenticator_full_class_name ? sprintf(', UserAuthenticatorInterface $userAuthenticator, %s $authenticator', $authenticator_class_name) : '' ?>, EntityManagerInterface $entityManager): Response
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher<?= $login_after_registration ? ', Security $security': '' ?>, EntityManagerInterface $entityManager): Response
{
$user = new <?= $user_class_name ?>();
$form = $this->createForm(<?= $form_class_name ?>::class, $user);
Expand All @@ -25,7 +25,7 @@ public function register(Request $request, <?= $password_hasher_class_details->g
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->set<?= ucfirst($password_field) ?>(
$userPasswordHasher->hashPassword(
$userPasswordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
)
Expand All @@ -44,14 +44,11 @@ public function register(Request $request, <?= $password_hasher_class_details->g
->htmlTemplate('registration/confirmation_email.html.twig')
);
<?php endif; ?>

// do anything else you need here, like send an email

<?php if ($authenticator_full_class_name): ?>
return $userAuthenticator->authenticateUser(
$user,
$authenticator,
$request
);
<?php if ($login_after_registration): ?>
return $security->login($user, <?= $authenticator ?>, '<?= $firewall ?>');
<?php else: ?>
return $this->redirectToRoute('<?= $redirect_route_name ?>');
<?php endif; ?>
Expand Down
110 changes: 92 additions & 18 deletions src/Security/InteractiveSecurityHelper.php
Expand Up @@ -11,6 +11,8 @@

namespace Symfony\Bundle\MakerBundle\Security;

use Symfony\Bundle\MakerBundle\Security\Model\Authenticator;
use Symfony\Bundle\MakerBundle\Security\Model\AuthenticatorType;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Style\SymfonyStyle;
Expand Down Expand Up @@ -140,24 +142,6 @@ public function guessPasswordField(SymfonyStyle $io, string $userClass): string
);
}

public function getAuthenticatorClasses(array $firewallData): array
{
if (isset($firewallData['guard'])) {
return array_filter($firewallData['guard']['authenticators'] ?? [], static fn ($authenticator) => class_exists($authenticator));
}

if (isset($firewallData['custom_authenticator'])) {
$authenticators = $firewallData['custom_authenticator'];
if (\is_string($authenticators)) {
$authenticators = [$authenticators];
}

return array_filter($authenticators, static fn ($authenticator) => class_exists($authenticator));
}

return [];
}

public function guessPasswordSetter(SymfonyStyle $io, string $userClass): string
{
if (null === ($methodChoices = $this->methodNameGuesser($userClass, 'setPassword'))) {
Expand Down Expand Up @@ -196,6 +180,96 @@ public function guessIdGetter(SymfonyStyle $io, string $userClass): string
);
}

/**
* @param array<string, array<string, mixed>> $firewalls Config data from security.firewalls
*
* @return Authenticator[]
*/
public function getAuthenticatorsFromConfig(array $firewalls): array
{
$authenticators = [];

/* Iterate over each firewall that exists e.g. security.firewalls.main
* $firewallName could be "main" or "dev", etc...
* $firewallConfig should be an array of the firewalls params
*/
foreach ($firewalls as $firewallName => $firewallConfig) {
if (!\is_array($firewallConfig)) {
continue;
}

$authenticators = [
...$authenticators,
...$this->getAuthenticatorsFromConfigData($firewallConfig, $firewallName),
];
}

return $authenticators;
}

/**
* Pass in a firewalls config e.g. security.firewalls.main like:
* pattern: ^/path
* form_login:
* login_path: app_login
* custom_authenticator:
* - App\Security\MyAuthenticator
*
* @param array<string, mixed> $firewallConfig
*
* @return Authenticator[]
*/
private function getAuthenticatorsFromConfigData(array $firewallConfig, string $firewallName): array
{
$authenticators = [];

foreach ($firewallConfig as $potentialAuthenticator => $configData) {
// Check if $potentialAuthenticator is a supported authenticator or if its some other key.
if (null === ($authenticator = AuthenticatorType::tryFrom($potentialAuthenticator))) {
// $potentialAuthenticator is probably something like "pattern" or "lazy", not an authenticator
continue;
}

// $potentialAuthenticator is a supported authenticator. Check if it's a custom_authenticator.
if (AuthenticatorType::CUSTOM !== $authenticator) {
// We found a "built in" authenticator - "form_login", "json_login", etc...
$authenticators[] = new Authenticator($authenticator, $firewallName);

continue;
}

/*
* $potentialAuthenticator = custom_authenticator.
* $configData is either [App\MyAuthenticator] or (string) App\MyAuthenticator
*/
$customAuthenticators = $this->getCustomAuthenticators($configData, $firewallName);

$authenticators = [...$authenticators, ...$customAuthenticators];
}

return $authenticators;
}

/**
* @param string|array<string> $customAuthenticators A single entry from custom_authenticators or an array of authenticators
*
* @return Authenticator[]
*/
private function getCustomAuthenticators(string|array $customAuthenticators, string $firewallName): array
{
if (\is_string($customAuthenticators)) {
$customAuthenticators = [$customAuthenticators];
}

$authenticators = [];

foreach ($customAuthenticators as $customAuthenticatorClass) {
$authenticators[] = new Authenticator(AuthenticatorType::CUSTOM, $firewallName, $customAuthenticatorClass);
}

return $authenticators;
}

private function methodNameGuesser(string $className, string $suspectedMethodName): ?array
{
$reflectionClass = new \ReflectionClass($className);
Expand Down
39 changes: 39 additions & 0 deletions src/Security/Model/Authenticator.php
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Security\Model;

/**
* @author Jesse Rushlow<jr@rushlow.dev>
*
* @internal
*/
final class Authenticator
{
public function __construct(
public AuthenticatorType $type,
public string $firewallName,
public ?string $authenticatorClass = null,
) {
}

/**
* Useful for asking questions like "Which authenticator do you want to use?".
*/
public function __toString(): string
{
return sprintf(
'"%s" in the "%s" firewall',
$this->authenticatorClass ?? $this->type->value,
$this->firewallName,
);
}
}
30 changes: 30 additions & 0 deletions src/Security/Model/AuthenticatorType.php
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Security\Model;

/**
* @author Jesse Rushlow <jr@rushlow.dev>
*
* @internal
*/
enum AuthenticatorType: string
{
case FORM_LOGIN = 'form_login';
case JSON_LOGIN = 'json_login';
case HTTP_BASIC = 'http_basic';
case LOGIN_LINK = 'login_link';
case ACCESS_TOKEN = 'access_token';
case X509 = 'x509';
case REMOTE_USER = 'remote_user';

case CUSTOM = 'custom_authenticator';
}

0 comments on commit cbf2646

Please sign in to comment.