Add devise-two-factor flow to your Devise User.
Used 12 times
M
Mike Kniazevic
Usage
Devise-Two-Factor byte doesn't require much to get started, but there are two prerequisites before you can start using it in your application:
- A Rails application with devise installed for User
- Secrets configured for ActiveRecord encrypted attributes
First, you'll need a Rails application setup with Devise. Visit the Devise homepage for instructions.
Devise-Two-Factor uses ActiveRecord encrypted attributes which in turn uses Rails' encrypted credentials. The Rails encrypted attributes guide has full details of how to set these up but briefly:
# generate suitable encryption secrets to stdout $ ./bin/rails db:encryption:init # Add the output from the command above to your encrypted credentials file via # Setting the EDITOR environment variable is optional, without it, your default editor will open $ EDITOR=code ./bin/rails credentials:edit
And the last one, after byte was run, add next code to your users/sessions controller:
include Users::AuthenticateWithOtpTwoFactor before_action :authenticate_with_otp_two_factor, only: :create
Run this command in your Rails app directory in the terminal:
rails app:template LOCATION="https://railsbytes.com/script/VB0sJl"
Template Source
Review the code before running this template on your machine.
run "bundle add devise-two-factor"
run "bundle add rqrcode-rails3"
run "bundle install"
rails_command("generate devise_two_factor User")
rails_command("db:migrate")
inject_into_file 'app/controllers/application_controller.rb', after: "class ApplicationController < ActionController::Base\n" do <<~RUBY
before_action :configure_permitted_parameters, if: :devise_controller?
RUBY
end
inject_into_file "app/controllers/application_controller.rb", before: /^end/ do <<~RUBY
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
RUBY
end
inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do <<~RUBY
include Users::Otp
RUBY
end
create_file "app/models/concerns/users/otp.rb", <<~RUBY
# frozen_string_literal: true
module Users::Otp
extend ActiveSupport::Concern
SVG_SETTINGS = {
offset: 0,
color: '000',
shape_rendering: 'crispEdges',
module_size: 4,
standalone: true
}.freeze
def generate_two_factor_secret_if_missing!
return if otp_secret.present?
update!(otp_secret: User.generate_otp_secret)
end
# rubocop:disable Rails/OutputSafety
def generate_two_factor_qr_code
RQRCode::QRCode.new(two_factor_qr_code_uri).as_svg(SVG_SETTINGS).html_safe
end
# rubocop:enable Rails/OutputSafety
def two_factor_qr_code_uri
issuer = 'RB2fa'
label = [issuer, email].join(':')
otp_provisioning_uri(label, issuer: issuer)
end
end
RUBY
create_file "app/controllers/concerns/users/authenticate_with_otp_two_factor.rb", <<~RUBY
# frozen_string_literal: true
module Users::AuthenticateWithOtpTwoFactor
extend ActiveSupport::Concern
def authenticate_with_otp_two_factor
return unless user&.otp_required_for_login?
user.generate_two_factor_secret_if_missing!
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)
end
end
private
def authenticate_user_with_otp_two_factor(user)
if valid_otp_attempt?(user)
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
user.save!
sign_in(user, event: :authentication)
else
flash.now[:alert] = I18n.t('flashes.invalid_2fa_code')
prompt_for_otp_two_factor(user)
end
end
def prompt_for_otp_two_factor(user)
session[:otp_user_id] = user.id
render 'devise/sessions/two_factor'
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt])
end
def user
@user ||= if session[:otp_user_id]
User.find(session[:otp_user_id])
else
User.find_by(email: user_params[:email])
end
end
def user_params
params.require(:user).permit(:email, :password, :remember_me, :otp_attempt)
end
end
RUBY
create_file "app/views/devise/sessions/two_factor.html.erb", <<~RUBY
<% if resource.consumed_timestep.blank? %>
<h6 class='bold'>Please scan the below QR code using an OTP compatible app (such as Google Authenticator or Authy)</h6>
<div class='center-text'>
<%= resource.generate_two_factor_qr_code %>
</div>
<% else %>
<h2>Log in</h2>
<% end %>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| %>
<div class='form-group'>
<%= f.label 'Your One-time password code' %>
<%= f.password_field :otp_attempt, autocomplete: "off", class: 'form-control' %>
</div>
<div class='actions'>
<%= f.submit "Log in", class: 'btn btn-success pull-right' %>
</div>
<% end %>
RUBY