DEV Community

t sriya
t sriya

Posted on

Part 4 : Building an Authentication System from Scratch

Implementing Forgot Password & Reset Password with Resend

In the previous article, we implemented secure user authentication using JWT Access Tokens and Refresh Tokens. Users can now register, log in, and access protected resources.

However, authentication systems must also handle a common real-world scenario—users forgetting their passwords.

Instead of creating a new account, users should be able to securely reset their existing password.

In this article, we'll build a complete password recovery workflow using Resend, secure reset tokens, PostgreSQL, and React.


Why a Password Reset Flow?

Passwords are intentionally stored as one-way hashes using bcrypt, which means they cannot be recovered—not even by the application itself.

Because of this, the only secure solution is to let users create a new password after verifying their identity.

The password reset process should:

  • Verify that the email belongs to a registered user.
  • Generate a secure, unique reset token.
  • Expire the token after a limited time.
  • Deliver the reset link through email.
  • Allow password updates only after validating the token.

This approach keeps user accounts secure while providing a smooth recovery experience.


Forgot Password Flow

The complete workflow is shown below.

User clicks "Forgot Password"
          │
          ▼
Enter Registered Email
          │
          ▼
Backend verifies email
          │
          ▼
Generate Secure Reset Token
          │
          ▼
Store Token & Expiry in Database
          │
          ▼
Send Reset Link via Resend
          │
          ▼
User receives Email
          │
          ▼
Clicks Reset Link
          │
          ▼
Reset Password Page
          │
          ▼
Submit New Password
          │
          ▼
Validate Token
          │
          ▼
Hash New Password
          │
          ▼
Update Database
Enter fullscreen mode Exit fullscreen mode

Why I Chose Resend

To deliver password reset emails, I chose Resend instead of configuring a traditional SMTP server.

Although SMTP works perfectly well, it often requires:

  • SMTP host configuration
  • Port configuration
  • App passwords
  • TLS configuration
  • Provider-specific settings

Resend provides a much simpler developer experience.

Advantages of Resend

  • Simple REST API
  • Excellent Node.js SDK
  • Reliable email delivery
  • Clean documentation
  • Easy integration
  • No SMTP configuration required

This allowed me to focus on the authentication workflow instead of managing email server configuration.


Generating a Secure Reset Token

Once the backend confirms that the email exists, it generates a unique reset token.

I used:

const resetToken = crypto.randomUUID();
Enter fullscreen mode Exit fullscreen mode

This creates a cryptographically secure UUID that is extremely difficult to guess.

The token is then assigned an expiration time.

const resetTokenExpiry =
new Date(Date.now() + 30 * 60 * 1000);
Enter fullscreen mode Exit fullscreen mode

In this project, the reset link remains valid for 30 minutes.


Storing the Reset Token

Instead of sending the token directly without tracking it, the application stores both:

  • reset_token
  • reset_token_expiry

inside PostgreSQL.

Users Table

Email

Password Hash

Reset Token

Reset Token Expiry
Enter fullscreen mode Exit fullscreen mode

These fields are later used to verify whether the reset request is still valid.


Repository Layer

The repository is responsible only for updating the database.

It stores:

  • reset token
  • expiration timestamp

for the corresponding user.

const saveRefreshToken = async (userId, refresh_token) => {
  await pool.query(`UPDATE users SET refresh_token= $1 where id = $2`, [
    refresh_token,
    userId,
  ]);
};
Enter fullscreen mode Exit fullscreen mode

Because database logic is isolated inside the repository, the service layer remains focused only on business logic.


Service Layer

The service performs the complete Forgot Password workflow.

It is responsible for:

  • Finding the user by email.
  • Generating a secure token.
  • Setting an expiration time.
  • Saving both values in PostgreSQL.
  • Constructing the password reset URL.
  • Sending the email using Resend.
const forgotPassword = async (userEmail) => {
  const user = await userRepository.findByEmail(userEmail);
  if (!user) {
    throw new Error("user not found!!");
  }
  const resetTokenExpiry = new Date(Date.now() + 30 * 60 * 1000);
  const reset_token = crypto.randomUUID();
  await userRepository.resetPassword(reset_token, resetTokenExpiry, userEmail);
  const resetUrl = `${process.env.FRONTEND_URL}/auth/reset-password/${reset_token}`;
  await emailService.sendEmail({
    from: "onboarding@resend.dev",
    to: userEmail,
    subject: "Reset Your Password",
    text: `Click here to reset your password: ${resetUrl}`,
  });

  return {
    success: true,
    message: "Password reset token generated successfully",
  };
};
Enter fullscreen mode Exit fullscreen mode

Keeping this logic inside the service makes the controller extremely lightweight.


Email Service

