I have set up a new 6.1 site from scratch and am trying to implement security. I have a user database from a previous site in Symfony 3.4 that I'm trying to use that contains existing password hashes and salts - I want to continue using the same hashes, so using the sha1 algorithm (will consider this later) Upgrade the hashing algorithm). Attempting to log in always returns the following:
#message: "The presented password is invalid." #code: 0 #file: "/** redacted **/vendor/symfony/security-http/EventLis tener/CheckCredentialsLis tener.php" #line: 69 #serialized: null -token: null trace: {▼ /** redacted **/vendor/symfony/security-http/EventLis tener/CheckCredentialsLis tener.php:69 {▶} /** redacted **/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:175 {▶} /** redacted **/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:326 {▶} /** redacted **/vendor/symfony/http-foundation/Session/Session.php:258 {▶} /** redacted **/vendor/symfony/http-foundation/Session/Session.php:278 {▶} /** redacted **/vendor/symfony/http-foundation/Session/Session.php:70 {▶} /** redacted **/vendor/symfony/security-http/Authentication/AuthenticationUtils.php:40 {▶} /** redacted **/src/Controller/SecurityController.php:33 {▶} /** redacted **/vendor/symfony/http-kernel/HttpKernel.php:153 {▶} /** redacted **/vendor/symfony/http-kernel/HttpKernel.php:75 {▶} /** redacted **/vendor/symfony/http-kernel/Kernel.php:202 {▶} /** redacted **/vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php:35 {▶} /** redacted **/vendor/autoload_runtime.php:29 {▶} /** redacted **/public/index.php:5 {▶} } }
This seems simple, it doesn't recognize the password. However, the password is the same as the one in the database used by the Symfony 3.4 version site, and uses the exact same password hasher.
This is basically a site out of the box, I haven't done any configuration other than trying to get the security to work, I followed the documentation exactly.
This is my security.yaml:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: AppEntityUsers: algorithm: sha1 iterations: 1 encode_as_base64: false # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: app_user_provider: entity: class: AppEntityUsers property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true provider: app_user_provider form_login: login_path: login check_path: login enable_csrf: false login_throttling: max_attempts: 3 # per minute interval: '15 minutes'
Here is SecurityController.php (contains routing/login):
namespace AppController; use AppEntityOffice; use DoctrinePersistenceManagerRegistry; use SymfonyBundleFrameworkBundleControllerAbstractController; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; use SymfonyComponentHttpFoundationSessionSession; use SymfonyComponentHttpFoundationSessionStorageHandlerNativeFileSessionHandler; use SymfonyComponentHttpFoundationSessionStorageNativeSessionStorage; use SymfonyComponentRoutingAnnotationRoute; use SymfonyComponentSecurityCoreSecurity; use SymfonyComponentSecurityHttpAuthenticationAuthenticationUtils; use PsrLogLoggerInterface; class SecurityController extends AbstractController { public function __construct(private ManagerRegistry $doctrine, private LoggerInterface $logger) {} #[Route('/login', name: 'login')] public function login(Request $request, AuthenticationUtils $authenticationUtils): Response { $sessionStorage = new NativeSessionStorage([], new NativeFileSessionHandler()); $session = new Session($sessionStorage); $doctrine = $this->doctrine; $logger = $this->logger; $logger->info('loginAction'); // get the login error if there is one $error = $authenticationUtils->getLastAuthenticationError(); // last username entered by the user $lastUsername = $authenticationUtils->getLastUsername(); return $this->render( 'security/login.html.twig', [ 'controller_name' => 'SecurityController', // last username entered by the user 'last_username' => $lastUsername, 'error' => $error, ] ); } }
This is my login.html.twig:
{% block PageHeader %}{% endblock %} {% block PageContent %} <div style="min-height: 250px;"> <h1>{{ 'security.login.title'|trans({}) }}</h1> <br /> {% if error %} {{ dump(error) }} {% if error.messageKey is defined %} <div class="error">{{ error.messageKey|trans(error.messageData) }}</div> {% endif %} {% endif %} <form action="{{ path('login') }}" method="post" class="login"> <label for="username">{{ 'security.login.username'|trans({}) }}</label> <input type="text" id="username" name="_username" value="{{ last_username }}" required="required" /> <br /><br /> <label for="password">{{ 'security.login.password'|trans({}) }}</label> <input type="password" id="password" name="_password" required="required" /> <br /> <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"> <input type="submit" id="_submit" name="_submit" value="{{ 'security.login.submit'|trans({}) }}" /> </form> </div> {% endblock %} {% block PageFooter %}{% endblock %}
This is my user class:
namespace AppEntity; use DoctrineCommonCollectionsArrayCollection; use DoctrineCommonCollectionsCollection; use DoctrineDBALTypesTypes; use DoctrineORMMapping as ORM; use AppRepositoryUsersRepository; use SymfonyComponentSecurityCoreUserPasswordAuthenticatedUserInterface; use SymfonyComponentSecurityCoreUserUserInterface; /** * Users * * @ORMTable(name="Users", indexes={@ORMIndex(name="IDX_D5428AED73FD6E34", columns={"Office"})}) * @ORMEntity(repositoryClass="AppRepositoryUsersRepository") */ class Users implements UserInterface, PasswordAuthenticatedUserInterface { /** * @var int * * @ORMColumn(name="UserId", type="integer", nullable=false) * @ORMId * @ORMGeneratedValue(strategy="IDENTITY") */ private $userid; /** * @var string|null * * @ORMColumn(name="Firstname", type="string", length=100, nullable=true) */ private $firstname; /** * @var string|null * * @ORMColumn(name="Surname", type="string", length=100, nullable=true) */ private $surname; /** * @var string * * @ORMColumn(name="Email", type="string", length=150, nullable=false) */ private $email; /** * @var string|null * * @ORMColumn(name="JobTitle", type="string", length=150, nullable=true) */ private $jobtitle; /** * @var string|null * * @ORMColumn(name="Password", type="string", length=150, nullable=true) */ private $password; /** * @var string|null * * @ORMColumn(name="Salt", type="string", length=50, nullable=true) */ private $salt; /** * @var int|null * * @ORMColumn(name="Status", type="integer", nullable=true) */ private $status; /** * @var DateTime|null * * @ORMColumn(name="CreatedOn", type="datetime", nullable=true) */ private $createdon; /** * @var int|null * * @ORMColumn(name="CreatedBy", type="integer", nullable=true) */ private $createdby; /** * @var DateTime|null * * @ORMColumn(name="LastUpdatedOn", type="datetime", nullable=true) */ private $lastupdatedon; /** * @var int|null * * @ORMColumn(name="LastUpdatedBy", type="integer", nullable=true) */ private $lastupdatedby; /** * @var DateTime|null * * @ORMColumn(name="Deleted", type="datetime", nullable=true) */ private $deleted; /** * @var Office * * @ORMManyToOne(targetEntity="Office") * @ORMJoinColumns({ * @ORMJoinColumn(name="Office", referencedColumnName="OfficeId") * }) */ private $office; /** * @var DoctrineCommonCollectionsCollection * * @ORMManyToMany(targetEntity="Usergroup", inversedBy="userid") * @ORMJoinTable(name="usergroupmap", * joinColumns={ * @ORMJoinColumn(name="UserId", referencedColumnName="UserId") * }, * inverseJoinColumns={ * @ORMJoinColumn(name="UserGroup", referencedColumnName="UserGroupId") * } * ) */ private $usergroup = array(); /** * Constructor */ public function __construct() { $this->usergroup = new DoctrineCommonCollectionsArrayCollection(); } public function getUserid(): ?int { return $this->userid; } public function getFirstname(): ?string { return $this->firstname; } public function setFirstname(?string $firstname): self { $this->firstname = $firstname; return $this; } public function getSurname(): ?string { return $this->surname; } public function setSurname(?string $surname): self { $this->surname = $surname; return $this; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): self { $this->email = $email; return $this; } public function getJobtitle(): ?string { return $this->jobtitle; } public function setJobtitle(?string $jobtitle): self { $this->jobtitle = $jobtitle; return $this; } public function getPassword(): ?string { return $this->password; } public function setPassword(?string $password): self { $this->password = $password; return $this; } public function getSalt(): ?string { return $this->salt; } public function setSalt(?string $salt): self { $this->salt = $salt; return $this; } public function getStatus(): ?int { return $this->status; } public function setStatus(?int $status): self { $this->status = $status; return $this; } public function getCreatedon(): ?DateTimeInterface { return $this->createdon; } public function setCreatedon(?DateTimeInterface $createdon): self { $this->createdon = $createdon; return $this; } public function getCreatedby(): ?int { return $this->createdby; } public function setCreatedby(?int $createdby): self { $this->createdby = $createdby; return $this; } public function getLastupdatedon(): ?DateTimeInterface { return $this->lastupdatedon; } public function setLastupdatedon(?DateTimeInterface $lastupdatedon): self { $this->lastupdatedon = $lastupdatedon; return $this; } public function getLastupdatedby(): ?int { return $this->lastupdatedby; } public function setLastupdatedby(?int $lastupdatedby): self { $this->lastupdatedby = $lastupdatedby; return $this; } public function getDeleted(): ?DateTimeInterface { return $this->deleted; } public function setDeleted(?DateTimeInterface $deleted): self { $this->deleted = $deleted; return $this; } public function getOffice(): ?Office { return $this->office; } public function setOffice(?Office $office): self { $this->office = $office; return $this; } /** * @return Collection<int, Usergroup> */ public function getUsergroup(): Collection { return $this->usergroup; } public function addUsergroup(Usergroup $usergroup): self { if (!$this->usergroup->contains($usergroup)) { $this->usergroup->add($usergroup); } return $this; } public function removeUsergroup(Usergroup $usergroup): self { $this->usergroup->removeElement($usergroup); return $this; } /** * @see UserInterface */ public function eraseCredentials(): array { // If you store any temporary, sensitive data on the user, clear it here } /** * @see UserInterface */ public function getRoles(): array { foreach ($this->usergroup as $key => $value) { $roles[] = $value->getUsergroup(); } return array_unique($roles); } /** * A visual identifier that represents this user. * * @see UserInterface */ public function getUserIdentifier(): string { return (string) $this->email; } }
Does anyone know how to solve this problem?
solved! The first thing is to make my User class inherit
LegacyPasswordAuthenticatedUserInterface
instead ofPasswordAuthenticatedUserInterface
.The second thing is to look at
/vendor/symfony/password-hasher/Hasher/MessageDigestPasswordHasher.php
and in theverify
function, comment out the beginning block lineif (\strlen($hashedPassword) !== $this->hashLength || str_contains($hashedPassword, '$')) {
.This block permanently returns false because
$this->hashLength
is always -1.$this->hashLength
is set in the constructor, I don't know why it always returns -1 or if that check is valid, but commenting it out allowed me to log in : )