DEV Community

Cover image for The Password Reset That Worked Until It Had to Send an Email
Maha Darshini
Maha Darshini

Posted on

The Password Reset That Worked Until It Had to Send an Email

Summer Bug Smash: Smash Stories 🐛🛹

Email sending notification

The plan was straightforward:

Frontend
   ↓
Backend API
   ↓
PostgreSQL
Enter fullscreen mode Exit fullscreen mode

Signup, login, JWT, refresh tokens, forgot password, reset password.

But getting all of it working exposed a chain of bugs that taught us much more about debugging than simply implementing the feature.

The First Crash: Duplicate Spring Beans

The backend wouldn't even start.

The first error was:

ConflictingBeanDefinitionException:
Annotation-specified bean name 'AIResponseParser'
conflicts with existing bean definition
Enter fullscreen mode Exit fullscreen mode

There were two different classes with the same name:

com.investmentai.ai.util.AIResponseParser
com.investmentai.ai.parser.AIResponseParser
Enter fullscreen mode Exit fullscreen mode

Both were registered as Spring components.

Instead of randomly deleting one, we searched the project for every reference:

Get-ChildItem src\main\java -Recurse -File -Filter *.java |
Select-String -Pattern "AIResponseParser"
Enter fullscreen mode Exit fullscreen mode

That helped us identify which implementation was actually being used.

After resolving the duplicate, we tried again.

And got another duplicate bean.


Round Two: Another Duplicate Bean

This time:

ConflictingBeanDefinitionException:
Annotation-specified bean name 'tokenValidator'
Enter fullscreen mode Exit fullscreen mode

There were two classes:

ai.security.TokenValidator
ai.validator.TokenValidator
Enter fullscreen mode Exit fullscreen mode

They had completely different responsibilities, but Spring derived the same default bean name from both classes.

Again, we searched for usages before changing anything.

The lesson was simple:

Don't fix a startup error blindly. First understand why both components exist and where they're being used.

After resolving this conflict, the backend finally started.


🗄️ Next Problem: Database Configuration

The authentication system depended on PostgreSQL running through Docker.

We verified the container:

docker compose ps
Enter fullscreen mode Exit fullscreen mode

Then verified the database directly:

docker compose exec postgres psql -U myuser -d mydatabase
Enter fullscreen mode Exit fullscreen mode

And checked the tables:

flyway_schema_history
refresh_tokens
users
Enter fullscreen mode Exit fullscreen mode

This helped separate the problem into two independent parts:

Application configuration
        ↓
Database connectivity
        ↓
Database schema
Enter fullscreen mode Exit fullscreen mode

Instead of assuming the database was broken, we tested it independently.

That made debugging much easier.


🔐 Finally: Signup and Login

Once the backend was stable, we tested signup directly through the API.

POST /auth/signup
Enter fullscreen mode Exit fullscreen mode

The user was successfully created.

Then:

POST /auth/login
Enter fullscreen mode Exit fullscreen mode

returned a JWT access token.

We also verified that the user was actually stored in PostgreSQL.

At this point, the core authentication flow was working:

Signup
   ↓
PostgreSQL
   ↓
Login
   ↓
JWT
Enter fullscreen mode Exit fullscreen mode

But authentication still had one major feature left.

Forgot password.


🔑 Building the Password Reset Flow

We created a dedicated password reset token table.

The important part of the design was that we didn't store the raw reset token in the database.

The flow was:

User requests password reset
          ↓
Generate random token
          ↓
Hash token
          ↓
Store hash in PostgreSQL
          ↓
Send raw token through email
          ↓
User clicks reset link
          ↓
Validate token
          ↓
Update password
Enter fullscreen mode Exit fullscreen mode

We also tracked:

  • expiration
  • whether the token was already used
  • the associated user

The database migration was applied successfully.

The reset link was generated correctly.

The reset page opened correctly.

Everything looked good.

Except...

the email never arrived.


📧 The SMTP Problem

We initially used Gmail SMTP.

The configuration looked normal:

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
Enter fullscreen mode Exit fullscreen mode

But the backend returned:

535-5.7.8 Username and Password not accepted
Enter fullscreen mode Exit fullscreen mode

This was interesting because the application could successfully connect to Gmail's SMTP server.

The failure was happening specifically during authentication.

We checked the environment variables.

We checked the App Password.

We checked the SMTP configuration.

We eventually realised that relying on a local .env file did not automatically make those variables available to the Spring Boot process.

That was another useful lesson:

A configuration file existing in your project doesn't necessarily mean your application is actually receiving those values.


🔄 Changing the Approach

Now the email is sent and shown in command prompt

We replaced:

Spring Boot
    ↓
Gmail SMTP
    ↓
Authentication failure
Enter fullscreen mode Exit fullscreen mode

with:

Spring Boot
    ↓
Email API
    ↓
User's inbox
Enter fullscreen mode Exit fullscreen mode

The important part was that we didn't rewrite the password-reset logic.

The existing service still did:

emailService.sendPasswordResetEmail(
    user.getEmail(),
    resetUrl
);
Enter fullscreen mode Exit fullscreen mode

Only the implementation of EmailService changed.

This was a good example of why separating business logic from infrastructure matters.

The authentication service shouldn't care whether an email is delivered through SMTP or an API.


🎯 The Final Test

We triggered:

POST /auth/forgot-password
Enter fullscreen mode Exit fullscreen mode

The backend:

  1. Found the user.
  2. Generated a reset token.
  3. Stored the token hash.
  4. Created the reset URL.
  5. Sent the email through the new email service.

And finally:
reset password link is sent to the mail

📩 The reset email arrived.

The user clicked the link.

The reset page opened.

The password was changed.

The new password worked for login.


🧠 What This Bug Taught Me

The biggest lesson wasn't about Spring Boot, PostgreSQL or email APIs.

It was about debugging by layers.

Instead of looking at the entire authentication system as one big problem, we broke it down:

Frontend
   ↓
HTTP Request
   ↓
Spring Controller
   ↓
Authentication Service
   ↓
JWT / Token Logic
   ↓
PostgreSQL
   ↓
Email Service
   ↓
Email Provider
Enter fullscreen mode Exit fullscreen mode

Then we tested each layer independently.

A problem that initially felt like:

"Authentication isn't working."

became several smaller, solvable problems:

  • Duplicate Spring beans
  • Database verification
  • Migration verification
  • Environment variable configuration
  • SMTP authentication
  • Email provider integration

And that made the debugging process much less overwhelming.


🏁 From Broken to Working

The final authentication flow became:

              Authentication
                     │
        ┌────────────┼────────────┐
        ↓            ↓            ↓
     Signup        Login      Forgot Password
        │            │            │
        ↓            ↓            ↓
   PostgreSQL      JWT       Reset Token
                                  │
                                  ↓
                              Email API
                                  │
                                  ↓
                             Reset Link
                                  │
                                  ↓
                           New Password
                                  │
                                  ↓
                                Login
Enter fullscreen mode Exit fullscreen mode

The best part wasn't that we avoided bugs.

We didn't.

The best part was learning how to trace a failure, isolate the layer causing it, verify assumptions, and change only what actually needed changing.

Sometimes debugging isn't about finding one bug.

Sometimes it's about following the trail until the whole system finally makes sense.

Top comments (0)