Module: EasyAdmin::TwoFactorAuthentication

Extended by:
ActiveSupport::Concern
Included in:
AdminUser
Defined in:
lib/easy_admin/two_factor_authentication.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.available?Boolean

Check if required gems are available

Returns:

  • (Boolean)


6
7
8
9
10
11
12
13
14
# File 'lib/easy_admin/two_factor_authentication.rb', line 6

def self.available?
  @available ||= begin
    require 'rotp'
    require 'rqrcode'
    true
  rescue LoadError
    false
  end
end

Instance Method Details

#backup_codes_remainingObject



97
98
99
# File 'lib/easy_admin/two_factor_authentication.rb', line 97

def backup_codes_remaining
  two_factor_available? ? (otp_backup_codes&.length || 0) : 0
end

#current_otpObject



40
41
42
43
44
45
# File 'lib/easy_admin/two_factor_authentication.rb', line 40

def current_otp
  return nil unless two_factor_available? && otp_secret.present?
  
  require 'rotp'
  ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin").now
end

#disable_two_factor!Object



131
132
133
134
135
136
137
138
# File 'lib/easy_admin/two_factor_authentication.rb', line 131

def disable_two_factor!
  update!(
    otp_required_for_login: false,
    otp_secret: nil,
    otp_backup_codes: nil,
    last_otp_at: nil
  )
end

#enable_two_factor!Object



125
126
127
128
129
# File 'lib/easy_admin/two_factor_authentication.rb', line 125

def enable_two_factor!
  return false unless two_factor_available? && otp_secret.present?
  
  update!(otp_required_for_login: true)
end

#generate_backup_codes!Object



79
80
81
82
83
84
85
86
87
# File 'lib/easy_admin/two_factor_authentication.rb', line 79

def generate_backup_codes!
  return false unless two_factor_available?
  
  # Generate 10 backup codes (8 characters each)
  codes = 10.times.map { SecureRandom.hex(4).upcase }
  self.otp_backup_codes = codes
  save!
  codes
end

#generate_otp_secret!Object



32
33
34
35
36
37
38
# File 'lib/easy_admin/two_factor_authentication.rb', line 32

def generate_otp_secret!
  return false unless two_factor_available?
  
  require 'rotp'
  self.otp_secret = ROTP::Base32.random
  save!
end

#invalidate_backup_code!(code) ⇒ Object



89
90
91
92
93
94
95
# File 'lib/easy_admin/two_factor_authentication.rb', line 89

def invalidate_backup_code!(code)
  return false unless two_factor_available?
  
  normalized_code = code.to_s.upcase.strip
  self.otp_backup_codes = otp_backup_codes.reject { |c| c == normalized_code }
  save!
end

#provisioning_uriObject



101
102
103
104
105
106
# File 'lib/easy_admin/two_factor_authentication.rb', line 101

def provisioning_uri
  return nil unless two_factor_available? && otp_secret.present?
  
  require 'rotp'
  ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin").provisioning_uri(email)
end

#qr_code_svg(size: 200) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/easy_admin/two_factor_authentication.rb', line 108

def qr_code_svg(size: 200)
  return nil unless two_factor_available?
  
  uri = provisioning_uri
  return nil if uri.blank?
  
  require 'rqrcode'
  
  qr_code = RQRCode::QRCode.new(uri)
  qr_code.as_svg(
    viewbox: true,
    module_size: 4,
    standalone: true,
    use_path: true
  )
end

#should_enable_two_factor?Boolean

Returns:

  • (Boolean)


152
153
154
# File 'lib/easy_admin/two_factor_authentication.rb', line 152

def should_enable_two_factor?
  two_factor_required? && !two_factor_enabled?
end

#two_factor_available?Boolean

Returns:

  • (Boolean)


24
25
26
# File 'lib/easy_admin/two_factor_authentication.rb', line 24

def two_factor_available?
  EasyAdmin::TwoFactorAuthentication.available?
end

#two_factor_enabled?Boolean

Returns:

  • (Boolean)


28
29
30
# File 'lib/easy_admin/two_factor_authentication.rb', line 28

def two_factor_enabled?
  two_factor_available? && otp_required_for_login? && otp_secret.present?
end

#two_factor_required?Boolean

Check if user needs 2FA based on role requirements

Returns:

  • (Boolean)


141
142
143
144
145
146
147
148
149
150
# File 'lib/easy_admin/two_factor_authentication.rb', line 141

def two_factor_required?
  return false unless two_factor_available?
  
  # Check if role requires 2FA (if role system exists)
  if respond_to?(:role) && role.respond_to?(:require_two_factor?)
    role.require_two_factor?
  else
    false
  end
end

#validate_and_consume_otp!(token) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/easy_admin/two_factor_authentication.rb', line 47

def validate_and_consume_otp!(token)
  return false unless two_factor_available? && otp_secret.present?
  return false if token.blank?
  
  require 'rotp'
  
  totp = ROTP::TOTP.new(otp_secret, issuer: "EasyAdmin")
  last_otp_at_timestamp = last_otp_at&.to_i
  
  # Verify with 30-second drift tolerance and replay protection
  if totp.verify(token.to_s, drift_behind: 30, drift_ahead: 30, after: last_otp_at_timestamp)
    touch(:last_otp_at)
    true
  else
    false
  end
end

#validate_backup_code!(code) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/easy_admin/two_factor_authentication.rb', line 65

def validate_backup_code!(code)
  return false unless two_factor_available?
  return false if code.blank? || otp_backup_codes.blank?
  
  normalized_code = code.to_s.upcase.strip
  
  if otp_backup_codes.include?(normalized_code)
    invalidate_backup_code!(normalized_code)
    true
  else
    false
  end
end