Skip to content

Dashboard

Xác thực 2 bước với google authenticator chưa bao giờ dễ dàng đến thế - P2

Created by Admin

Chào các bạn. Ở phần 1 mình có giới thiệu về xác thực 2 bước đối với google authenticator cơ bản. Ở phần 2 này mình sẽ giới thiệu với các bạn làm config code làm sao để có thể xác thực bước thứ 2 bằng mã otp từ google authenticator

Ở phần trước chúng ta đã tạo được mã QR và thực hiện verify rồi, bây giờ ta tiếp tục tạo ra mã backup codes sau khi verify

Backup codes

Chúng ta tạo ra 10 mã backup codes, được sử dụng trong trường hợp người dùng bị mất điện thoại hoặc lỡ tay xóa ứng dụng Google Authenticator

Mã backup code được tạo ra như thế nào

current_user.generate_otp_backup_codes!
current_user.save!

Câu lệnh trên sẽ tạo mã backup code vào thực hiện lưu vào trong database của mình theo kiểu array. Mỗi khi user dùng 1 mã backup code để thực hiện verify thì trong database sẽ tự động remove đi 1 mã. Cho nên có 10 mã, chúng ta được sử dụng thay mã otp google authenticator 10 lần.

Sau khi thực hiện scan và verify otp, thì chúng ta sẽ chuyển qua trang backup codes này. Để thuận tiện cho hiển thị và sử dụng cho chức năng download, thì chúng ta nên lưu mã backup codes này vào session để còn sử dụng lại. Nguyên nhân, vì mã backup code nếu đã được lưu vào database rồi thì nó sẽ thực hiện mã hóa luôn, và không thể giải mã lại được. Nên cần lưu vào session để tiện sử dụng ở những chỗ khác. Khi enable chức năng two factor thành công thì chúng ta sẽ xóa session này đi.

Download and Copy backup codes

Download

Hành động này sẽ thực hiện tải về 1 file txt chứa mã backup codes

def download
    send_data params[:two_fa][:codes], filename: "backup_codes.txt"
  end
<%= form_tag(download_two_factor_settings_path, method: :post,
                                        class: "form-horizontal form-label-left download-two-factor") do %>
    <textarea name="two_fa[codes]" class="list-backup-codes hide"></textarea>
    <button type="button" class="btn btn-default btn-two-factor-download">
      <i class="fa fa-download"></i> Download
    </button>
    <button type="button" class="btn btn-default btn-two-factor-copy">
      <i class="fa fa-clipboard"></i> Copy
    </button>
<% end %>

Code javascript khi thực hiện click vào button download

  $(document).on('click', '.btn-two-factor-download', function() {
    $('.list-backup-codes').val(getOtpBackupCodes());
    $('.download-two-factor').submit();
    $('.btn-enable-two-factor').removeAttr('disabled');
  });

Copy

$(document).on('click', '.btn-two-factor-copy', function() {
    $('.list-backup-codes').val(getOtpBackupCodes());
    $('.list-backup-codes').removeClass('hide');
    $('.list-backup-codes').select();
    document.execCommand('copy');
    $('.list-backup-codes').addClass('hide');
    $('.copy_message').removeClass('hide');
    $('.btn-enable-two-factor').removeAttr('disabled');
  });

Chỉ đơn giản là chạy comand thực hiện việc copy đống backup codes đó thôi. Rồi có thể paste vào bất cứ đâu để lưu lại

Disable two factor settings

Sau khi config two factor thành công thì còn lại là disable chức năng two factor và thực hiện generate lại mã backup codes mới, trong trường hợp bạn bị mất mã backup codes cũ rồi.

Chức năng tạo lại mã backup codes cho những user đã enable two factor, về cơ bản là nó giống với việc tạo mã backup codes khi bạn scan qrcode

Require password

Để thực hiện việc bảo mật, mỗi khi user truy cập vào trang setting two factor này thì chúng ta sẽ yêu cầu họ nhập mk hiện tại của user. Nhập đúng mk thì mới chuyển hướng tới trang two factor setting

  • Tạo function require password
def required_password
    return if session[:password_token] == current_user.encrypted_password

    render "two_factor_settings/required_password"
  end

Hàm này chỉ đơn giản là hiển thị form nhập mật khẩu thôi.

before_action :required_password, only: [:new, :edit]

Chúng ta sẽ cho nó hiện thị form nhập password trước khi chuyển đến trang scan qrcode dành cho user thực hiện enable two factor setting, và trang disable two factor setting.

def confirm_password
    unless current_user.valid_password? enable_2fa_params_password[:password]
      flash.now[:danger] = "Current password not valid. Please try again"
      return render "two_factor_settings/required_password"
    end

    session[:password_token] = current_user.encrypted_password
    redirect_to new_two_factor_settings_path
  end

Hàm này thực hiện kiểm tra mật khẩu nhập ở form yêu cầu mk xem có đúng không và chuyển hướng đến trang config two factor setting

Mục đích để tránh có người khách không phải bản thân user thực hiện action. Hạn chế rủi ro về bảo mật tài khoản cho user

Nhập OTP, Backup Code sau khi login

