Updated 2 October 2026
Secure Password Reset Flow: Preventing Account State Exposure
Password reset flows are among the most frequently attacked authentication endpoints. When implemented incorrectly, they leak account existence information to attackers, enabling credential stuffing and enumeration attacks. A secure password reset implementation must treat every request as potentially malicious, returning uniform responses regardless of whether the submitted identifier corresponds to an existing account.
Why Account State Exposure Matters
Attackers automate queries against password reset endpoints to discover valid user accounts. If the system returns different responses for existing versus non existent accounts, an attacker can enumerate a database of valid email addresses. This enumeration becomes the foundation for credential stuffing, phishing campaigns, and targeted account takeover attempts. The OWASP Authentication Cheat Sheet explicitly recommends defending against account enumeration by returning identical responses regardless of whether an account exists.
The application should return the same response for both valid and invalid username/email inputs to prevent account enumeration. The response may take longer for valid accounts to avoid timing attacks, but the same HTTP status code and message should be returned.
Beyond enumeration, password reset tokens represent a high value target. If tokens are predictable, reusable, or lack proper expiration, attackers can hijack the flow to take over accounts en masse. The OWASP Forgot Password Cheat Sheet provides specific guidance on token security, emphasizing that tokens must be single use, cryptographically random, and bound to a specific user or session.
Designing the Reset Request Endpoint
The reset request endpoint accepts a user identifier, typically an email address, and initiates the recovery process. This endpoint must implement three critical protections: uniform response handling, rate limiting, and secure token generation.
Uniform response handling means the endpoint returns the same HTTP status code, response body, and response time regardless of whether the submitted email exists in your database. A user who submits an invalid email should receive the same message as a user with a valid account: "If an account exists with this email, we have sent reset instructions."
Rate limiting prevents automated enumeration. Apply strict request limits per IP address and per identifier. The OWASP Forgot Password Cheat Sheet recommends implementing rate limits on the forgot password request to prevent attackers from testing thousands of email addresses in sequence.
- Return identical HTTP 200 status for all valid reset requests
- Add artificial delay to normalize response times between existing and non existing accounts
- Limit requests to 3 to 5 attempts per hour per IP address
- Limit requests to 1 attempt per hour per account identifier
- Use CAPTCHA for suspicious traffic patterns
Generating Secure Reset Tokens
Reset tokens must meet stringent security requirements. The OWASP Forgot Password Cheat Sheet specifies that tokens should be generated using a cryptographically secure random number generator, stored as hashes in your database, and include sufficient entropy to resist brute force attacks.
Store tokens as hashed values rather than plaintext. When a user requests a password reset, generate a random token, store its hash in the database alongside the user ID and expiration timestamp, and send the raw token to the user's email address. This ensures that even if your database is compromised, reset tokens remain unusable.
import { randomBytes } from 'crypto';
import { createHash } from 'crypto';
interface ResetToken {
token: string;
tokenHash: string;
userId: string;
expiresAt: Date;
used: boolean;
}
function generateResetToken(): { token: string; tokenHash: string } {
const token = randomBytes(32).toString('hex');
const tokenHash = createHash('sha256').update(token).digest('hex');
return { token, tokenHash };
}
export async function createPasswordResetRequest(
auth: OwnAuth,
email: string
): Promise<{ success: boolean }> {
// Always return success to prevent enumeration
const result = await auth.requestPasswordReset({ email });
return { success: true };
}Set token expiration to a short window, typically 15 to 30 minutes. This limits the window of opportunity for attackers who intercept tokens through man in the middle attacks or compromised email accounts. The token should be single use: once submitted successfully, it must be invalidated immediately to prevent replay attacks.
Processing the Reset and Invalidating Sessions
When a user submits a reset token with a new password, the endpoint must verify the token, update the password, and invalidate existing sessions. The OWASP Forgot Password Cheat Sheet emphasizes that all active sessions should be terminated after a successful password reset to prevent session hijacking through stolen session tokens.
Verify the token hash against the stored value, check expiration, and ensure the token has not been used. Upon successful verification, hash the new password using a secure algorithm like Argon2id and update the user's credential record. Then revoke all existing sessions for that user.
import { createHash } from 'crypto';
export async function processPasswordReset(
auth: OwnAuth,
token: string,
newPassword: string
): Promise<{ success: boolean; error?: string }> {
// Verify the token
const verifyResult = await auth.resetPassword({
token,
newPassword,
});
if (!verifyResult.success) {
return { success: false, error: 'Invalid or expired token' };
}
// Revoke all existing sessions after successful reset
await auth.revokeAllSessions({
userId: verifyResult.userId,
});
return { success: true };
}The OWASP Session Management Cheat Sheet covers session revocation in detail, recommending that applications provide mechanisms to invalidate sessions across all devices when credentials change. This ensures that if an attacker had established a session with a compromised password, they lose access immediately upon password reset.
Implementation with Own Auth
Own Auth provides built in password reset functionality through the requestPasswordReset and resetPassword methods. The requestPasswordReset method generates a secure, single use token stored as a hash, sends the reset email through your configured provider, and returns without exposing account state. The resetPassword method verifies the token, updates the password credential, and supports automatic session revocation.
import { createOwnAuth } from 'own-auth';
import { defineOwnAuthConfig } from 'own-auth';
const auth = createOwnAuth(
defineOwnAuthConfig({
databaseUrl: process.env.DATABASE_URL,
passwordReset: {
tokenExpiresInSeconds: 900, // 15 minutes
rateLimit: {
maxRequests: 3,
windowSeconds: 3600,
},
},
})
);
// Request endpoint - returns uniform response
app.post('/forgot-password', async (req, res) => {
const { email } = req.body;
// requestPasswordReset does not expose whether account exists
await auth.requestPasswordReset({ email });
// Always return the same response
res.json({ message: 'If an account exists, reset instructions have been sent' });
});
// Reset endpoint - verifies token, updates password, revokes sessions
app.post('/reset-password', async (req, res) => {
const { token, newPassword } = req.body;
const result = await auth.resetPassword({
token,
newPassword,
revokeAllSessions: true, // Automatic session invalidation
});
if (!result.success) {
return res.status(400).json({ error: 'Invalid or expired token' });
}
res.json({ message: 'Password has been reset successfully' });
});Summary of Security Requirements
A secure password reset flow requires attention to multiple security dimensions. At the request stage, prevent enumeration through uniform responses and aggressive rate limiting. At the token stage, use cryptographically random single use tokens stored as hashes with short expiration windows. At the completion stage, invalidate all existing sessions to prevent session hijacking. Following the OWASP Forgot Password Cheat Sheet guidance ensures your implementation addresses the most common attack vectors while maintaining usability for legitimate users.
By treating password reset as a high security operation and applying consistent defenses at each stage, you protect your users from account takeover while maintaining a smooth recovery experience.
Frequently Asked Questions
Why should password reset return the same response for valid and invalid emails?
Returning identical responses prevents account enumeration. If an attacker receives different responses for existing versus non existing accounts, they can automate queries to build a list of valid user accounts, which enables credential stuffing, phishing, and targeted attacks.
How long should a password reset token remain valid?
Reset tokens should expire quickly, typically within 15 to 30 minutes. Short expiration windows limit the attack surface if tokens are intercepted through man in the middle attacks or compromised email accounts.
Should sessions be invalidated after a password reset?
Yes. All existing sessions should be terminated after a successful password reset. This prevents attackers who had obtained a session through a compromised password from maintaining access after the password changes.
How should reset tokens be stored in the database?
Reset tokens must be stored as hashed values, not plaintext. Generate a cryptographically random token, hash it using SHA-256 or similar, store the hash in the database, and send the raw token to the user's email. This ensures token usability is not compromised if the database is breached.