Bảo mật 2 lớp (2fa) trong Rails app
This post hasn't been updated for 3 years
Bảo mật 2 lớp (2fa) thường được dùng trong những ứng dụng yêu cầu độ bảo mật cao. Vậy làm thế nào để thêm 2fa vào Rails app? Trong bài viết này mình sẽ đi chi tiết vào cách làm sử dụng gem devise-two-factor
.
Setup
Bài viết này được build trên 1 Rails app đã có sẵn dùng gem devise, vì vậy bạn nên tham khảo thêm thông tin về gem này trước khi tiếp tục. Ở đây chúng ta sẽ build model AdminUser
, tất nhiên model name nào cũng đc tùy bạn, như User
chẳng hạn.
Thêm các gem cần thiết vào Gemfile
:
gem 'devise-two-factor' # for two factor
gem 'rqrcode_png' # for qr codes
Chạy bundle
để cài đặt gem.
Việc cần làm tiếp theo là cần add thêm cột cần thiết vào database để lưu trữ mã OTP bí mật dùng cho việc authenticating. Gem trên sẽ cung cấp việc phát sinh mã để lưu trong cột này.
Chạy lệnh sau trong terminal:
rails generate devise_two_factor AdminUser TWO_FACTOR_SECRET_KEY_NAME
với TWO_FACTOR_SECRET_KEY_NAME là biến ENV dùng cho key mã hóa 2 lớp.
Khi chạy xong migrate, file migration sẽ có dạng như bên dưới:
class AddDeviseTwoFactorToAdminUsers < ActiveRecord::Migration
def change
add_column :admin_users, :encrypted_otp_secret, :string
add_column :admin_users, :encrypted_otp_secret_iv, :string
add_column :admin_users, :encrypted_otp_secret_salt, :string
add_column :admin_users, :otp_required_for_login, :boolean
add_column :admin_users, :consumed_timestep, :integer
end
end
Edit file này để thêm 1 cột nữa, cột này nhằm lưu trữ opt_secret
tạm thời trong suốt quá trình yêu cầu 2fa (sẽ đề cập sau).
add_column :admin_users, :unconfirmed_otp_secret, :string
Bây giờ kiểm tra lại model (AdminUser), bạn sẽ thấy phần thiết lập database_authenticatable
đã được thay bởi two_factor_authenticatable
.
class AdminUser < ActiveRecord::Base
devise :rememberable, :trackable, :lockable,
:session_limitable, :two_factor_authenticatable,
:otp_secret_encryption_key => ENV['TWO_FACTOR_SECRET']
# ...
end
Chạy rake db:migrate
để hoàn tất.
Authentication
Trước tiên mình cần view mặc định của devise, chạy
rails generate devise:views
để copy view của devise vào app.
Ở file app/views/devise/sessions/new.html.erb
ta sẽ thêm trường otp_attempt
<h2>Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :otp_attempt %><br />
<%= f.text_field :otp_attempt, autocomplete: "off" %>
</div>
<% if devise_mapping.rememberable? -%>
<div class="field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<% end -%>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
Sau đó, ở app/controllers/application_controller.rb
cần thiết lập để cho phép params mới:
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_in) << :otp_attempt
end
OK vậy là phần authentication đã xong, nhưng mà hiện vẫn chưa activate 2fa được :v
Two Factor Activation
Giờ đến phần controller và views. Thêm phần code dưới đây vào model để giảm logic cho controller:
def activate_two_factor params
otp_params = { otp_secret: unconfirmed_otp_secret }
if !valid_password?(params[:password])
errors.add :password, :invalid
false
elsif !validate_and_consume_otp!(params[:otp_attempt], otp_params)
errors.add :otp_attempt, :invalid
false
else
activate_two_factor!
end
end
def deactivate_two_factor params
if !valid_password?(params[:password])
errors.add :password, :invalid
false
else
self.otp_required_for_login = false
self.otp_secret = nil
save
end
end
private
def activate_two_factor!
self.otp_required_for_login = true
self.otp_secret = current_admin_user.unconfirmed_otp_secret
self.unconfirmed_otp_secret = nil
save
end
Khi method này được gọi, params yêu cầu phải chứa password và otp attempt, nếu cả 2 đều hợp lệ thì method sẽ kích hoạt 2fa.
Với routes, ta sẽ có như bên dưới:
namespace :admin do
get '/two_factor' => 'two_factors#show', as: 'admin_two_factor'
post '/two_factor' => 'two_factors#create'
delete '/two_factor' => 'two_factors#destroy'
end
Giờ quay lại với controller:
class Admin::TwoFactorsController < ApplicationController
before_filter :authenticate_admin_user!
def new
end
# Nếu user đã bật 2fa, chúng ta sẽ tạo mã otp_secret tạm thời
# và render `new` template lên.
# Ngược lại, sẽ render `show` templapte cho phép user tắt 2fa
def show
unless current_admin_user.otp_required_for_login?
current_admin_user.unconfirmed_otp_secret = AdminUser.generate_otp_secret
current_admin_user.save!
@qr = RQRCode::QRCode.new(two_factor_otp_url).to_img.resize(240, 240).to_data_url
render 'new'
end
end
# AdminUser#activate_two_factor sẽ trả về 1 boolean. Nếu false thì có lỗi xảy ra
def create
permitted_params = params.require(:admin_user).permit :password, :otp_attempt
if current_admin_user.activate_two_factor permitted_params
redirect_to root_path, notice: "You have enabled Two Factor Auth"
else
render 'new'
end
end
# Nếu password chính xác thì 2fa sẽ đc tắt
def destroy
permitted_params = params.require(:admin_user).permit :password
if current_admin_user.deactivate_two_factor permitted_params
redirect_to root_path, notice: "You have disabled Two Factor Auth"
else
render 'show'
end
end
private
def two_factor_otp_url
"otpauth://totp/%{app_id}?secret=%{secret}&issuer=%{app}" % {
:secret => current_admin_user.unconfirmed_otp_secret,
:app => "your-app",
:app_id => "YourApp"
}
end
end
Cuối cùng là views:
<!-- app/views/admin/two_factors/new.html.erb -->
<div class="page-header"><h2>Enable Two Factor Auth</h2></div>
<p>To enable <em>Two Factor Auth</em>, scan the following QR Code:</p>
<p class="text-center"><%= image_tag @qr %></p>
<p>Then, verify that the pairing was successful by entering your password and a code below.</p>
<%= form_for current_admin_user, url: [:admin, :two_factor], method: 'POST' do |f| %>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="field">
<%= f.label :otp_attempt %><br />
<%= f.text_field :otp_attempt, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Enable" %>
</div>
<% end %>
<!-- app/views/admin/two_factors/show.html.erb -->
<div class="page-header"><h2>Disable Two Factor Auth</h2></div>
<p>Type your password to disable <em>Two Factor Auth</em></p>
<%= form_for current_admin_user, url: [:admin, :two_factor], method: 'DELETE' do |f| %>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "off" %>
</div>
<div class="actions">
<%= f.submit "Disable" %>
</div>
<% end %>
Bây giờ nếu user đã loggin vào /admin/two_factor
mà chưa bật 2fa, họ sẽ thấy new
template. Điền vào form active 2fa.
Một khi đã bât 2fa, nếu vào lại trang /admin/two_factor
sẽ render show
template, ở đây họ có thể điền vào form để deactive 2fa.
All Rights Reserved