Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ config.otp_length = 6 # TOTP code length
config.direct_otp_valid_for = 5.minutes # Time before direct OTP becomes invalid
config.direct_otp_length = 6 # Direct OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to perform 2fA again. Default is 0.
config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY']
config.otp_secret_encryption_key = ENV['OTP_SECRET_ENCRYPTION_KEY'] # Can be also a lambda
config.second_factor_resource_id = 'id' # Field or method name used to set value for 2fA remember cookie
config.delete_cookie_on_logout = false # Delete cookie when user signs out, to force 2fA again on login
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ def after_two_factor_success_for(resource)
end

def set_remember_two_factor_cookie(resource)
expires_seconds = resource.class.remember_otp_session_for_seconds
expires_seconds = if resource.respond_to?(:remember_otp_session_for_seconds)
resource.remember_otp_session_for_seconds
else
resource.class.remember_otp_session_for_seconds
end

if expires_seconds && expires_seconds > 0
cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
Expand Down
15 changes: 12 additions & 3 deletions lib/two_factor_authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,23 @@ module Devise
mattr_accessor :remember_otp_session_for_seconds
@@remember_otp_session_for_seconds = 0

mattr_accessor :otp_secret_encryption_key
@@otp_secret_encryption_key = ''

mattr_accessor :second_factor_resource_id
@@second_factor_resource_id = 'id'

mattr_accessor :delete_cookie_on_logout
@@delete_cookie_on_logout = false

mattr_writer :otp_secret_encryption_key
@@otp_secret_encryption_key = ''

def self.otp_secret_encryption_key
if @@otp_secret_encryption_key.respond_to?(:call)
@@otp_secret_encryption_key.call
else
@@otp_secret_encryption_key
end
end
delegate :otp_secret_encryption_key, to: 'self.class'
end

module TwoFactorAuthentication
Expand Down
13 changes: 10 additions & 3 deletions lib/two_factor_authentication/hooks/two_factor_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Warden::Manager.after_authentication do |user, auth, options|
if auth.env["action_dispatch.cookies"]
cookie_jar = auth.cookies || auth.env["action_dispatch.cookies"]
if cookie_jar
expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}"
actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
actual_cookie_value = cookie_jar.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
bypass_by_cookie = actual_cookie_value == expected_cookie_value
end

Expand All @@ -13,5 +14,11 @@
end

Warden::Manager.before_logout do |user, auth, _options|
auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
should_delete = Devise.delete_cookie_on_logout

if user.respond_to?(:delete_cookie_on_logout?)
should_delete = user.delete_cookie_on_logout?
end

auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if should_delete
end
28 changes: 20 additions & 8 deletions lib/two_factor_authentication/models/two_factor_authenticatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def authenticate_direct_otp(code)

def authenticate_totp(code, options = {})
totp_secret = options[:otp_secret_key] || otp_secret_key
digits = options[:otp_length] || self.class.otp_length
drift = options[:drift] || self.class.allowed_otp_drift_seconds
digits = options[:otp_length] || (self.respond_to?(:otp_length) && self.otp_length) || self.class.otp_length
drift = options[:drift] || (self.respond_to?(:allowed_otp_drift_seconds) && self.allowed_otp_drift_seconds) || self.class.allowed_otp_drift_seconds
raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
totp = ROTP::TOTP.new(totp_secret, digits: digits)
new_timestamp = totp.verify(
Expand All @@ -50,7 +50,7 @@ def authenticate_totp(code, options = {})

def provisioning_uri(account = nil, options = {})
totp_secret = options[:otp_secret_key] || otp_secret_key
options[:digits] ||= options[:otp_length] || self.class.otp_length
options[:digits] ||= options[:otp_length] || (self.respond_to?(:otp_length) && self.otp_length) || self.class.otp_length
raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
account ||= email if respond_to?(:email)
ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
Expand All @@ -74,11 +74,15 @@ def send_two_factor_authentication_code(code)
end

def max_login_attempts?
second_factor_attempts_count.to_i >= max_login_attempts.to_i
second_factor_attempts_count.to_i > max_login_attempts.to_i
end

def max_login_attempts
self.class.max_login_attempts
self.max_login_attempts
end

def attempts_left
max_login_attempts.to_i - second_factor_attempts_count.to_i
end

def totp_enabled?
Expand All @@ -100,7 +104,7 @@ def generate_totp_secret

def create_direct_otp(options = {})
# Create a new random OTP and store it in the database
digits = options[:length] || self.class.direct_otp_length || 6
digits = options[:length] || (self.respond_to?(:direct_otp_length) && self.direct_otp_length) || self.class.direct_otp_length || 6
update_attributes(
direct_otp: random_base10(digits),
direct_otp_sent_at: Time.now.utc
Expand All @@ -118,7 +122,7 @@ def random_base10(digits)
end

def direct_otp_expired?
Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for
Time.now.utc > direct_otp_sent_at + self.direct_otp_valid_for
end

def clear_direct_otp
Expand Down Expand Up @@ -166,13 +170,21 @@ def otp_encrypt(value)
def encryption_options_for(value)
{
value: value,
key: Devise.otp_secret_encryption_key,
key: otp_secret_encryption_key,
iv: iv_for_attribute,
salt: salt_for_attribute,
algorithm: 'aes-256-cbc'
}
end

def otp_secret_encryption_key
if self.respond_to?(:otp_secret_encryption_key)
self.otp_secret_encryption_key
else
Devise.otp_secret_encryption_key
end
end

def iv_for_attribute(algorithm = 'aes-256-cbc')
iv = encrypted_otp_secret_key_iv

Expand Down
2 changes: 1 addition & 1 deletion lib/two_factor_authentication/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class Mapper
protected

def devise_two_factor_authentication(mapping, controllers)
resource :two_factor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
resource :two_factor_authentication, :only => [:show, :update], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
collection { get "resend_code" }
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/two_factor_authentication/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module TwoFactorAuthentication
VERSION = "2.2.0".freeze
VERSION = "2.3.1".freeze
end