Resetting Passwords
Resetting Passwords
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
To wrap up block 5, let's solve a problem EVERY application with login eventually needs: "forgot password?" – implemented securely, so nobody can take over someone else's account.
Installing the ResetPasswordBundle
composer require symfonycasts/reset-password-bundlephp bin/console make:reset-passwordGenerates a ResetPasswordRequest entity, a repository, a controller, AND the needed templates – a larger, multi-part workflow, which we'll follow step by step here.
The workflow at a glance
- The user enters their email address at
/reset-password. - Symfony generates a time-limited, ONE-TIME reset token and stores a HASH of it (not the token itself!) in the database.
- An email with a link (including the token) gets sent (chapter 38 covers sending email in depth).
- When the user clicks the link, Symfony checks the token against the stored hash.
- If the token is valid AND not expired, a new password may be set.
Why a hash of the token gets stored, not the token itself
Achtung: EXACTLY the same principle as with passwords (chapter 28): if the RAW reset token sat in the database, ANYONE with database access (leak, malicious insider) could use it to reset someone else's password THEMSELVES. The HASHED token in the database, in contrast, is worthless without knowing the original token (which is only sent via email).
Step 1: The reset request
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
#[Route('/reset-password', name: 'app_forgot_password_request')]
public function request(
Request $request,
ResetPasswordHelperInterface $resetPasswordHelper,
UserRepository $userRepository,
): Response {
if ($request->isMethod('POST')) {
$email = $request->request->get('email');
$user = $userRepository->findOneBy(['email' => $email]);
if ($user !== null) {
$resetToken = $resetPasswordHelper->generateResetToken($user);
// Send an email with $resetToken->getToken() (chapter 38)
}
// ALWAYS show the same message, regardless of whether the email exists!
$this->addFlash('info', 'If an account with this email exists, a message has been sent.');
return $this->redirectToRoute('app_login');
}
return $this->render('reset_password/request.html.twig');
}Achtung: The IDENTICAL success message, regardless of whether the email exists, is DELIBERATE: a different message ("email not found" vs. "message sent") would tell an attacker WHICH email addresses exist as registered accounts – a so-called "user enumeration" leak.
Step 2: Actually resetting the password
#[Route('/reset-password/reset/{token}', name: 'app_reset_password')]
public function reset(
string $token,
Request $request,
ResetPasswordHelperInterface $resetPasswordHelper,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $entityManager,
): Response {
try {
$user = $resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (\Exception) {
$this->addFlash('error', 'This link is invalid or has expired.');
return $this->redirectToRoute('app_forgot_password_request');
}
if ($request->isMethod('POST')) {
$newPassword = $request->request->get('password');
$user->setPassword($passwordHasher->hashPassword($user, $newPassword));
$resetPasswordHelper->removeResetRequest($token);
$entityManager->flush();
$this->addFlash('success', 'Your password has been changed successfully.');
return $this->redirectToRoute('app_login');
}
return $this->render('reset_password/reset.html.twig');
}validateTokenAndFetchUser() AUTOMATICALLY checks: does a matching hash exist? Has it not expired yet (default: 1 hour)? removeResetRequest() INVALIDATES the token after successful use – a reset link works EXACTLY ONCE, not repeatedly.
Configuring the expiry time
reset_password:
request_password_repository: App\Repository\ResetPasswordRequestRepository
lifetime: 3600 # 1 hour in seconds
throttle_limit: 3600 # prevents repeated requests within this timeframethrottle_limit is an additional protection against abuse: prevents countless reset emails from being triggered for the SAME email address in quick succession (which would be both a spam vector and a resource exhaustion risk).
With that, block 5 (security & authentication) is complete! Our task manager now has full user management: registration, login, object-based access control, API authentication, and password reset. Block 6 covers Symfony's actual centerpiece – services, dependency injection, and events.