Alexandre Sequeira
Alexandre Sequeira

Reputation: 1

Try to create reset password but i've the same message error": "Invalid or expired reset token."

I have a project with a front-end in React.js and a back-end in Symfony 6.4 with the API Platform 3. I'm using a reset-password bundle. I've tested it with Insomnia, and I'm able to send an email with a link inside that contains a token. However, when I click on the link, I get the error "Invalid or expired reset token." I think I have a problem, but I don't know why. In my database, the token is not complete. For example, the link in the email looks like this: http://127.0.0.1:8000/reset-password/4ojbHunuRIBs81kHJRyTaR71NgWKgyayceG6ISPg But in my database, I have a column selector with the beginning of the token: 4ojbHunuRIBs81kHJRyT, and a column with the hashed token. Maybe it's not that the problem. I see a other think when i watch the time in my dev.log or in phpmyadmin, i've 2 hour différence with the time of my computer but when i watch tape date on the terminal in vs code in my project i've the good date and time. Can you help me fix this? My entity:

<?php

namespace App\Entity;

use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\ResetPasswordRequestRepository;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;

/**
 * @IgnoreAnnotation("ORM\Column")
 */
#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
#[ApiResource(
    operations: [
        new Post(
            uriTemplate: '/reset_password/request',
            status: 202,
        ),
        new Get(
            uriTemplate: '/reset_password/{token}',
            status: 202,
        ),
    ],
    output: false,
)]
class ResetPasswordRequest implements ResetPasswordRequestInterface
{
    use ResetPasswordRequestTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $user = null;

    public function __construct(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
    {
        $this->user = $user;
        $this->initialize($expiresAt, $selector, $hashedToken);
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUser(): object
    {
        return $this->user;
    }
}

My controller for create token and send a mail:

<?php

namespace App\Controller;

use App\Entity\User;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Address;
use App\Entity\ResetPasswordRequest;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;

class ForgotPasswordController extends AbstractController
{
    public function __construct(
        private ResetPasswordHelperInterface $resetPasswordHelper,
        private EntityManagerInterface $entityManager,
        private MailerInterface $mailer,
        private LoggerInterface $logger
    ) {
    }

    #[Route('/reset-password', name: 'app_forgot_password_request', methods: ['POST'])]
    public function request(Request $request): Response
    {
        $requestParams = $request->toArray();
        $email = $requestParams['email'] ?? null;
        $this->logger->info('Forgot password request email:', ['email' => $email]);

        if (!$email) {
            return new JsonResponse('No email provided.', Response::HTTP_BAD_REQUEST);
        }

        // Vérifier que l'email correspond à un utilisateur existant
        $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email]);
        $this->logger->info('Found user:', [
            'email' => $user?->getEmail(),
            'id' => $user?->getId(),
        ]);

        if (!$user) {
            return new JsonResponse('No user found for this email address.', Response::HTTP_BAD_REQUEST);
        }

        try {
            $resetToken = $this->resetPasswordHelper->generateResetToken($user);
        } catch (ResetPasswordExceptionInterface $e) {
            $this->logger->error('Error generating reset token:', ['exception' => $e]);
            return new JsonResponse(sprintf(
                '%s - %s',
                ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE,
                $e->getReason()
            ), Response::HTTP_BAD_REQUEST);
        }

        try {
            $emailMessage = $this->sendPasswordResetEmail($email, $this->mailer, $resetToken);
            $this->logger->info('Password reset email:', [
                'email' => $email,
                'message' => $emailMessage ? 'Email sent' : 'Email not sent',
            ]);
        } catch (\Exception $e) {
            $this->logger->error('Error sending password reset email:', ['exception' => $e]);
            return new JsonResponse('An error occurred while processing your request.', Response::HTTP_INTERNAL_SERVER_ERROR);
        }

        return new JsonResponse(['message' => 'Password reset email sent'], Response::HTTP_OK);
    }

    private function sendPasswordResetEmail(string $email, MailerInterface $mailer, ResetPasswordToken $resetToken)
    {
        $emailMessage = (new Email())
            ->from(new Address('[email protected]', 'admin bot'))
            ->to($email)
            ->subject('Your password reset request')
            ->text(sprintf(
                'To reset your password, please visit the following link: %s. This link will expire in %s.',
                $this->generateUrl('reset_password', ['token' => $resetToken->getToken()], UrlGeneratorInterface::ABSOLUTE_URL),
                $resetToken->getExpirationMessageKey()
            ));

        try {
            $mailer->send($emailMessage);
        } catch (\Exception $e) {
            $this->logger->error('Error sending password reset email:', ['exception' => $e]);
            return null;
        }

        return $emailMessage;
    }
}

And my controller for go to the page to update password and update the password:

<?php

namespace App\Controller;

use App\Entity\User;
use Psr\Log\LoggerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class ResetPasswordController extends AbstractController
{
    public function __construct(
        private ResetPasswordHelperInterface $resetPasswordHelper,
        private EntityManagerInterface $entityManager,
        private LoggerInterface $logger,
        private UserPasswordHasherInterface $passwordHasher
    ) {
    }

    #[Route('/reset-password/{token}', name: 'reset_password', methods: ['GET'])]
    public function reset(Request $request, string $token): Response
    {
        try {

            $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
            dd($token);
        } catch (ResetPasswordExceptionInterface $e) {
            $this->logger->error('Error validating reset token:', ['exception' => $e]);
            return $this->json([
                'error' => 'Invalid or expired reset token.'
            ], Response::HTTP_BAD_REQUEST);
        }

        // Afficher la page de réinitialisation du mot de passe
        return $this->json([
            'user' => $user,
        ]);
    }

    #[Route('/reset-password', name: 'update_password', methods: ['PATCH'])]
    public function updatePassword(Request $request): Response
    {
        $requestParams = $request->toArray();
        $token = $requestParams['token'] ?? null;
        $newPassword = $requestParams['new_password'] ?? null;

        if (!$token || !$newPassword) {
            return new JsonResponse('Invalid request data.', Response::HTTP_BAD_REQUEST);
        }

        try {
            $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
        } catch (ResetPasswordExceptionInterface $e) {
            $this->logger->error('Error validating reset token:', ['exception' => $e]);
            return new JsonResponse('Invalid or expired reset token.', Response::HTTP_BAD_REQUEST);
        }

        // Vérifier les contraintes du mot de passe
        $passwordPattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/';
        if (!preg_match($passwordPattern, $newPassword)) {
            return new JsonResponse('Le mot de passe doit contenir au moins 12 caractères, une majuscule, un chiffre et un caractère spécial', Response::HTTP_BAD_REQUEST);
        }

        // Hasher le nouveau mot de passe
        $hashedPassword = $this->passwordHasher->hashPassword($user, $newPassword);
        $user->setPassword($hashedPassword);

        $this->entityManager->flush();

        return new JsonResponse(['message' => 'Password updated successfully.'], Response::HTTP_OK);
    }
}

I've tried to modify the lifetime of the token, but nothing has changed. I've also tried to store the entire token in a single column in the database, but that didn't work either. Itry to debug in function reset the token is full I've tried to modify the timezone, which seemed to work fine since I found the same time, but it still didn't solve the issue. And since this project is intended to be international, I can't stay in the French time zone. I would like the token link to work properly. After that, I need to implement the update functionality.

Upvotes: 0

Views: 173

Answers (0)

Related Questions