Ruby
Article
By Vinoth

Handle Password and Email Changes in Your Rails API

By Vinoth

This is part two of our series about authentication from scratch using JWT. The first part of the series can be found here. In the previous tutorial, we saw a quick overview about JWT, different authentication mechanisms, and a set of basic authentication APIs, like registration, confirmation, and login. In this part, we will see the next set of APIs such as password (reset and change) and email update.

This series is more than a JWT tutorial. Its main goal is to see how to build your own custom authentication solution from scratch and JWT is just the method we opted to use.

We will be continue building on our sample application that we developed in the first part which could be found here. If you wish to follow along, you can check out the part-i branch on the linked repository.

Password

The API we cover here is a forgot password sequence. The flow generates a password reset token along with an endpoint for the user to validate the token. This endpoint is called when a user clicks the password reset link sent to them via email. That last endpoint is for finally changing the password.

Forgot Password

The forgot password endpoint generates a password reset token, saves it in the database, and sends an email to the user. This is similar to the confirmation instructions module we saw in the first part. Let’s begin by adding the columns necessary for the password reset functionality. Run,

rails g migration AddPasswordResetColumnsToUser

And in the generated migration file, add the following:

add_column :users, :reset_password_token, :string
add_column :users, :reset_password_sent_at, :datetime

These two columns are sufficient for the purpose. reset_password_token will store the token that we generate and reset_password_sent_at tracks the time the token is sent for expiry purposes. Let’s add the endpoints now. Start by generating the password controller:

rails g controller passwords

Add the following routes to your config/routes.rb file:

post 'password/forgot', to: 'password#forgot'
post 'password/reset', to: 'password#reset'

Now, let’s add the corresponding action for the above mentioned routes to controllers/password_controller.rb:

...
  def forgot
    if params[:email].blank?
      return render json: {error: 'Email not present'}
    end

    user = User.find_by(email: email.downcase)

    if user.present? && user.confirmed_at?
      user.generate_password_token!
      # SEND EMAIL HERE
      render json: {status: 'ok'}, status: :ok
    else
      render json: {error: ['Email address not found. Please check and try again.']}, status: :not_found
    end
  end

  def reset
    token = params[:token].to_s

    if params[:email].blank?
      return render json: {error: 'Token not present'}
    end

    user = User.find_by(reset_password_token: token)

    if user.present? && user.password_token_valid?
      if user.reset_password!(params[:password])
        render json: {status: 'ok'}, status: :ok
      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
 ...

Let’s go over this quickly. In the forgot action, get the email in the post request and fetch the user. If the user is found and confirmed, call the generate_password_token on the user model and send the email. The email sending part is skipped, but make sure to include the password_reset_token of the user in the email. In the reset action, get the token sent in the request and validate it via password_token_valid? and reset the password via reset_password. These methods are yet to be added to the user model, let’s do it now:

Add the below methods to models/user.rb:

...
def generate_password_token!
  self.reset_password_token = generate_token
  self.reset_password_sent_at = Time.now.utc
  save!
end

def password_token_valid?
  (self.reset_password_sent_at + 4.hours) > Time.now.utc
end

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

private

def generate_token
  SecureRandom.hex(10)
end
...

In the generate_password_token! method we generate a token using the generate_token method and store it in the reset_password_token column, alos setting the reset_password_sent_at to the current time. In the password_token_valid? method, verify the token is sent within the last 4 hours which is the reset password expiry. You are free to change it however you see fit. the reset_password! method updates new password of the user and nullifies the reset token.

Reset password set is done. You can test it by sending a post request /passwords/forgot with the email in the body and /passwords/reset with a new password and token in the body. Let’s add the password update link now.

Update Password

To add the update password, add the route to your routes file:

put 'password/update', to: 'password#update'

Here is the corresponding action in PasswordsController:

def update
  if !params[:password].present?
    render json: {error: 'Password not present'}, status: :unprocessable_entity
    return
  end

  if current_user.reset_password(params[:password])
    render json: {status: 'ok'}, status: :ok
  else
    render json: {errors: current_user.errors.full_messages}, status: :unprocessable_entity
  end
end

The password update action is quite straightforward. Get the password from the parameter and save it to DB using the reset_password method that we declared before in the user model. You can now test the password update URL by sending a PUT request to /password/update with the new password in the body. Let’s move on to the next big functionality, Email Update.

