Building an Email Link-Based Password Reset Flow in My Placement Portal
A password reset flow appears effortless from a user's perspective. Click a link, enter a new password, and log back in.
However, implementing it securely involves significantly more work behind the scenes.
While building my Placement Portal, I decided to use an email link-based password reset mechanism instead of the traditional OTP-based approach. Although it improves the user experience considerably, it also introduces several moving parts that must work together correctly.
The Password Reset Flow
The complete flow looks like this:
- The user clicks the "Forgot Password?" button.
- A dialog box appears asking for the registered email address.
- The frontend sends the email address to the backend.
- The backend processes the password reset request.
- The system always returns a success response irrespective of whether the email exists or not.
- If the email exists, the backend generates a password reset link.
- The password reset email is sent to the user.
- The user clicks the link from their inbox.
- The frontend captures the password reset credentials from the URL.
- The user submits their new password.
- The password is updated successfully.
- The user is redirected back to the login screen.
Preventing Email Enumeration Attacks
One interesting aspect of password reset flows is that the system should never disclose whether an email address exists within the application.
Imagine the following response:
- "Email does not exist."
- "Password reset email sent."
An attacker could easily use this behaviour to determine which email addresses are registered on the platform.
To prevent this, the backend always responds with a success message such as:
"If an account exists with this email address, a password reset link has been sent."
Whether the email exists or not, the response remains identical.
If the email is invalid or not registered, the user simply never receives the email.
Generating Secure Password Reset Links
When a valid user requests a password reset, the backend performs two important operations:
- Encodes the user's ID into a URL-safe format.
- Generates a secure token associated with that user.
These values are embedded within the password reset URL.
The email sent to the user follows a structure similar to:
{FRONTEND_URL}/password/reset/[uid]/[token]/
The token ensures that only the intended user can reset their password, while the encoded UID uniquely identifies the account.
Sending Emails Securely
To send password reset emails during development, I configured Gmail as the email provider.
Instead of exposing my actual Gmail password, I used Google's App Password feature.
This approach provides:
- Better security.
- Easier email integration.
- Separation between personal credentials and application credentials.
Dynamic Routes on the Frontend
Once the user clicks the password reset link, they are redirected to a dynamic frontend route.
The route captures:
- UID
- Token
The page then presents a simple form containing:
- New Password
- Confirm Password
This information is subsequently submitted to the password reset confirmation endpoint.
Updating the Password
After form submission, the frontend sends a POST request to the password reset confirmation endpoint containing:
- UID
- Token
- New Password
- Confirm Password
The backend validates the request and updates the user's password if everything is correct.
Finally, the user is redirected to the login page and can authenticate using their new credentials.
Final Thoughts
Password reset is one of those features that users expect to "just work."
However, behind a seemingly simple user experience lies:
- Token generation
- URL-safe encoding
- Dynamic frontend routes
- Email delivery mechanisms
- Security considerations
- Attack prevention strategies
- Authentication workflows
Good software engineering often hides complexity from users, and password reset flows are an excellent example of that.
I'm documenting every engineering and architectural decision while building this Placement Portal in public.