DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Sam Markham
Sam Markham

Posted on • Originally published at sharkham.github.io

Forgot/Reset Password functionality with Rails and React (and Redux)

(originally published May 6, 2020)

For my final project with Flatiron School, I have been building an app that tracks novel writing progress for novel contests. I have actually built it for a specific novel contest, so throughout coding it, I knew my goal once I had finished the basic functionality was to deploy it for use.

The biggest thing that stuck out to me as necessary for this was forgot/reset password functionality. I could do an admin controller later, fix my styling later, et cetera, but if I had live users and they were forgetting their passwords, this would be a problem.

Many hours later, I had done enough research and trial and error to build a solution.

Before I get into it, a bit of a disclaimer--if you're logging in users through a Rails API backend, this is really a Rails issue. There are a few good tutorials on how to do this functionality with only Rails already. I drew heavily from them for my solution (and will link them later!), but what I wasn't able to find was something incorporating React to do this. Honestly, there are good reasons for this, which I will get into later! But, if you are looking to use Rails only for the backend of password resets and React with Redux for the front end, read on!

In the Rails API:

First, some routes:

  post 'api/v1/forgot_password' => "api/v1/passwords#forgot"
  post 'api/v1/reset_password' => "api/v1/passwords#reset"
Enter fullscreen mode Exit fullscreen mode

There are two routes here because first you want the user to be able to submit their email through a forgot password action that will send a code to their email to start the process, and then once they have the code, you want them to be able to use it to submit their new password.

Next, the user model needs a couple new columns. Whether you set this up through adding them after the fact or putting them directly in the original migration, they should look like this in your schema:

    t.string "password_reset_token"
    t.datetime "password_reset_sent_at"
Enter fullscreen mode Exit fullscreen mode

Then you need a new controller:

class Api::V1::PasswordsController < ApplicationController
  def forgot
    user = User.find_by(email: params[:_json])
    if user
      render json: {
        alert: "If this user exists, we have sent you a password reset email."
      }
      user.send_password_reset
    else
      #this sends regardless of whether there's an email in database for security reasons
      render json: {
        alert: "If this user exists, we have sent you a password reset email."
      }
    end
  end

  def reset
    user = User.find_by(password_reset_token: params[:token], email: params[:email])
    if user.present? && user.password_token_valid?
      if user.reset_password(params[:password])
        render json: {
          alert: "Your password has been successfuly reset!"
        }
        session[:user_id] = user.id
      else
        render json: { error: user.errors.full_messages }, status: :unprocessable_entity
      end
    else
      render json: {error:  ['Link not valid or expired. Try generating a new link.']}, status: :not_found
    end
  end

end
Enter fullscreen mode Exit fullscreen mode

For my app, I have the passwords controller namespaced under Api::V1, but this is just preference. As long as the namespace is the same in the route and the controller (and the controller is under the proper Api and then V1 folders, if applicable), it'll work.

A lot of this code is drawn from these tutorials (one, two), so I won't get too deep into specifics, but I recommend reading them if you're deploying this to get a better understanding of exactly what's going on!

Briefly, the important thing about the forgot action is that you're finding the user by the email param that the user has submitted through a form (we'll get there), and then sending an email regardless of whether the email is in the database for security reasons, but letting the user know about this so they don't spend forever waiting for an email only to realize later, oh no that was the wrong email I put in. When testing this, I recommend having different alerts for each case so you know which is which, but for deployment, this is what worked for me.

The reset method is searching for a user by their email and the password_reset_token that firing the forgot action sets on their account. This is a deviation from the tutorials I used for this part, and I'll get into why later. If the user exists and their token is valid, then the password reset fires, and if that works, they're also logged in by setting the session[:user_id] to their id. If the token is expired, or doesn't exist, or there's no user by that email, an error is rendered.

Of course, to make this work we need some methods on the user model!

class User < ApplicationRecord
  ...
  has_secure_password
  validates :password, length: { minimum: 5, wrong_length: "Password must be at least 5 characters." }, if: :password

...

  def send_password_reset
    self.password_reset_token = generate_base64_token
    self.password_reset_sent_at = Time.zone.now
    save!
    UserMailer.password_reset(self).deliver_now
  end

  def password_token_valid?
    (self.password_reset_sent_at + 1.hour) > Time.zone.now
  end

  def reset_password(password)
    self.password_reset_token = nil
    self.password = password
    save!
  end

  private

  def generate_base64_token
    test = SecureRandom.urlsafe_base64
  end

