Handle Password and Email Changes in Your Rails API
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.
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.