Password reset email: a secure flow end to end
Updated
A reset email is a login credential in disguise. Anyone who can read it can take over the account, so the flow matters more than the template.
When it fires
The user submits the "forgot password" form. The email must arrive quickly, and the same response must be shown whether or not the address is registered.
Workflow
- Receive the address. Always return the same generic message.
- If an account exists, generate a random token, store its hash with a 60 minute expiry.
- Email the reset link.
- On submit of the new password, verify the token hash and expiry, set the password, delete the token, and invalidate existing sessions.
- Send a security alert confirming the password changed.
Implementation (PHP 8, cURL)
<?php
function sendResetEmail(PDO $db, array $user): void {
$token = bin2hex(random_bytes(32));
$db->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, ?)')
->execute([$user['id'], hash('sha256', $token), date('c', time() + 3600)]);
$link = 'https://app.yourdomain.com/reset?token=' . $token;
$ch = curl_init('https://api.inboxili.com/api/v1/transactional/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOXILI_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'to' => $user['email'],
'from_email' => 'security@yourdomain.com',
'from_name' => 'Acme',
'subject' => 'Reset your Acme password',
'html_body' => '<p>Use this link to choose a new password: <a href="{{link}}">Reset password</a>. It expires in 60 minutes. If you did not ask for this, you can ignore this email.</p>',
'text_body' => "Reset your password: $link",
'template_data' => ['link' => $link],
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
error_log('Inboxili send failed: ' . $body); // log it, but do not reveal it to the user
}
}
Sample email
Subject: Reset your Acme password Use this link to choose a new password: Reset password. It expires in 60 minutes. If you did not ask for this, you can ignore this email.
Things that go wrong
- Account enumeration through different messages or response times. Queue the send and return immediately.
- Reusable tokens. Delete on use.
- Host header injection building the link. Use a configured base URL.
- Silent failures. Log the API error server-side, since the user-facing message is deliberately vague.
Delivery events
Track bounced for reset emails. Repeated bounces for an account can indicate someone is typing addresses that are not theirs.
Deliverability notes
Use a consistent From address on a verified domain and keep the body free of marketing. Users search their inbox for "reset", so keep the subject plain.
Frequently asked questions
- Should the page say whether the email exists?
- No. Respond with the same message either way: 'If that address has an account, we have sent a reset link.' Otherwise your form lets anyone enumerate your users.
- How long should a reset link last?
- Short: 30 to 60 minutes. Make it single use.
Send a password reset email
Create a workspace, verify a domain, and make your first API call.