Bảo mật 2 lớp (2fa) trong Rails app
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ả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:
<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 %>
<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.