CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets

From Expertiza_Wiki
Jump to navigation Jump to search

Demo Video

Click to watch the demo video

You can also Watch it on YouTube

Project Background

Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or "forgot password" feature, representing a significant gap in both usability and security.

This created several issues:

  • Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.
  • A user who forgets their credentials has no means of recovering access independently.
  • A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).

This project introduces a more structured workflow for handling password reset and account recovery operations.

The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.

Typical operations in this workflow include:

  • requesting a password reset
  • generating a secure reset token
  • validating reset requests
  • updating the user's password
  • preventing unauthorized reset attempts

The system should ensure that these steps are handled in a secure, predictable, and maintainable way.

Objectives

1. Implement a unified password reset workflow

The system should support a consistent flow for recovering user accounts.

Typical steps include:

  • User requests password reset
  • System generates a reset token
  • User receives reset instructions via email
  • User accesses password reset page via email link
  • User submits new password
  • System validates and updates credentials

This workflow should be easy to maintain and extend.

2. Improve security and validation

Password reset functionality must enforce security best practices.

This may include:

  • Ensure forgot password mechanism adheres to industry standards.
  • Ensure reset links are generated correctly across development, staging, and production environments.
  • Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.

The system should ensure that only authorized password reset requests are processed.

3. Improve frontend robustness and validation

The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.

This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.

Key areas of focus include:

  • Handling invalid or expired password reset tokens
  • Properly displaying backend validation errors
  • Ensuring clear success and failure messages for users
  • Confirming that form validation aligns with backend rules

The goal is to ensure that the password reset workflow remains reliable and user-friendly.

Core Changes

In this project:

  • The User model or authentication model was enhanced.
  • A new password reset workflow was introduced.
  • Controller logic was updated to handle reset requests.
  • Routes were added to support password reset endpoints.
  • Tests were added to verify authentication and recovery behavior.

Password Reset Workflow

Purpose

The password reset workflow allows users who have forgotten their password to regain access to their account securely.

A typical password reset flow may look like:

  • User selects "Forgot Password" in the login page.
  • User provides email address associated with account.
  • System generates a reset token.
  • System sends reset email containing link with password token in it.
  • User navigates to password reset portal and creates a new password.
  • System validates the request and updates credentials

This workflow must ensure both usability and security.

Implementation

When the user enters the website, they are presented with login fields. If they do not remember their password, they can select the Forgot password? link.


The user is now presented with a new window. This is where they will input their email to receive a token for resetting their password.

The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.


When a user submits their email to request a password reset.

# POST /password_resets
def create
  if @user
    token = @user.generate_token_for(:password_reset)
    UserMailer.send_password_reset_email(@user, token).deliver_later
  end

  # Always return a 200 OK to prevent email enumeration attacks
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok
end


A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.

def find_user_by_email
  @user = User.find_by(email: params[:email])
end


Assuming the email is valid, a password token is dispatched along with an HTML format email.

generates_token_for :password_reset, expires_in: 15.minutes do
  password_salt&.last(10) || updated_at.to_s
end


Navigating to the URL presents the user with the following password reset page.


When a user submits a new password using the token from the email link.

# PATCH/PUT /password_resets/:token
def update
  if @user.update(password_params)
    render json: { message: I18n.t('password_reset.updated') }, status: :ok
  else
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
  end
end


A before_action for update. It validates and decodes the reset token from the URL.

def find_user_by_token
  @user = User.find_by_token_for(:password_reset, params[:token])

  unless @user
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity
  end
end


If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error

def password_params
  params.require(:user).permit(:password, :password_confirmation)
end

How it works - Token Generation

  • Tokens are generated using Rails 7.1's built-in generates_token_for API.
    • When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:
      • A purpose tag (password_reset) so the token cannot be reused for other token-based operations.
      • An expiry timestamp embedded in the token (15-minute window).
      • A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at

How it works - Token Validation

  • Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:
    • Rails' find_by_token_for performs several checks atomically:
      • Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).
      • Purpose check — the token must have been generated for :password_reset specifically.
      • Expiry check — the embedded timestamp must be within the 15-minute window.
      • Fingerprint check — the password_salt fragment in the token must match the user's current state.
    • If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message ("The token has expired or is invalid.").
      • This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.

