Authentication
Registration, login, email verification and password reset.
Available flows
Authentication is built on Symfony Security. Registration, login, email verification and password reset are ready to use.
| Feature | Detail |
|---|---|
| Email / password login | src/Controller/Auth/LoginController.php |
| Registration with email verification | src/Controller/Auth/RegistrationController.php |
| Email address confirmation | src/Controller/Auth/EmailVerificationController.php |
| Password reset | src/Controller/Auth/PasswordResetController.php |
| Registration service | src/Service/Auth/RegistrationService.php |
| Reset service | src/Service/Auth/PasswordResetService.php |
| Verification email | src/Mail/Auth/VerificationEmailMailer.php |
| Reset email | src/Mail/Auth/PasswordResetEmailMailer.php |
| Email event subscriber | src/EventSubscriber/Auth/SendVerificationEmailSubscriber.php |
| Reset event subscriber | src/EventSubscriber/Auth/SendPasswordResetEmailSubscriber.php |
| Login rate limiter | src/Security/LoginRateLimiterSubscriber.php |
| Reset token | src/Entity/Auth/PasswordResetToken.php |
Login
Login relies on Symfony Security's form_login. The form submits _email and _password fields; the firewall verifies credentials, manages the remember me cookie and the session. No custom controller required.
Form fields
| Field | Constraint |
|---|---|
| Email address | Required, email autocomplete |
| Password | Required, visibility toggle |
| Remember me | 30-day cookie (kernel.secret) |
Rate limiting
Two independent counters protect the login route: one per IP and one per email (5 attempts / 15 min each). The subscriber intercepts CheckPassportEvent before credential verification, consumes a token on LoginFailureEvent, and resets both counters on LoginSuccessEvent.
src/Security/LoginRateLimiterSubscriber.php
Remember me
The remember me cookie is set to 30 days and signed with kernel.secret. It is cleared on logout.
config/packages/security.yaml
Registration
The registration form collects first name, last name, email and password. Data is validated server-side, the password is hashed, the user is stored, then a signed verification email is dispatched via an event.
Form fields
| Field | Constraint |
|---|---|
| First name | Required, 2–100 chars |
| Last name | Required, 2–100 chars |
| Email address | Valid format, max 180 chars |
| Password (repeated) | Min. 8 characters |
Anti-enumeration: the same redirect is returned whether or not the email already exists in the database.
Rate limiting
Registration is protected by a token bucket limiter: 5 attempts per 15 minutes per IP.
config/packages/framework.yaml
Password strength
A Stimulus controller evaluates password strength client-side (length ≥ 8/12, uppercase, digit, special char). Labels are passed via data-* attributes for full i18n support.
assets/controllers/password_strength_controller.js
Email verification
After registration, a time-limited signed URL is sent by email. The signature uses HMAC-SHA256 keyed with APP_SECRET and includes a 24-hour expiry timestamp — no database token required.
The signature is compared with hash_equals() to prevent timing attacks.
After successful verification, the user is automatically logged in via Security::login() — no second login step needed.
Events
| Feature | Detail |
|---|---|
UserRegisteredEvent |
Dispatched after registration — triggers the verification email via SendVerificationEmailSubscriber. |
UserEmailVerifiedEvent |
Dispatched after email verification — hook here to start onboarding flows. |
Translations
Auth strings live in a dedicated translation domain, separate from the boilerplate UI strings. Validator constraint messages use the standard validators domain:
| File | Language |
|---|---|
translations/auth.fr.yaml | Français |
translations/auth.en.yaml | English |
translations/validators.fr.yaml | Français |
translations/validators.en.yaml | English |
Password reset
The flow relies on a 64-character random token stored in the database as a SHA-256 hash. The link sent by email contains the raw token, which expires after 1 hour. After use, the token is soft-deleted (deleted_at field).
Full flow
- The user submits their email on /auth/forgot-password
- Rate limiting checked (3 req / 1h per IP + per email)
- Token generated (bin2hex(random_bytes(32))), SHA-256 hash stored in the database, email dispatched via PasswordResetRequestedEvent
- The user clicks the link → new password form
- Token validated (hash + expiry + deleted_at IS NULL), password updated, token soft-deleted, auto-login via Security::login()
PasswordResetToken entity
The primary key is the email — only one active token per address at a time. The deleted_at field ensures soft-delete after use. Expired tokens are cleaned up by a scheduled task (deleteExpired()).
src/Entity/Auth/PasswordResetToken.php
Rate limiting
Two independent counters on /auth/forgot-password: 3 requests per hour per IP and per email. Both must pass.
config/packages/framework.yaml
Anti-enumeration: the same check-reset-email page is returned whether or not the email exists in the database.
Symfony Security configuration
Firewalls, encoders and access controls are configured in:
config/packages/security.yaml
The dev firewall allows access to assets, the profiler and the documentation without authentication.
User entity
The User entity implements UserInterface and PasswordAuthenticatedUserInterface. The preferredLocale field stores the user's locale at registration time, used to send transactional emails in the correct language:
src/Entity/Auth/User.php