Module: EasyAdmin::TwoFactorAuthentication
- Extended by:
- ActiveSupport::Concern
- Included in:
- AdminUser
- Defined in:
- lib/easy_admin/two_factor_authentication.rb
Class Method Summary collapse
-
.available? ⇒ Boolean
Check if required gems are available.
Instance Method Summary collapse
- #backup_codes_remaining ⇒ Object
- #current_otp ⇒ Object
- #disable_two_factor! ⇒ Object
- #enable_two_factor! ⇒ Object
- #generate_backup_codes! ⇒ Object
- #generate_otp_secret! ⇒ Object
- #invalidate_backup_code!(code) ⇒ Object
- #provisioning_uri ⇒ Object
- #qr_code_svg(size: 200) ⇒ Object
- #should_enable_two_factor? ⇒ Boolean
- #two_factor_available? ⇒ Boolean
- #two_factor_enabled? ⇒ Boolean
-
#two_factor_required? ⇒ Boolean
Check if user needs 2FA based on role requirements.
- #validate_and_consume_otp!(token) ⇒ Object
- #validate_backup_code!(code) ⇒ Object
Class Method Details
.available? ⇒ Boolean
Check if required gems are available
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_remaining ⇒ Object
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_otp ⇒ Object
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_uri ⇒ Object
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
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
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
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
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&.to_i # Verify with 30-second drift tolerance and replay protection if totp.verify(token.to_s, drift_behind: 30, drift_ahead: 30, after: ) 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 |