OTP-Based Authorization with Devise 🔐

Sandesh Bodke
4 min readOct 30, 2023

--

What is OTP-Based Authorization?

OTP-based authorization involves sending a unique one-time password to a user’s registered email or phone number. The user must enter this OTP to gain access to their account. This adds an extra layer of security beyond the traditional username and password combination, as the OTP is valid for a single use and expires quickly.

Image credits www.hoxhunt.com

Setting Up Devise with Confirmable

In this blog, our focus is solely on implementing email-based OTP verification. If you wish to implement mobile-based verification, you may need services like Twilio to manage that process. However, please note that such mobile-based verification is not covered in this blog.

Before implementing OTP-based authentication with Devise, ensure you have Devise integrated into your Ruby on Rails application. You can add Devise to your application by following the official documentation if you haven't already.

Ensure that the Gemfile includes the Devise gem.

Once Devise is set up, we can add Confirmable to our User model. Confirmable is a Devise module that allows users to confirm their email address or phone number. In our case, we will use it to confirm the OTP.

Here are the steps to add Confirmable to your User model:

  1. Generate the Devise User Model:
rails generate devise User

2. Add Confirmable to the User Model:

Open the app/models/user.rb file and add the :confirmable option to your Devise configuration:

class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:lockable, :timeoutable, :confirmable
end

3. Migrate the Database:

After adding Confirmable to your User model, generate and run a database migration to update your database schema:

rails db:migrate

Ensure that the mailer is correctly configured by adding the necessary setup and configurations in the environment file.

4. Override Devise send_confirmation_instructions method

class User
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:lockable, :timeoutable, :confirmable

OTP_LENGTH = 6

def send_confirmation_instructions
token = SecureRandom.random_number(10**OTP_LENGTH).to_s.rjust(OTP_LENGTH, "0")
self.confirmation_token = token
self.confirmation_sent_at = Time.now.utc
save(validate: false)
UserMailer.confirmation_instructions(self, self.confirmation_token).deliver_now
end
end

In summary, We’ve overridden send_confirmation_instructions to handle email confirmation. It generates a random OTP (6-digit number), associates it with the user, marks the timestamp when the confirmation instructions were sent, and sends a confirmation email to the user.

Additionally, it’s necessary to establish a distinct mailer responsible for dispatching confirmation instructions because we are customizing the functionality by overriding the default actions within Devise.

def confirmation_instructions(user, otp)
@user = user
@otp = otp
email = @user.email.presence || @user.unconfirmed_email
mail(to: email, subject: "Your Otp")
end

Put your email view in app/views/user_mailer/users_instructions.html.erb file.

Devise automatically dispatches confirmation instructions to the user’s email upon signing up.

Let’s write some HTML code,

<div class="form-container mb-4">
<p class="heading">Confirm your email</p>
<p class="content">
We have sent OTP to registered email, please confirm your email
</p>

<%= form_with model: [:invitations, @user], url: validate_otp_invitations_users_path, method: :post do |form| %>
<%= form.hidden_field :id, value: @user.id %>

<%= form.text_field :otp, placeholder: "Type OTP" %>

<%= form.submit "Validate OTP" %>
<% end %>

<%= form_with model: [:invitations, @user], url: resend_otp_invitations_users_path, method: :post do |form| %>
<%= form.hidden_field :id, value: @user.id %>

<p class="text-center p-2 d-flex">
<%= form.submit "Resend OTP", style: "color: #0073ff;background: none;" %>
</p>
<% end %>
</div>

In the code above, we’ve incorporated two forms: one for validating the OTP and another for resending the OTP.

Similarly, we’ve to handle these form submissions on the backend side. Let’s write both functions now,

5. Handle Devise confirmation on the controller side

Let’s create confirmations_controller to validate enter OTP

class InvitationsController < ApplicationController
OTP_EXPIRATION_TIME = 5.minutes.ago

def validate_otp
user = User.find_by_confirmation_token(permitted_params[:otp])

if user && user.confirmation_sent_at >= OTP_EXPIRATION_TIME
user.confirm
user.update(confirmation_token: nil)
redirect_to confirmations_success_invitations_users_path, notice: "Email confirmed successfully!"
else
flash[:alert] = "Invalid or expired OTP. Please try again."
redirect_to success_invitations_users_path(id: params[:user][:id])
end
end

def resend_otp
user = User.find_by(id: params[:user][:id])
user.send_confirmation_instructions
redirect_to success_invitations_users_path(id: params[:user][:id])
end

private

def permitted_params
params.require(:user).permit(:otp)
end
end

The function validate_otp locates the confirmation_token from the users' table and uses it to confirm that specific user. It also incorporates an expiration time, typically set at 5 minutes in the above example.

On the other hand, the function resend_otp merely resends the confirmation instructions.

Conclusion

  • This is a simple method to implement OTP-based authentication using Devise’s confirmable feature without relying on third-party libraries or extensive configurations.
  • For email-based OTP verification, if you intend to implement mobile-based verification, you may require services such as Twilio to handle the process.

That’s all for implementation,

So, If you found this helpful please give some claps 👏 and share it with everyone.

Sharing is Caring!

So, did you find this article helpful? If you did or like my other articles, feel free to buy me a coffee.

Thank you ;)

--

--

Sandesh Bodke
Sandesh Bodke

Written by Sandesh Bodke

Passionate software developer 🖥️ | Code magician ✨ | Coffee addict ☕ | Nature lover 🌿 | Travel enthusiast ✈️ | Capturing moments through my lens 📷

Responses (1)