Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 30 additions & 33 deletions src/Maker/MakeRegistrationForm.php
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAuthenticatorsInFirewallsConfig?

{
$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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh... lost the review comment on push...

@weaverryan:
I'm actually a bit lost in this method. We're looping over the keys under a firewall, right? And the first if statement with tryFrom will exit unless the key matches the specific set we have in AuthenticatorType, right? So then, $configData is represents the values beneath those keys. So if I have:

firewalls:
    main:
        form_login:
            path: /login

Won't $configData at this point be ['path' => '/login']? Or am I totally misreading the method?

Copy link
Collaborator Author

@jrushlow jrushlow Mar 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I think you're thinking 1 level too deep..

i refactored this to split up iterating over firewalls, the keys of firewalls (main, anotherFirewall), and the custom_authenticators

We pass in security.firewalls -> iterate over each key in firewalls to give us:

main:
    path: '/some-path'
    form_login:
        - option
        ....
    custom_authenticators:
        - some authenticator    
anotherFirewall:
dev:
test:

tryFrom($potentialAuthenticator) checks if the "key" is form_login, custom_authenticator, or path. If it is path, tryFrom() returns null and we continue.

Now $potentialAuthenticator is either form_login or custom_authenticator and $configData is the array of either of their stuff..

If its form_login, we create the value object and add it to authenticators[] otherwiser we now know that $potentialAuthenticator === 'custom_authenticator' and configData is either (string) MyCustom or [MyCustom, NotSureWhyYouCanHave2ButYouCan]

Atleast this is what im trying todo, but i could be missing something myself 🤕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, it looks clear now 👍

// 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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\Security\Model;

/**
* @author Jesse Rushlow<[email protected]>
*
* @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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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\Security\Model;

/**
* @author Jesse Rushlow <[email protected]>
*
* @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';
}
Loading