How it works - Secure Password Update

  • Once the token is validated, the update action applies the new password.
    • Strong parameters filter the request, permitting only :password and :password_confirmation
      • The user has to be able to confirm the password by writing it again.
    • At the storage layer:
      • has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.
      • password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.
      • Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.
      • Upon a successful save, password_salt changes, which immediately invalidates the reset token used.

Results - Frontend

  • Implemented two new authentication pages — Forgot Password and Reset Password — enabling users to recover account access via a tokenized email link
  • Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages
  • The forgot password flow sends a "POST /password_resets" request with the user's email; the reset flow reads a "?token=" query parameter from the email link and sends a "PUT /password_resets/:token" request with the new password
  • Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links
  • Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes
  • The login page already contained a "Forgot password?" link pointing to "/forgot-password"; both new routes were already registered in "App.tsx"

Results - Backend

  • app/controllers/passwords_controller.rb — Implements two endpoints: POST /password_resets to trigger a reset email (silently no-ops if email not found to prevent enumeration) and "PATCH /password_resets/:token" to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.
  • user.rb — Added "generates_token_for :password_reset, expires_in: 15.minutes" using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against "password_salt" so it automatically invalidates after the password is changed.
  • user_mailer.rb — Added "send_password_reset_email" which constructs a reset URL pointing to the frontend ("FRONTEND_URL/password_edit/check_reset_url?token=<token>") and delivers the email asynchronously via "deliver_later".
  • app/views/user_mailer/send_password_reset_email.html.erb — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.
  • routes.rb — Registered "resources :password_resets, only: [:create, :update], controller: "passwords", param: :token" to wire up the two endpoints with the token as the URL parameter.
  • en.yml — Added all user-facing strings under the "password_reset" namespace (email subject, success messages, and error messages) for i18n consistency.

Security Considerations

Password reset systems must be designed carefully to prevent abuse.

Potential risks include:

  • Unauthorized password changes
  • Brute-force token guessing
  • Replay attacks using old reset links
  • Account enumeration
  • Token leakage via URL

The system addresses these risks:

  • A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.
  • Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.
  • When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.
  • The create action always returns 200 OK with the same message ("If the email exists, a reset link has been sent.") regardless of whether the submitted email matches a real account.
  • The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.

Testing and Verification

Frontend And Backend Test Verification

Goals of Testing

The testing strategy verifies that password reset functionality works correctly and securely.

Key goals include:

  • Verifying reset request behavior
  • Verifying token validation
  • Verifying password updates
  • Verifying proper error handling

Frontend Tests

The front end implementation achieves these goals by verifying that:

  • Pages render the correct UI elements (headings, inputs, and submit buttons)
  • Form validation prevents submission with invalid or mismatched input and displays appropriate error messages
  • Successful API calls dispatch the correct success alerts to the user
  • Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message
  • Token validation on the reset page redirects unauthenticated users back to login

Backend Tests

The back end implementation achieves these goals by verifying that:

  • Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration
  • A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL
  • Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200
  • Invalid, malformed, or expired tokens (>15 minutes) are rejected with HTTP 422 and an appropriate error message
  • Password validation rules (minimum 6 characters) are enforced during the reset flow

Test Results

  • 30 total tests across the backend (16) and frontend (14), all passing with zero failures
  • Backend uses RSpec to cover all controller endpoints ("POST /password_resets", "PUT /password_resets/:token"), the "UserMailer", and "User" model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection
  • Frontend uses Vitest + React Testing Library to test both "ForgotPassword" and "ResetPassword" components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages
  • Feature-specific backend coverage is estimated at ~95–100% for "PasswordResetsController" and "UserMailer"; the reported 59.79% overall figure reflects measurement against the entire codebase
  • Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature

Test Coverage

  • Backend (reimplementation-back-end)
    • spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec
    • spec/mailers/user_mailer_spec.rb — new mailer spec
    • spec/factories/users.rb — added :password_reset_user factory
  • Frontend (reimplementation-front-end)
    • src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file
    • src/pages/Authentication/tests/ResetPassword.test.tsx — new test file

Collaborators

  • Mentor: Prerak Manish Bhandari
  • Jose Vargas
  • John Weisz
  • Jared Monseur