Đối với những tài khoản đã enable thì sau khi login sẽ hiển thị lên form nhập otp hoặc backup code như thế này Trước khi thực hiện login bằng devise thì chúng ta sử dụng before_action để thực hiện kiểm tra và hiển thị form nhập OTP. Việc thêm before_action sẽ được thêm trong controller login, thường là sessions_controller của devise.

module AuthenticateWithOtpTwoFactor
  extend ActiveSupport::Concern

  def authenticate_with_otp_two_factor
    user = self.resource = find_user

    return prompt_for_two_factor(user) if session[:otp_user_id].blank?

    authenticate_user_with_backup_code_two_factor(user) if params[:code_type] == "backup_code"
    authenticate_user_with_otp_two_factor(user) unless params[:code_type] == "backup_code"
  end

  private

  def prompt_for_two_factor user
    return unless session[:otp_user_id] || user&.valid_password?(user_params[:password])

    @user = user
    session[:otp_user_id] = user.id
    render "devise/sessions/two_factor"
  end

  def authenticate_user_with_otp_two_factor user
    if user_params[:otp_attempt].blank?
      flash.now[:danger] = "OTP Code not blank"
      prompt_for_two_factor user
      return
    end

    if user.current_otp == user_params[:otp_attempt] && user&.validate_and_consume_otp!(user_params[:otp_attempt])
      session.delete :otp_user_id
      sign_in user
    else
      flash.now[:danger] = "OTP Code invalid. Please try again"
      prompt_for_two_factor user
    end
  rescue StandardError => e
    flash.now[:danger] = e.message
    prompt_for_two_factor user
  end

  def authenticate_user_with_backup_code_two_factor user
    if user_params[:backup_code_attempt].blank?
      flash.now[:danger] = "Backup Code not blank. Please try again"
      prompt_for_two_factor user
      return
    end

    if user&.invalidate_otp_backup_code!(user_params[:backup_code_attempt])
      session.delete :otp_user_id
      sign_in user
    else
      flash.now[:danger] = "Backup code invalid"
      prompt_for_two_factor user
    end
  rescue StandardError => e
    flash.now[:danger] = e.message
    prompt_for_two_factor admin
  end

  def user_params
    params.require(:user).permit(:email, :password, :otp_attempt, :backup_code_attempt)
  end

  def find_user
    if user_params[:email]
      User.find_by email: user_params[:email]
    elsif session[:otp_user_id]
      User.find_by id: session[:otp_user_id]
    end
  end

  def otp_two_factor_enabled?
    find_user&.otp_required_for_login?
  end
end

Chúng ta tạo ra module AuthenticateWithOtpTwoFactor để thực hiện việc kiểm tra OTP và backup code khi login. Việc check mã otp lấy từ app google authenticator và mã backup code sẽ là 2 function 2 khác nhau, vì vậy trên view cũng sẽ là 2 form nhập khác nhau

Include module này vào sessions_controller

include AuthenticateWithOtpTwoFactor

prepend_before_action :authenticate_with_otp_two_factor, if: -> {action_name == "create" && otp_two_factor_enabled?}

Mã HTML của trang nhập OTP

<div class="container jumbotron">
  <body class="login">
    <div>
      <div class="login_wrapper">
        <div class="animate form login_form">
          <section class="login_content">
            <% flash.each do |key, value| %>
              <% unless value.is_a? Hash%>
                <div class="alert alert-<%= key == "notice" ? "info" : "danger"%>">
                  <span class="close" data-dismiss="alert" aria-label="close">&times;</span>
                  <%= value %>
                </div>
              <% end %>
            <% end %>
            <%= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| %>
              <div class="otp_code <%= "hide" if params[:code_type] == "backup_code" %>">
                <div>
                  <%= f.text_field :otp_attempt, class: "form-control", placeholder: "Enter OTP Code", autofocus: "autofocus" %>
                </div>
                <div class="form-group">
                  <span class="recover_code_link">Switch to backup code</span>
                </div>
              </div>
              <div class="backup_code <%= "hide" if params[:code_type] != "backup_code" %>">
                <div>
                  <%= f.text_field :backup_code_attempt, class: "form-control", placeholder: "Enter backup code", autofocus: "autofocus" %>
                </div>
                <div class="form-group">
                  <span class="otp_code_link">Switch to otp code</span>
                </div>
              </div>
              <div class="form-group">
                <input type="hidden" name="code_type" id="admin_code_type" value="<%= params[:code_type]%>" />
                <%= f.button :submit, class: "btn btn-default submit" do %>
                  <span class="text">Confirm</span>
                <% end %>
              </div>
              <div class="clearfix"></div>
            <% end %>
          </section>
        </div>
      </div>
    </div>
  </body>
</div>

Đến đây là chúng ta đã hoàn thành chức năng xác thực 2 bước với devise-two-factorrqrcode rồi.

Kết

  • Đây là source code mẫu để các bạn tham khảo github
  • Mình có đẩy source mẫu này lên heroku các bạn vào đây sign_up để tạo một tài khoản rồi sau sau đó login để trải nghiệm nhé

Cám ơn các bạn đã t

Source: https://viblo.asia/p/xac-thuc-2-buoc-voi-google-authenticator-chua-bao-gio-de-dang-den-the-p2-bJzKmVVEZ9N