1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
|
require 'rotp'
module Devise
module Models
module TwoFactorAuthenticatable
extend ActiveSupport::Concern
include Devise::Models::DatabaseAuthenticatable
included do
unless %i[otp_secret otp_secret=].all? { |attr| method_defined?(attr) }
require 'attr_encrypted'
unless singleton_class.ancestors.include?(AttrEncrypted)
extend AttrEncrypted
end
unless attr_encrypted?(:otp_secret)
attr_encrypted :otp_secret,
:key => self.otp_secret_encryption_key,
:mode => :per_attribute_iv_and_salt unless self.attr_encrypted?(:otp_secret)
end
end
attr_accessor :otp_attempt
end
def self.required_fields(klass)
[:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep]
end
# This defaults to the model's otp_secret
# If this hasn't been generated yet, pass a secret as an option
def validate_and_consume_otp!(code, options = {})
otp_secret = options[:otp_secret] || self.otp_secret
return false unless code.present? && otp_secret.present?
totp = otp(otp_secret)
if self.consumed_timestep
# reconstruct the timestamp of the last consumed timestep
after_timestamp = self.consumed_timestep * otp.interval
end
if totp.verify(code.gsub(/\s+/, ""), drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift, after: after_timestamp)
return consume_otp!
end
false
end
def otp(otp_secret = self.otp_secret)
ROTP::TOTP.new(otp_secret)
end
def current_otp
otp.at(Time.now)
end
# ROTP's TOTP#timecode is private, so we duplicate it here
def current_otp_timestep
Time.now.utc.to_i / otp.interval
end
def otp_provisioning_uri(account, options = {})
otp_secret = options[:otp_secret] || self.otp_secret
ROTP::TOTP.new(otp_secret, options).provisioning_uri(account)
end
def clean_up_passwords
super
self.otp_attempt = nil
end
protected
# An OTP cannot be used more than once in a given timestep
# Storing timestep of last valid OTP is sufficient to satisfy this requirement
def consume_otp!
if self.consumed_timestep != current_otp_timestep
self.consumed_timestep = current_otp_timestep
return save(validate: false)
end
false
end
module ClassMethods
Devise::Models.config(self, :otp_secret_length,
:otp_allowed_drift,
:otp_secret_encryption_key)
def generate_otp_secret(otp_secret_length = self.otp_secret_length)
ROTP::Base32.random_base32(otp_secret_length)
end
end
end
end
end
|