Skip to content

[make:auth] support new password hasher interface #824

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
Closed
1 change: 1 addition & 0 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public function generateClass(string $className, string $templateName, array $va
$variables = array_merge($variables, [
'class_name' => Str::getShortClassName($className),
'namespace' => Str::getNamespace($className),
'generator' => $this->templateComponentGenerator,
]);

$this->addOperation($targetPath, $templateName, $variables);
Expand Down
116 changes: 106 additions & 10 deletions src/Maker/MakeAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Bundle\MakerBundle\Maker;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
Expand All @@ -23,6 +24,7 @@
use Symfony\Bundle\MakerBundle\Security\SecurityControllerBuilder;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlManipulationFailedException;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
Expand All @@ -34,9 +36,30 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Yaml\Yaml;

/**
Expand Down Expand Up @@ -207,7 +230,8 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$input->getArgument('authenticator-type'),
$input->getArgument('authenticator-class'),
$input->hasArgument('user-class') ? $input->getArgument('user-class') : null,
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null
$input->hasArgument('username-field') ? $input->getArgument('username-field') : null,
$generator
);

// update security.yaml with guard config
Expand Down Expand Up @@ -257,7 +281,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
);
}

private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField)
private function generateAuthenticatorClass(array $securityData, string $authenticatorType, string $authenticatorClass, $userClass, $userNameField, Generator $generator): void
{
// generate authenticator class
if (self::AUTH_TYPE_EMPTY_AUTHENTICATOR === $authenticatorType) {
Expand All @@ -275,19 +299,91 @@ private function generateAuthenticatorClass(array $securityData, string $authent
'Entity\\'
);

$useStatements = [
RedirectResponse::class,
Request::class,
UrlGeneratorInterface::class,
TokenInterface::class,
Security::class,
TargetPathTrait::class,
];

$guardUseStatements = [
InvalidCsrfTokenException::class,
UsernameNotFoundException::class,
UserInterface::class,
UserProviderInterface::class,
CsrfToken::class,
CsrfTokenManagerInterface::class,
AbstractFormLoginAuthenticator::class,
];

$security53UseStatements = [
Response::class,
AbstractLoginFormAuthenticator::class,
CsrfTokenBadge::class,
UserBadge::class,
PasswordCredentials::class,
Passport::class,
PassportInterface::class,
];

$useStatements = array_merge($useStatements, ($this->useSecurity52 ? $security53UseStatements : $guardUseStatements));

$isEntity = $this->doctrineHelper->isClassAMappedEntity($userClass);
$hasEncoder = $this->userClassHasEncoder($securityData, $userClass);
$guardPasswordAuthenticated = $hasEncoder && interface_exists(
PasswordAuthenticatedInterface::class);

$guardTemplateVariables = [
'csrf_token_class_details' => $generator->createClassNameDetails(CsrfTokenManagerInterface::class, '\\'),
'user_class_name' => $userClassNameDetails->getShortName(),
'user_needs_encoder' => $hasEncoder,
'password_class_details' => $generator->createClassNameDetails(UserPasswordEncoderInterface::class, '\\'),
'password_variable_name' => 'passwordEncoder',
'user_is_entity' => $isEntity,
'provider_key_type_hint' => $this->providerKeyTypeHint(),
'password_authenticated' => $guardPasswordAuthenticated,
];

$security53TemplateVariables = [
'username_field_var' => Str::asLowerCamelCase($userNameField),
];

if (!$this->useSecurity52) {
if ($isEntity) {
$useStatements[] = $userClassNameDetails->getFullName();
$useStatements[] = EntityManagerInterface::class;
$guardTemplateVariables['entity_manager_class_details'] = $generator->createClassNameDetails(EntityManagerInterface::class, '\\');
}

if ($hasEncoder) {
$encoder = UserPasswordEncoderInterface::class;

if (interface_exists(UserPasswordHasherInterface::class)) {
$encoder = UserPasswordHasherInterface::class;
$guardTemplateVariables['password_class_details'] = $generator->createClassNameDetails(UserPasswordHasherInterface::class, '\\');
$guardTemplateVariables['password_variable_name'] = 'passwordHasher';
}

$useStatements[] = $encoder;
}

if ($guardPasswordAuthenticated) {
$useStatements[] = PasswordAuthenticatedInterface::class;
}
}

$this->generator->generateClass(
$authenticatorClass,
sprintf('authenticator/%sLoginFormAuthenticator.tpl.php', $this->useSecurity52 ? 'Security52' : ''),
[
'user_fully_qualified_class_name' => trim($userClassNameDetails->getFullName(), '\\'),
'user_class_name' => $userClassNameDetails->getShortName(),
array_merge([
'use_statements' => TemplateComponentGenerator::generateUseStatements($useStatements),
'url_generator_class_details' => $generator->createClassNameDetails(UrlGeneratorInterface::class, '\\'),
'username_field' => $userNameField,
'username_field_label' => Str::asHumanWords($userNameField),
'username_field_var' => Str::asLowerCamelCase($userNameField),
'user_needs_encoder' => $this->userClassHasEncoder($securityData, $userClass),
'user_is_entity' => $this->doctrineHelper->isClassAMappedEntity($userClass),
'provider_key_type_hint' => $this->providerKeyTypeHint(),
]
], ($this->useSecurity52 ? $security53TemplateVariables : $guardTemplateVariables)
)
);
}

Expand Down
39 changes: 13 additions & 26 deletions src/Resources/skeleton/authenticator/LoginFormAuthenticator.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,25 @@

namespace <?= $namespace ?>;

<?= $user_is_entity ? "use $user_fully_qualified_class_name;\n" : null ?>
<?= $user_is_entity ? "use Doctrine\\ORM\\EntityManagerInterface;\n" : null ?>
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
<?= $user_needs_encoder ? "use Symfony\\Component\\Security\\Core\\Encoder\\UserPasswordEncoderInterface;\n" : null ?>
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
<?= ($password_authenticated = $user_needs_encoder && interface_exists('Symfony\Component\Security\Guard\PasswordAuthenticatedInterface')) ? "use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;\n" : '' ?>
use Symfony\Component\Security\Http\Util\TargetPathTrait;
<?= $use_statements; ?>

class <?= $class_name; ?> extends AbstractFormLoginAuthenticator<?= $password_authenticated ? " implements PasswordAuthenticatedInterface\n" : "\n" ?>
{
use TargetPathTrait;

public const LOGIN_ROUTE = 'app_login';

<?= $user_is_entity ? " private \$entityManager;\n" : null ?>
private $urlGenerator;
private $csrfTokenManager;
<?= $user_needs_encoder ? " private \$passwordEncoder;\n" : null ?>
<?= $user_is_entity ? sprintf(" private %s\$entityManager;\n", $generator->getPropertyType($entity_manager_class_details)) : null ?>
private <?= $generator->getPropertyType($url_generator_class_details) ?>$urlGenerator;
private <?= $generator->getPropertyType($csrf_token_class_details) ?>$csrfTokenManager;
<?= $user_needs_encoder ? sprintf(" private %s$%s;\n", $generator->getPropertyType($password_class_details), $password_variable_name) : null?>

public function __construct(<?= $user_is_entity ? 'EntityManagerInterface $entityManager, ' : null ?>UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager<?= $user_needs_encoder ? ', UserPasswordEncoderInterface $passwordEncoder' : null ?>)
public function __construct(<?= $user_is_entity ? 'EntityManagerInterface $entityManager, ' : null ?>UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager<?= $user_needs_encoder ? sprintf(', %s $%s', $password_class_details->getShortName(), $password_variable_name) : null ?>)
{
<?= $user_is_entity ? " \$this->entityManager = \$entityManager;\n" : null ?>
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
<?= $user_needs_encoder ? " \$this->passwordEncoder = \$passwordEncoder;\n" : null ?>
<?= $user_needs_encoder ? sprintf(" \$this->%s = \$%s;\n", $password_variable_name, $password_variable_name) : null ?>
}

public function supports(Request $request)
Expand Down Expand Up @@ -81,10 +65,13 @@ public function getUser($credentials, UserProviderInterface $userProvider)

public function checkCredentials($credentials, UserInterface $user)
{
<?= $user_needs_encoder ? "return \$this->passwordEncoder->isPasswordValid(\$user, \$credentials['password']);\n"
: "// Check the user's password or other credentials and return true or false
<?php if ($user_needs_encoder): ?>
return $this-><?= $password_variable_name ?>->isPasswordValid($user, $credentials['password']);
<?php else : ?>
// Check the user's password or other credentials and return true or false
// If there are no credentials to check, you can just return true
throw new \Exception('TODO: check the credentials inside '.__FILE__);\n" ?>
throw new \Exception('TODO: check the credentials inside '.__FILE__);
<?php endif ?>
}

<?php if ($password_authenticated): ?>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,15 @@

namespace <?= $namespace ?>;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
<?= $use_statements; ?>

class <?= $class_name; ?> extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;

public const LOGIN_ROUTE = 'app_login';

private <?= $use_typed_properties ? 'UrlGeneratorInterface ' : null ?>$urlGenerator;
private <?= $generator->getPropertyType($url_generator_class_details) ?>$urlGenerator;

public function __construct(UrlGeneratorInterface $urlGenerator)
{
Expand Down