# frozen_string_literal: true require "rotp" module Maquina module Multifactor extend ActiveSupport::Concern included do if has_attribute?(:otp_secret_key) && has_attribute?(:otp_recovery_codes) encrypts :otp_secret_key, :otp_recovery_codes def multifactor? otp_secret_key.present? end def enable_multifactor!(multifactor_secret_key) self.otp_secret_key = multifactor_secret_key self.otp_recovery_codes = 10.times.map { SecureRandom.alphanumeric(12) }.join(" ") save! end def disable_multifactor! self.last_otp_at = nil self.otp_secret_key = nil self.otp_recovery_codes = nil save! end def verify_multifactor_code!(multifactor_code, multifactor_secret_key = nil) multifactor_secret_key ||= otp_secret_key options = {drift_behind: 15} options[:after] = last_otp_at.to_i if last_otp_at.present? last_at = totp_instance(multifactor_secret_key).verify(multifactor_code, **options) if last_at.present? self.last_otp_at = Time.at(last_at).utc.to_datetime save! end last_at end def verify_multifactor_recovery_code!(recovery_code) return nil if !multifactor? codes = otp_recovery_codes.split(" ") valid_code = codes.detect do |code| ActiveSupport::SecurityUtils.secure_compare(recovery_code.strip, code) end if valid_code.present? codes.delete(recovery_code.strip) self.otp_recovery_codes = codes.join(" ") save! end valid_code end private def totp_instance(multifactor_secret_key = nil) multifactor_secret_key ||= otp_secret_key @totp_instance ||= ROTP::TOTP.new(multifactor_secret_key, issuer: I18n.t(:application_name), default: "Maquina") end end end class_methods do def generate_multifactor_secret ROTP::Base32.random end end end end