Rather than sending emails directly from the authentication service, I created a dedicated Email Service.

This follows the Single Responsibility Principle, making the authentication service responsible only for authentication.

The Email Service handles:

  • Initializing the Resend client.
  • Preparing the email.
  • Sending the email.
  • Handling delivery errors.
const { Resend } = require("resend");

const resend = new Resend(process.env.RESEND_API_KEY);

const sendEmail = async ({ from, to, subject, text }) => {
  try {
    const response = await resend.emails.send({
      from,
      to,
      subject,
      html: `
        <div style="font-family:sans-serif">
          <p>${text}</p>
        </div>
      `,
    });

    console.log("Resend response:", response);

    return response;
  } catch (err) {
    console.error("Resend Error:");
    console.error(err);
    throw err;
  }
};

module.exports = { sendEmail };
Enter fullscreen mode Exit fullscreen mode

Because the Email Service is reusable, future features such as:

  • Email Verification
  • Welcome Emails
  • Account Notifications

can all use the same implementation.


Sending the Reset Link

After generating the reset token, the backend creates a URL pointing to the frontend.

https://your-app.com/auth/reset-password/<token>
Enter fullscreen mode Exit fullscreen mode

The user receives an email similar to:

Reset Your Password

Click the link below to reset your password.

https://your-app.com/auth/reset-password/<token>
Enter fullscreen mode Exit fullscreen mode


Reset Password Flow

Clicking the email redirects the user to the Reset Password page.

The frontend extracts the token from the URL.

/auth/reset-password/:token
Enter fullscreen mode Exit fullscreen mode

The user enters:

  • New Password
  • Confirm Password

The frontend then submits:

POST /reset-password
Enter fullscreen mode Exit fullscreen mode

along with the token.


Backend Validation

Before updating the password, the backend verifies:

✅ Does the token exist?

✅ Has the token expired?

If either validation fails, the request is rejected.

This prevents attackers from reusing old reset links.


Updating the Password

Once the token is verified:

  1. Hash the new password using bcrypt.
  2. Update the password hash.
  3. Remove the reset token.
  4. Remove the expiration timestamp.

The token is deleted immediately after a successful password reset.

This guarantees that every reset link can be used only once.


Security Measures

Several security practices are implemented throughout the password reset workflow.

✅ Secure UUID token generation

✅ Token expiration (30 minutes)

✅ Password hashing using bcrypt

✅ Single-use reset links

✅ Email verification before reset

✅ Invalid token rejection

These practices significantly reduce the risk of unauthorized password changes.


Testing the Forgot Password Flow

Request

POST /forgot-password
Enter fullscreen mode Exit fullscreen mode
{
    "userEmail":"sriya@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

Response

{
    "success": true,
    "message": "Password reset token generated successfully"
}
Enter fullscreen mode Exit fullscreen mode

The user receives a reset email.


Testing the Reset Password Flow

Request

POST /reset-password
Enter fullscreen mode Exit fullscreen mode
{
    "token":"<reset-token>",
    "password":"NewPassword123"
}
Enter fullscreen mode Exit fullscreen mode

Response

{
    "success": true,
    "message":"Password updated successfully"
}
Enter fullscreen mode Exit fullscreen mode


Challenges Faced During Development

Implementing this feature wasn't completely straightforward.

Some of the challenges I encountered included:

  • SMTP IPv6 connection failures on Render.
  • Migrating from Nodemailer to Resend.
  • Missing environment variables during deployment.
  • Reset links returning Vercel 404 errors.
  • Debugging frontend routing after deployment.

Resolving these issues helped me gain a much deeper understanding of cloud deployment, environment management, and production debugging.


Summary

In this article we implemented:

  • Forgot Password API
  • Secure reset token generation
  • Token expiration
  • Database token storage
  • Email delivery using Resend
  • Reset Password API
  • Password update with bcrypt
  • Single-use reset links

Together, these features create a secure and user-friendly password recovery workflow commonly used in production applications.


What's Next?

The authentication system is now functionally complete.

In the next article, we'll implement Logout, understand why Refresh Tokens should be invalidated, and finally deploy the complete application to Render and Vercel, along with the production issues I encountered and how I resolved them.

Continue Reading Part 5 - https://dev.to/t_sriya_2af6abc7e8d4e87da/part-5-building-an-authentication-system-from-scratch-5h7

Live App: https://auth-flow-five-iota.vercel.app/auth/
Backend API: https://auth-flow-backend-1v2h.onrender.com/
github url: https://github.com/sriyaT/Auth-Flow

Connect : LinkedIn : https://www.linkedin.com/in/t-sriya-b4234510a/, github : https://github.com/sriyaT

Author: Sriya T.

Top comments (0)