end
Enter fullscreen mode Exit fullscreen mode

send_password_reset sets the user's password_reset_token attribute to a randomly generated token, sets the password_reset_sent_at to the current time, and then after saving these to the user, sends an email to the user that will include this token and further instructions. More on that soon! The password_token_valid method checks if the token has been sent within the hour--if it's been longer than an hour, the application won't accept it. This sort of thing is personal preference, I've seen it set to longer than an hour, but I went with a shorter time window for extra security because some of the React implementation is a bit lower security compared to some other ways of doing this. The reset_password method sets the token to nil so that once it's used once to reset the password it can't be reset again, and it changes the user's password to whatever they entered in the form.

The password validation line is important--without this you won't be able to set the password_reset_token and password_reset_sent_at. For more information on why, I have a separate blog post on that here.

The next thing to get set up is the Mailer functionality. First we need to generate a mailer:

rails g mailer user_mailer password_reset
Enter fullscreen mode Exit fullscreen mode

This will create a user_mailer.rb file under mailers, and two views for the password_reset email. This code goes in UserMailer--it's the method you're calling in send_password_reset:

class UserMailer < ApplicationMailer

  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password Reset"
  end

end
Enter fullscreen mode Exit fullscreen mode

The two views generated with the terminal command are really just html and plain text versions of the same email, and your code for both of them should be the same other than that one can use html tags.

Hi <%= @user.name %>,

You are receiving this email because you have requested a password reset for your Novel Tracker account.

Please use this code to reset your password: <%= @user.password_reset_token %>

This code will expire one hour from password reset request.

To reset your password please enter your code in the form here: http://localhost:3001/reset_password

If you did not request your password to be reset please ignore this email and your password will stay as it is.

Enter fullscreen mode Exit fullscreen mode

You can use ERB tags to put in the user's name (or username, if your app uses that instead), and, importantly, the token.

This is where my code diverges a little. This tutorial shows how to create a reset password view, and, even though the example there is done in a Rails-only project, many single page applications are not completely single page, and do something similar to this too--a reset password view through the API, and the rest of the app through the front end.

Because I'm stubborn, and because I didn't want to figure out how to style a page rendered through Rails the same way I'd styled my React frontend, I decided to try to figure out how to do this through React instead. This led to a few specific choices here:

One: exposing the password token in the email instead of including it as part of a dynamically generated link for the user to follow. Some apps do have both options, but mine only has one, because I wanted it to happen through a static link in React. This is because React is a bit strange about links. Well, not strange, but because it uses Client-side routing instead of Server-side routing, basically all of the app's content loads on the initial GET request to the server, and all of the routing from then is moving around within pages that are already downloaded from the beginning.

There are ways around this--this stack overflow thread gets into some. The specifics of figuring that out is beyond the scope of this blog post, but for my app, I have configured things so that any links a user doesn't need to be logged in to access can be navigated to by typing in the URL manually, and everything else (that requires a check for a logged in user) can't be. If you're going to use the way of doing things I'm outlining in this blog post for your project, make sure this is possible in your app!

Two: including a link to the reset password page. As previously described, if you can get it to work for your React app, it will be cleaner to do it this way and a bit more secure to have it not linked to from your front end.

Having a static link to a reset password page does, however, make things a little less secure. This is why I have configured mine to require both the correct token and the matching user email in order to reset a user's password.

Alright! The next step is configuring your settings so the mailing itself will work. REMEMBER: WHEN YOU CHANGE THESE SETTINGS, RESTART YOUR SERVER AFTERWARDS! I am embarrassed to admit this took me a lot of time in testing to figure out, so there is a reminder here!

In config/environments/development.rb:

  #added settings
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = true

  config.action_mailer.delivery_method = :smtp
  host = 'localhost:3000'
  config.action_mailer.default_url_options = { :host => 'localhost:3000', protocol: 'http' }

  # SMTP settings for gmail
  config.action_mailer.smtp_settings = {
    :address              => "smtp.gmail.com",
    :port                 => 587,
    :user_name            => ENV["GMAIL_ACCOUNT"],
    :password             => ENV["GMAIL_PASSWORD"],
    :authentication       => "plain",
    :enable_starttls_auto => true
  }
