Two Factor Authentication has been popular on applications to improve security. 2FA is implemented to better protect both a user’s credentials and the resources the user can access. And one popular method is using SMS OTP where you sign in to an application and you then receive an SMS with an OTP that you need to then enter into the application. But the thing is, SMS OTP is not cheap and thus would require subscription from a Third Party App like Twilio.
A free alternative method would then be TOTP (Time-Based One Time Password) which is mostly used by Authenticator Apps like Google Authenticator etc. TOTP is a type of two-factor authentication that uses a six-digit code (or one-time passcode, OTP) or one-time tokens generated by an authenticator app. The code changes every 30 seconds, so it’s unique for each login attempt.
To integrate TOTP 2FA in your Rails App, We would need the following dependencies
Adding Devise Two Factor
Assuming you are already using Devise for User Authentication, devise-two-factor works on top of devise to add 2FA functionality. Follow the gem installation to setup devise 2FA but to summarize, This gem would add this fields to your User table. And the most important part is generating the otp_secret for each users because the creation of the TOTP would be based from that secret
add_column :users, :otp_secret, :string
add_column :users, :consumed_timestep, :integer
add_column :users, :otp_required_for_login, :boolean
# Setting up 2FA for a user
current_user.otp_required_for_login = true
current_user.otp_secret = User.generate_otp_secret
current_user.save!
Adding ROTP and RqrCode Gem
ROTP is a gem used to generate and validate TOTP and is compatible with popular authenticator apps like Google Authenticator. And we will use the rqrcode gem to generate a QR code SVG based from the TOTP generated.
totp = ROTP::TOTP.new(current_user.otp_secret)
qr_link = totp.provisioning_uri(current_user.email)
qrcode = RQRCode::QRCode.new(qr_link)
@svg = qrcode.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 3,
use_path: true,
)
The QR Code then would be render in your application in order for the User to scan it using their Authenticator App
Given that you would just need to add a new page where the user can scan the code and set it up with their authenticator apps to require OTP for login.
ROTP and RQRCode Gems are compatible with popular authenticator apps like to mention a few:
- Google Authenticator
- Lastpass Authenticator
- 2FAS Auth
- FreeOTP
- Aegis Authenticator
- StepTwo
Applying 2FA with TOTP
Now were all set with generating the TOTP using the said libraries, We then need to have a custom devise session controller to implement our 2FA Process
class Users::SessionsController < Devise::SessionsController
...
def create
...
end
end
# routes.rb
devise_for :users,
controllers: { sessions: 'users/sessions' }
Then we would need to customize the create action in the session controller to handle authentication whenever the user is required for otp
def create
if otp_two_factor_enabled?
authenticate_with_otp_two_factor
else
super
end
end
private
def find_user
if session[:otp_user_id]
User.find(session[:otp_user_id])
elsif user_params[:email]
User.find_by(email: user_params[:email])
end
end
def otp_two_factor_enabled?
return false if find_user.blank?
find_user.otp_required_for_login
end
Now whenever a user is required for 2FA, There would be two steps - Render the Page for the User to enter the code from the authenticator app - Validate the TOTP entered by the user
def authenticate_with_otp_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_user_with_otp_two_factor(user)
elsif user&.valid_password?(user_params[:password])
prompt_for_otp_two_factor(user)
else
flash[:error] = "Invalid Password or Email"
redirect_to new_user_session_path
end
end
def prompt_for_otp_two_factor(user)
@user = user
session[:otp_user_id] = user.id
render 'view_file_to_render_qr_code'
end
def authenticate_user_with_otp_two_factor(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
sign_in(user, event: :authentication)
flash[:success] = "Signed in successfully"
redirect_to after_sign_in_path_for(user)
else
flash.now[:alert] = 'Invalid code'
prompt_for_otp_two_factor(user)
end
end
def valid_otp_attempt?(user)
totp = ROTP::TOTP.new(user.otp_secret)
totp.verify(user_params[:otp_attempt]).present?
end
Sample Prompt HTML
Please enter the code from your Authenticator App
<%= form_for(resource, as: resource_name, url: session_path(resource_name),
data: { turbo: false }, method: :post) do |f| %>
<%= f.text_field :otp_attempt, label: 'Code', autocomplete: 'one-time-code',
autofocus: true, required: true, hide_label: true, placeholder: "6 Digit Code" %>
<%= button_tag(type: 'submit') do %>
Verify
<% end %>
<% end %>
And that’s it! Users that enabled 2FA on their account will be required to enter the TOTP from their Authenticator App before they are able to login. And this implementation can still be expanded like applying 2FA only every N days (e.g every 15 days) which we can cover on the next part of this blog.
Ps. if you have any questions
Ask here