# Reset Password in Rails from Scratch

As a continuation of [Rails Authentication from Scratch](https://code.avi.nyc/rails-authentication-from-scratch), let's add a password reset feature to our application.

## Generating the Reset Password Token

The general flow of a password reset is as follows:

1. User clicks "Forgot Password" link
2. User enters email address
3. User receives email with a link to reset password
4. User clicks link and is taken to a form to enter a new password
5. User enters new password.
6. User submits form and password is updated.

That's the happy path. To make this happen we need to create unique secure links that are sent to the email address they entered that can load a form keyed to the user that generated the link. We'll use the `SecureRandom` library to generate a random token that we can use as a unique identifier for the password reset.

The first step is to add a `password_reset_token` column to our `users` table. We can do this with a migration:

```bash
rails g migration add_password_reset_token_to_users password_reset_token:string password_reset_token_expires_at:datetime
```

With the `password_reset_token_expires_at`, you can add a layer of security expiring the token after a certain amount of time.

When the user requests to reset their password, we'll populate these fields in the model.

## PasswordReset

Let's build a virtual model, basically a Ruby class to encapsulate this functionality.

[`app/models/password_reset.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb)
```ruby
class PasswordReset
  include ActiveModel::Model

  attr_accessor :user, :email

  def save
    @user = User.find_by(email: email)
    if @user
      @user.password_reset_token = SecureRandom.urlsafe_base64
      @user.password_reset_token_expires_at = 24.hours.from_now
      @user.save
      UserMailer.password_reset(@user).deliver_later
    end
  end

  def self.find_by_valid_token(token)
    User.where("password_reset_token = ? AND password_reset_token_expires_at > ?", token, Time.now).first
  end
end
```

As an `ActiveModel` we can use this class with `form_for`. The class basically does two things, it will generate the required token and expiration date when we make it and it will handle finding a user by a valid token. Using a new model that isn't backed by a database table is fine. By doing this we can keep our `User` class free from this functionality but still have a nice object for our forms and our controllers.

## PasswordResetsController

We'll use a `PasswordResetsController` for all the functionality with:

- `GET /password_resets/new` as a route to a form to request a reset password link.
- `POST /password_resets` as a route to create the password reset and send out the email.
- `GET /password_resets/:id/edit` where `:id` is the valid password reset token and present the user with a form to reset their password.
- `PATCH /password_resets/:id/` to set the new password if the password reset token is valid.

We can create these routes RESTfully with `resources :password_resets, only: [:new, :create, :edit, :update]` in `config/routes.rb`.


## Requesting a Password Reset

Let's build the form to request a password reset. Because we have the `PasswordReset` model, we can create an instance to wrap a form around.

[`app/controllers/password_resets_controller.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb)
```ruby
class PasswordResetsController < ApplicationController
  def new
    @password_reset = PasswordReset.new
  end
end
```

[`app/views/password_resets/new.html.erb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/views/password_resets/new.html.erb)
```erb
<%= render_form_for(@password_reset) do |f| %>
  <div class="grid gap-2">
    <div class="grid gap-1">
      <%= f.label :email, class: "sr-only" %>
      <%= f.email_field :email, placeholder: "name@example.com",
                                autocomplete: :email,
                                autocorrect: "email",
                                autocapitalize: "none" %>
    </div>
    <%= f.submit "Reset Password" %>
  </div>
<% end %>
```

That allows for a user to enter their email and will submit a `POST /password_resets`. And remember, `render_form_for` is just a wrapper for `shadcn-ui` around `form_for`, so it behaves the same.

When this form is submitted, the `create#passwordresets` will work like:

[`app/controllers/password_resets_controller.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb)
```ruby
class PasswordResetsController < ApplicationController
  def new
    @password_reset = PasswordReset.new
  end

  def create
    @password_reset = PasswordReset.new(password_reset_params)
    @password_reset.save

    flash[:notice] = "A link to reset your password has been sent to your email."
    redirect_to root_url
  end
end
```

`PasswordReset#save` will:

[`app/models/password_reset.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb)
```ruby
class PasswordReset
  include ActiveModel::Model

  attr_accessor :user, :email

  def save
    @user = User.find_by(email: email)
    if @user
      @user.password_reset_token = SecureRandom.urlsafe_base64
      @user.password_reset_token_expires_at = 24.hours.from_now
      @user.save
      UserMailer.password_reset(@user).deliver_later
    end
  end
end
```

So it will set the `password_reset_token` to a secure random string that expires in 24 hours. It will also send out the email. Let's generate that now:

`rails g mailer user`

[`app/mailers/user_mailer.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/mailers/user_mailer.rb)
```ruby
class UserMailer < ApplicationMailer
  def password_reset(user)
    @user = user
    mail(to: @user.email, subject: "Reset Your Password")
  end
end
```

And the mailer template:

[`app/views/user_mailer/password_reset.html.erb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/views/user_mailer/password_reset.html.erb)
```erb
<p>Hello,</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_reset_url(@user.password_reset_token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
```

The mailer template will use the `password_reset_token` to create a URL `edit_password_reset_url(@user.password_reset_token)`. We'll use that URL to validate and find the password reset in `PasswordResetsController#edit` and `PasswordResetsController#update`.

Together all this creates the request password reset flow.

### Mailers in Development

I find the best way to test mailers like this in development is to use the [letter opener](github.com/ryanb/letter_opener) gem. Once that is added to your Gemfile, you can set:

```ruby
config.action_mailer.perform_caching = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = {host: "localhost", port: 3000}
config.action_mailer.delivery_method = :letter_opener
```

In `config/development.rb`. Then when you test this flow in development, the email with the link will open in a new browser tab and you can click it and continue the flow as described below.

## Resetting the Password

The first step is to build `GET /password_resets/:id/edit`. We'll use `PasswordReset.find_by_valid_token` that we created to find the user for the valid password reset token.

[`app/models/password_reset.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb)
```ruby
class PasswordReset
  # Rest of Model...

  def self.find_by_valid_token(token)
    User.where("password_reset_token = ? AND password_reset_token_expires_at > ?", token, Time.now).first
  end
end
```

The `find_by_valid_token` uses SQL to find the matching token and ensure that it's valid given the current time. Let's implement this in our `PasswordResetsController#edit`.

[`app/controllers/password_resets_controller.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb)
```ruby
class PasswordResetsController < ApplicationController
  # Rest of Controller...

  def edit
    @user = PasswordReset.find_by_valid_token(params[:id])
    if @user
    else
      flash[:alert] = "Your password reset link is not valid."
      redirect_to new_password_reset_path
    end
  end
```

If we can find a user by the token in the URL, we'll render the edit form, which will present them with the ability to change their password, otherwise, we'll redirect them to make a new request for a password reset link. Here's what the edit form looks like:

[`app/views/password_resets/edit.html.erb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/views/password_resets/edit.html.erb)
```erb
<%= render_form_for(@user, url: password_reset_path, method: :patch) do |f| %>
  <div class="grid gap-2">
    <div class="grid gap-1">
      <div class="grid gap-1">
        <%= f.label :password, class: "sr-only" %>
        <%= f.password_field :password, placeholder: "Your password...",
                                      autocomplete: "current-password" %>
      </div>
      <div class="grid gap-1">
        <%= f.label :password, class: "sr-only" %>
        <%= f.password_field :password_confirmation, placeholder: "Confirm your password...",
                                      autocomplete: "current-password" %>
      </div>
      <%= f.submit "Reset Password" %>
    </div>
  </div>
<% end %>
```

The URL of the form will be a `PATCH` request `password_reset_path` creating a submission to `PATCH /password_resets/:id/` which will route to `PasswordResetsController#update`. In that action we'll find the user the same way we did in edit and accept the fields from the form to update the password if a user was found. The entire controller now looks like:

[`app/controllers/password_resets_controller.rb`](https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb)
```ruby
class PasswordResetsController < ApplicationController
  def new
    @password_reset = PasswordReset.new
  end

  def create
    @password_reset = PasswordReset.new(password_reset_params)
    @password_reset.save

    flash[:notice] = "A link to reset your password has been sent to your email."
    redirect_to root_url
  end

  def edit
    @user = PasswordReset.find_by_valid_token(params[:id])
    if @user
    else
      flash[:alert] = "Your password reset link is not valid."
      redirect_to new_password_reset_path
    end
  end

  def update
    @user = PasswordReset.find_by_valid_token(params[:id])
    if @user
      if @user.update(user_params)
        flash[:notice] = "Your password has been updated."
        redirect_to root_url
      else
        flash.now[:alert] = "There was an error updating your password."
        render :edit, status: :unprocessable_entity
      end
    else
      flash[:error] = "Your password reset link is not valid."
      redirect_to new_password_reset_path
    end
  end

  private

  def password_reset_params
    params.require(:password_reset).permit(:email)
  end

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

## Conclusion

The key is managing the `password_reset_token` via `SecureRandom`. The rest is just patterns on top of Rails controllers, views, and mailers. Once again, it's not that hard to build a secure password reset yourself, it just takes a bit of wiring up.

In the next post, we'll build the ability to login via a magic link which is a pretty similar implementation to this if you could guess.