--ADVERTISEMENT--

Email Update

Email update allows a user to update their primary email on their account. Upon request, we should check if the email is already being used by any other user. If the email is OK, store it and send a verification mail to the new email. Upon confirmation, we’ll replace the primary email with the new email and clear out the token.

So, there are two APIs in total: One to make an email update request, one to actually update the email. Let’s get started.

Begin by doing a migration to add the necessary column to support this module. Generate a migration:

rails g migration AddUnconfirmedEmailTouser

Add the following content to it and run rake db:migrate:

add_column :users, :unconfirmed_email, :string

Update

Now, let’s update the routes for these two endpoints. Add these lines to config/routes.rb:

...
resources :users, only: [:create, :update] do
    collection do
        post 'email_update'
...

Add the corresponding actions to UsersController:

def update
    if current_user.update_new_email!(@new_email)
      # SEND EMAIL HERE
      render json: { status: 'Email Confirmation has been sent to your new Email.' }, status: :ok
    else
      render json: { errors: current_user.errors.values.flatten.compact }, status: :bad_request
    end
end

Also add a before_action to do the validations on the new email, add this at top of the user controller class with the methods marked private:

class UsersController < ApplicationController
    before_action :validate_email_update, only: :update
    ...
    ...

    private
    def validate_email_update
      @new_email = params[:email].to_s.downcase

      if @new_email.blank?
        return render json: { status: 'Email cannot be blank' }, status: :bad_request
      end

      if  @new_email == current_user.email
        return render json: { status: 'Current Email and New email cannot be the same' }, status: :bad_request
      end

      if User.email_used?(@new_email)
        return render json: { error: 'Email is already in use.'] }, status: :unprocessable_entity
      end
    end
    ...

Here we check if the requested email is already in use, and if the email is the same of what the account already has. If everything is fine, call update_new_email! and send the email. Note that, the email has to be sent to user’s unconfirmed_email instead of their primary one. We have used a couple of new model methods here, so let’s go define them. In models/user.rb add the below functions:

def update_new_email!(email)
  self.unconfirmed_email = email
  self.generate_confirmation_instructions
  save
end

def self.email_used?(email)
  existing_user = find_by("email = ?", email)

  if existing_user.present?
    return true
  else
    waiting_for_confirmation = find_by("unconfirmed_email = ?", email)
    return waiting_for_confirmation.present? && waiting_for_confirmation.confirmation_token_valid?
  end
end

Here, in email_used?, apart from checking if the email is used primarily on any accounts we also check if it’s being updated and waiting for confirmation. This can be removed depending upon your needs. The confirmation_token_valid? method was added in the first part of this tutorial.

You can now test this route by sending a POST request to /users/update with email in the request body.

Email Update

Now, let’s add the action for the email update endpoint. Add this code to UsersController:

def email_update
  token = params[:token].to_s
  user = User.find_by(confirmation_token: token)

  if !user || !user.confirmation_token_valid?
    render json: {error: 'The email link seems to be invalid / expired. Try requesting for a new one.'}, status: :not_found
  else
    user.update_new_email!
    render json: {status: 'Email updated successfully'}, status: :ok
  end
end

This action is quite straightforward. Fetch the user by the token and see if the token is valid. If so, update the email and respond. Let’s add the update_new_email! method to the user model:

def update_new_email!
  self.email = self.unconfirmed_email
  self.unconfirmed_email = nil
  self.mark_as_confirmed!
end

Here we replace the primary email with the updated email and set the updated email field to nil. Also, call the mark_as_confirmed! which we added in the previous part of the series. This method nullifies the confirmation token and sets the confirmed at value. The email update endpoint is also up now. Try sending a POST request to /users/email_update with the email token we generated in previous section in the request body.

Conclusion

With that, we have arrived at the conclusion of our two-part tutorial on authentication from scratch for a Rails API. To recap, we have implemented Devise’s authentication, confirmation, password, and reconfirmation modules. Not too shabby.

The code used in this tutorial is available here. The code where this tutorial starts from is available in the part-i branch.

I hope this tutorial helped you in understanding the authentication and rolling out your own authentication system. Thanks for reading.

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account