Installation
composer require ayto/reset-password-bundle
Add to config/bundles.php:
Ayto\ResetPasswordBundle\ResetPasswordBundle::class => ['all' => true],
Configure the Bundle
Create config/packages/reset_password.yaml:
reset_password:
from_email: 'no-reply@example.com'
token_lifetime: 3600
user_class: App\Entity\User
Implement ResetPasswordUserInterface
Extend your User entity:
use Ayto\ResetPasswordBundle\Model\ResetPasswordUserInterface;
class User implements ResetPasswordUserInterface {
// Required methods: getResetPasswordToken(), setResetPasswordToken(),
// getResetPasswordTokenExpiresAt(), setResetPasswordTokenExpiresAt(), getEmail()
}
Add Routes
Include in config/routes.yaml:
reset_password:
resource: '@ResetPasswordBundle/Resources/config/routes.yaml'
Trigger First Reset Send a test email to a user via:
php bin/console reset-password:send-email user@example.com
(Check bin/console for available commands.)
User Requests Reset
/reset-password/request (auto-routed).User entity, and sends an email.User Clicks Reset Link
/reset-password/reset/{token}.Password Update
User entity.Extend User Entity
Ensure your User entity implements ResetPasswordUserInterface and includes:
#[ORM\Column(nullable: true)]
private ?string $resetToken = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeInterface $resetTokenExpiresAt = null;
Customize Email Templates Override default templates in:
templates/bundles/ResetPasswordBundle/emails/reset_password.html.twig
Example:
{# Extend base template #}
{% extends '@ResetPasswordBundle/emails/reset_password.html.twig' %}
{# Customize subject/body #}
{% block subject %}Reset Your Password on {{ app.name }}{% endblock %}
Integrate with API Platform
Add a PasswordResetController to handle token validation via API:
#[ApiResource]
class PasswordResetController extends AbstractController {
#[Route('/api/reset-password/validate/{token}', name: 'api_reset_password_validate')]
public function validateToken(string $token): JsonResponse {
$user = $this->get('reset_password.manager')->findUserByResetToken($token);
return $this->json(['valid' => $user !== null]);
}
}
JWT Token Handling After password reset, force JWT token reissue:
#[Route('/api/reset-password/update', name: 'api_reset_password_update', methods: ['POST'])]
public function updatePassword(Request $request): JsonResponse {
$data = json_decode($request->getContent(), true);
$token = $data['token'];
$newPassword = $data['password'];
$this->get('reset_password.manager')->resetPassword($token, $newPassword);
// Log out old JWT sessions (if using stateless auth)
return $this->json(['message' => 'Password updated. Log in again.']);
}
Event Listeners for Side Effects
Listen to ResetPasswordEvents to trigger actions like:
use Ayto\ResetPasswordBundle\Event\PasswordResetEvent;
class PasswordResetLogger {
public function onPasswordReset(PasswordResetEvent $event) {
// Log or notify
}
}
Register in services.yaml:
services:
App\EventListener\PasswordResetLogger:
tags:
- { name: 'kernel.event_listener', event: 'reset_password.password_reset', method: 'onPasswordReset' }
Doctrine Lifecycle Callbacks Auto-generate tokens on user creation:
#[ORM\PrePersist]
public function prePersist(): void {
$this->resetToken = bin2hex(random_bytes(32));
$this->resetTokenExpiresAt = new \DateTime('+1 hour');
}
Rate Limiting
Use Symfony’s RateLimiter to prevent brute-force attacks on /reset-password/request:
# config/packages/security.yaml
firewalls:
main:
pattern: ^/reset-password
rate_limiter: reset_password_limiter
Testing
Mock the ResetPasswordManager in PHPUnit:
$manager = $this->createMock(ResetPasswordManagerInterface::class);
$manager->method('sendResetEmail')->willReturn(true);
$this->container->set('reset_password.manager', $manager);
Token Expiration
token_lifetime (3600s). Ensure your frontend handles expired tokens gracefully.resetTokenExpiresAt in your User entity.Email Delivery Failures
from_email is valid in reset_password.yaml.config/packages/mailer.yaml).php bin/console debug:mailer to test email transport.Token Leaks
/reset-password/reset/{token}). Use POST requests for production to avoid logging.{# templates/bundles/ResetPasswordBundle/reset_password/reset.html.twig #}
<form method="POST" action="{{ path('reset_password_reset', { token: token }) }}">
User Entity Mismatch
user_class in config doesn’t match your actual User entity, the bundle throws RuntimeException.reset_password.yaml.JWT Session Conflicts
// In your PasswordResetController
$this->get('lexik_jwt_authentication.jwt_manager')->invalidateToken($oldToken);
Token Validation Errors
resetTokenExpiresAt is in the past or resetToken is missing.php bin/console debug:container reset_password.manager
Call findUserByResetToken($token) manually to inspect user data.Template Overrides Not Working
bundles/ResetPasswordBundle/).php bin/console cache:clear
Email Not Customized
reset_password.html.twig).dump() to debug variables:
{{ dump(token) }}
Custom Token Generator Override the default token generation (32-byte random string):
// src/Service/CustomTokenGenerator.php
class CustomTokenGenerator implements TokenGeneratorInterface {
public function generateToken(): string {
return base64_encode(random_bytes(24)); // Custom logic
}
}
Register as a service:
services:
Ayto\ResetPasswordBundle\Service\TokenGenerator:
class: App\Service\CustomTokenGenerator
Additional User Fields
Extend ResetPasswordUserInterface to add fields like resetAttempts:
interface ExtendedResetPasswordUserInterface extends ResetPasswordUserInterface {
public function getResetAttempts(): int;
public function incrementResetAttempts(): void;
}
Update your User entity and bundle config to use the new interface.
Multi-Factor Authentication (MFA) Extend the reset flow to require MFA:
// In your PasswordResetController
public function resetPassword
How can I help you explore Laravel packages today?