Enter fullscreen mode Exit fullscreen mode

Most of these settings are from this article, and I would recommend reading it as well for more information on how they work and for troubleshooting! Some of the trickier things here: your app needs somewhere to send mail from. This article recommends setting up a dedicated gmail account for this, which has worked for me. I have kept the information about it in my .env file, which I have added to my .gitignore file so it won't be uploaded to GitHub when I update my project there.

The other recommendation from the article that I appreciated was setting up two-factor authentication and then setting an app password for apps to use for the email account--the app password is what I'm calling here with my GMAIL_PASSWORD variable. When I have tested this, the gmail account I've sent to still puts these emails in the spam folder, but they do go through at least!

Also check out the previously linked article for advice on settings for your config/environments/production.rb file. As of writing this post I am still in the process of deploying my backend, so cannot yet speak to what changes work for me there.

In the React front-end

For reference, I didn't really code everything in Rails first and then everything in React after--it happened around the same time and involved a lot of testing throughout. But, for the purposes of this post, I thought it would be easier to separate these concerns to show how it works.

So, with that said, forgot/reset password in React! First, you need a ForgotPassword component to display the form for users to request the code to their email:

import React, { Component } from 'react';
import { forgotPassword } from '../helpers/passwords';
import { Link, withRouter } from 'react-router-dom';

class ForgotPassword extends Component {

  state = {
    email: ""
  }

  handleChange = (event) => {
    const { name, value } = event.target
    this.setState({
      [name]: value
    })
  }

  handleSubmit = (event) => {
    event.preventDefault()
    forgotPassword(this.state.email)
    this.setState({
      email: ""
    })
    this.props.history.push('/')
  }

  render() {
    return (
        <p>Request password reset:</p>
        <form onSubmit={this.handleSubmit}>
          <input required id="forgotpasswordemail" onChange={this.handleChange} name="email" placeholder="email" type="email" value={this.state.email}/>
          <button >Submit</button>
        </form>
    );
  }
}

export default withRouter(ForgotPassword);
Enter fullscreen mode Exit fullscreen mode

ForgotPassword component

My ForgotPassword component! Please note this isn't exactly the code above--I removed styling with reactstrap to make the code in this post easier to read.

This is a basic class component with a controlled form, but on submit, two important things are happening:

  1. The user's email is submitted to the forgotPassword method being called from the helpers/passwords.js file
  2. The user is being redirected back to the home page with this.props.history.push(), and this method is possible to use here because of the last line: withRouter(ForgotPassword).

In that helpers file:

const baseURL = "http://localhost:3000/api/v1"

export const forgotPassword = (email) => {
  return fetch(`${baseURL}/forgot_password`, {
    credentials: "include",
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(email)
  })
  .then(res => res.json())
  .then(response => {
    alert(response.alert)
  })
  .catch(console.log)
}
Enter fullscreen mode Exit fullscreen mode

This method sends a POST request with the user's email to our /forgot_password route, and when it receives a response, it displays an alert with that response. Going all the way back to our passwords_controller in the Rails section of this post, that alert is "If this user exists, we have sent you a password reset email."

The next step of getting this set up in React is the ResetPassword component to display the form for users to enter the code they have received by email and use it to reset their password:

import React, { Component } from 'react';
import { resetPassword } from '../helpers/passwords';
import { connect } from 'react-redux';


class ResetPassword extends Component {

  state = {
    token: "",
    email: "",
    password: "",
    password_confirmation: ""
  }

  handleChange = (event) => {
    const { name, value } = event.target
    this.setState({
      [name]: value
    })
  }

  handleSubmit = (event) => {
    event.preventDefault()
    const { password, password_confirmation } = this.state;
    if (password !== password_confirmation) {
      alert("Passwords don't match");
      this.setState({
        password: "",
        password_confirmation: ""
      })
    } else {
      this.props.resetPassword(this.state)
      this.setState({
        token: "",
        email: "",
        password: "",
        password_confirmation: ""
      })
    }
  }

