Forgot password
Since 0.26 the panel's login page can show Forgot password?. A person types the address they sign in with, gets a one-time link by email, and chooses a new password. Saving it signs that account out everywhere else.
Requires flocms-core 2.5 (for the two pages that open without signing in, and for ending other sessions) and the panel's login styling ('login_theme' => true).
Turning it on
New installations have it in config/admin.php. An existing site adds:
'password_reset' => true,
then runs:
php flo admin:install (writes the migration of the admin_password_resets table)
php flo migrate
php flo admin:doctor (checks the table, the email settings and APP_URL)
php flo admin:mail-test you@example.com
Until the table exists the login page shows no link, and /admin/password/forgot says that password reset is not available.
Options (all optional):
'password_reset' => [
'expires' => 60, // minutes a link works (5 to 1440)
'per_email' => 3, // requests per address per hour
'per_ip' => 10, // requests per IP address per hour
'table' => 'admin_password_resets',
],
'password_reset' => false switches it off.
The panel sends two emails: the link, and a short note after the password changed ("If this wasn't you, contact your site administrator"). They are written in the language of the page the person used, in the panel's colours, with a plain-text version. Settings come from .env:
MAIL_TRANSPORT=smtp # mail (PHP mail(), the default), smtp or log
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls # tls (STARTTLS, port 587), ssl (port 465) or none
MAIL_USERNAME=no-reply@example.com
MAIL_PASSWORD=…
MAIL_FROM=no-reply@example.com # default: PRIMARY_EMAIL
MAIL_FROM_NAME="My site" # default: the panel's brand
- cPanel:
mail()usually works as it is. If messages land in spam, create a mailbox in cPanel and use its SMTP settings. logwrites the messages tostorage/logs/admin-mail.loginstead of sending them: for a computer without mail (XAMPP). Never on a live site: the log holds the links.admin:doctorwarns whenAPP_ENV=production.APP_URLmust be the site's full address (https://example.com): it starts the link.- Your own mailer (PHPMailer, an email API): set
'mail' => ['send' => fn (\FloCMS\Admin\Mail\Message $message) => …]inconfig/admin.php. The message hasto,toName,subject,textandhtml.
What the person sees
- Forgot password? on the login page opens
/admin/password/forgot. - They type their address. The answer is always the same, "If an account exists for … we've sent it a link", whether the address has an account or not, so the form can't be used to find out who has one. The email is sent after the page has been answered (on PHP-FPM), so both cases also take the same time.
- The link opens
/admin/password/reset/<token>: a new password, typed twice, at least 8 characters (as on the profile page), with a show/hide button. - Saving sends them to the login page with "Your password has been changed. You can sign in now." They are not signed in automatically, so the usual login checks (status, role, throttling) still apply.
A used, expired or wrong link shows "This link no longer works" with a button to request a new one. All pages work without JavaScript, right-to-left, and on phones.
Security
- Only hashes are stored: of the address and the IP address (keyed with
APP_KEY), and of the link's token (SHA-256). Rows are deleted after a day. - A link works once and for
expiresminutes; a newer link replaces older ones; two tabs saving at the same moment can't both use it. - Emails go only to active accounts (status 1). Pending and suspended accounts get the same answer and no email.
- Limits: 3 requests per address and 10 per IP address an hour (IPv6 per /64); over a limit the answer stays the same and no email is sent. Wrong links are limited per IP address too: after 10 wrong links in an hour, links opened from that address stop working for the rest of the hour, so people behind one shared address (an office) may have to wait. Behind a proxy, list it in
TRUSTED_PROXIESso the visitor's address is used. Two requests at the very same moment can go one over a limit. - Same timing: the session is saved and the answer sent before the email goes out (PHP-FPM, LiteSpeed, and Apache when output compression doesn't hold the answer back), so a known address doesn't answer more slowly than an unknown one.
- The two pages open without signing in only when the panel's own controller serves them (flocms-core 2.5 binds them to it): a site's own
PasswordControllerstays behind the login. - SMTP never sends the password without TLS, except to a relay on the same server (
localhost). - The pages are not cached, not indexed, can't be framed, and send no referrer, so the token in the address never reaches another site.
- Other sessions end: each signed-in session keeps a fingerprint of the account's password hash, which FloCMS checks when it reloads the user on admin requests. A session gets its fingerprint on its first admin request, which is the page the login opens. When the password changes (by a reset link, on the profile page, or by an admin on Users) the account's other sessions end with "Your session has ended. Please log in again." The person who changed their own password stays signed in. With
auth.refresh_intervalabove 0, other sessions end at their next refresh instead of at once. A PHP upgrade that rehashes passwords at login ends a user's other sessions once.
Activity log
- Reset link (
password_reset_requested): a link was sent, credited to the account. Requests for an address without an account, or for an inactive account, are logged without an email being sent; the address itself is never written down. - Reset limit (
password_reset_limited): logged once when a limit is reached. - Password reset (
password_reset): the password was changed with a link.
Pages and URLs
| URL | Who |
|---|---|
/{lang}/admin/password/forgot |
anyone (a signed-in admin is sent to the dashboard) |
/{lang}/admin/password/forgot/sent |
after a request |
/{lang}/admin/password/reset/<token> |
anyone with the link |
The slug password is reserved for these pages. A site's own PasswordController always wins (admin:doctor says so).