  render() {
    return (
        <p>Reset Password:</p>
        <form onSubmit={this.handleSubmit}>
          <label for="token">Token:</label>
          <input required id="token" onChange={this.handleChange} name="token" placeholder="token" type="token" value={this.state.token}/>
          <p>The code that was emailed to you. This is case-sensitive.</p>
          <label for="email">Email:</label>
          <input required id="email" onChange={this.handleChange} name="email" placeholder="email" type="email" value={this.state.email}/>
          <label for="password">New password:</label>
          <input required id="password" onChange={this.handleChange} name="password" placeholder="password" type="password" value={this.state.password}/>
          <p>Set your new password here.</p>
          <label for="password_confirmation">Confirm new password:</label>
          <input required id="password_confirmation" onChange={this.handleChange} name="password_confirmation" placeholder="password confirmation" type="password" value={this.state.password_confirmation}/>
          <button type="secondary">Reset Password</button>
        </form>
    );
  }
}

const mapDispatchToProps = dispatch => {
  return {
    resetPassword: (credentials) => dispatch(resetPassword(credentials))
  }
}

export default connect(null, mapDispatchToProps)(ResetPassword);
Enter fullscreen mode Exit fullscreen mode

ResetPassword component

My ResetPassword component! As with the previous screenshot, this also includes reactstrap styling not included in my code here for simplicity's sake.

A little more is going on here! First, in handleSubmit, an alert fires and the password and password_confirmation fields reset to blank values if they don't match, to make sure the user is really resetting their password to the right thing. Second, if everything is in order on the form, resetPassword fires.

A bit of a disclaimer on this one: resetPassword isn't quite what I would consider a Redux action, and I honestly haven't figured out yet whether it's a good idea to put it in an actions folder, as is Redux convention, or not. I am dispatching it instead of just calling it though, and mapping it to props through mapDispatchToProps and the connect function, and this is because after it fires, I want it to fire my getCurrentUser action and log the user in, and that is a Redux action.

Here's what that looks like!

import { getCurrentUser } from '../actions/currentUser'

const baseURL = "http://localhost:3000/api/v1"

export const forgotPassword = (email) => {
...
}

export const resetPassword = (credentials) => {
  return dispatch => {
    return fetch(`${baseURL}/reset_password`, {
      credentials: "include",
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(credentials)
    })
    .then(res => res.json())
    .then(response => {
      if (!!response.error) {
        alert(response.error)
      } else {
        alert(response.alert)
        dispatch(getCurrentUser())
      }
    })
    .catch(console.log)
  }
}
Enter fullscreen mode Exit fullscreen mode

This method sends the credentials submitted in the ResetPassword component form to the /reset_password path as a POST request and returns a response. If there's an error in the action in passwords_controller, that will be an error, and this will show as an alert on the front end. If things go well on the back end, it show a "your password was reset!" alert and then checks the session for a current user.

Getting into that functionality is a little beyond the scope of this blog post too, but I will demonstrate this part of my sessions functionality briefly to put the previous code in context:

routes.rb:

  get '/api/v1/get_current_user' => "api/v1/sessions#get_current_user"
Enter fullscreen mode Exit fullscreen mode

application_controller.rb:

...
  def current_user
    User.find_by(id: session[:user_id])
  end

  def logged_in?
    !!current_user
  end
    ...
Enter fullscreen mode Exit fullscreen mode

sessions_controller.rb:

  def get_current_user
    if logged_in?
      render json: current_user
    end
  end
Enter fullscreen mode Exit fullscreen mode

actions/currentUser.js:

... 
export const getCurrentUser = () => {
  return dispatch => {
    return fetch(`${baseURL}/get_current_user`, {
      credentials: "include",
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    })
    .then(res => res.json())
    .then(user => {
      if (user.error) {
        alert(user.error)
      } else {
        dispatch(setCurrentUser(user))
      }
    })
    .catch(console.log)
  }
}
Enter fullscreen mode Exit fullscreen mode

So, the getCurrentUser action dispatches a GET request to the get_current_user action in the sessions_controller, and if there is currently a user in session--as a user is set in session in the reset action in the passwords_controller in the code at the beginning of this post--then it returns the user object, and uses that to set a current user in the Redux store, which, for the purposes of my app, is logging them in.

As a final note, there is no redirect in my ResetPassword component because my app has conditional rendering for that page--once a user is logged in, they will be redirected away from routes that logged in users don't need to see anyway.

Phew! I think that's it. If you've made it this far, thank you for sticking it out, and I hope this helps if you're trying to implement something similar!

Top comments (0)

Create an Account!

πŸ‘€ Just want to lurk?

That's fine, you can still create an account and turn on features like 🌚 dark mode.