rodauth 0.10.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +146 -0
- data/README.rdoc +644 -220
- data/Rakefile +99 -11
- data/doc/account_expiration.rdoc +55 -0
- data/doc/base.rdoc +104 -0
- data/doc/change_login.rdoc +29 -0
- data/doc/change_password.rdoc +26 -0
- data/doc/close_account.rdoc +31 -0
- data/doc/confirm_password.rdoc +22 -0
- data/doc/create_account.rdoc +34 -0
- data/doc/disallow_password_reuse.rdoc +37 -0
- data/doc/email_base.rdoc +19 -0
- data/doc/jwt.rdoc +35 -0
- data/doc/lockout.rdoc +83 -0
- data/doc/login.rdoc +27 -0
- data/doc/login_password_requirements_base.rdoc +50 -0
- data/doc/logout.rdoc +21 -0
- data/doc/otp.rdoc +100 -0
- data/doc/password_complexity.rdoc +50 -0
- data/doc/password_expiration.rdoc +52 -0
- data/doc/password_grace_period.rdoc +10 -0
- data/doc/recovery_codes.rdoc +60 -0
- data/doc/release_notes/1.0.0.txt +443 -0
- data/doc/remember.rdoc +82 -0
- data/doc/reset_password.rdoc +70 -0
- data/doc/session_expiration.rdoc +27 -0
- data/doc/single_session.rdoc +43 -0
- data/doc/sms_codes.rdoc +119 -0
- data/doc/two_factor_base.rdoc +27 -0
- data/doc/verify_account.rdoc +70 -0
- data/doc/verify_account_grace_period.rdoc +15 -0
- data/doc/verify_change_login.rdoc +9 -0
- data/lib/roda/plugins/rodauth.rb +3 -262
- data/lib/rodauth.rb +260 -0
- data/lib/rodauth/features/account_expiration.rb +108 -0
- data/lib/rodauth/features/base.rb +479 -0
- data/lib/rodauth/features/change_login.rb +77 -0
- data/lib/rodauth/features/change_password.rb +66 -0
- data/lib/rodauth/features/close_account.rb +82 -0
- data/lib/rodauth/features/confirm_password.rb +51 -0
- data/lib/rodauth/features/create_account.rb +128 -0
- data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
- data/lib/rodauth/features/email_base.rb +63 -0
- data/lib/rodauth/features/jwt.rb +151 -0
- data/lib/rodauth/features/lockout.rb +262 -0
- data/lib/rodauth/features/login.rb +61 -0
- data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
- data/lib/rodauth/features/logout.rb +37 -0
- data/lib/rodauth/features/otp.rb +338 -0
- data/lib/rodauth/features/password_complexity.rb +89 -0
- data/lib/rodauth/features/password_expiration.rb +111 -0
- data/lib/rodauth/features/password_grace_period.rb +46 -0
- data/lib/rodauth/features/recovery_codes.rb +240 -0
- data/lib/rodauth/features/remember.rb +200 -0
- data/lib/rodauth/features/reset_password.rb +207 -0
- data/lib/rodauth/features/session_expiration.rb +55 -0
- data/lib/rodauth/features/single_session.rb +87 -0
- data/lib/rodauth/features/sms_codes.rb +498 -0
- data/lib/rodauth/features/two_factor_base.rb +135 -0
- data/lib/rodauth/features/verify_account.rb +232 -0
- data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
- data/lib/rodauth/features/verify_change_login.rb +20 -0
- data/lib/rodauth/migrations.rb +130 -0
- data/lib/rodauth/version.rb +9 -0
- data/spec/account_expiration_spec.rb +90 -0
- data/spec/all.rb +1 -0
- data/spec/change_login_spec.rb +149 -0
- data/spec/change_password_spec.rb +177 -0
- data/spec/close_account_spec.rb +162 -0
- data/spec/confirm_password_spec.rb +70 -0
- data/spec/create_account_spec.rb +127 -0
- data/spec/disallow_password_reuse_spec.rb +84 -0
- data/spec/lockout_spec.rb +228 -0
- data/spec/login_spec.rb +188 -0
- data/spec/migrate/001_tables.rb +103 -16
- data/spec/migrate/002_account_password_hash_column.rb +11 -0
- data/spec/migrate_password/001_tables.rb +60 -42
- data/spec/migrate_travis/001_tables.rb +116 -0
- data/spec/password_complexity_spec.rb +108 -0
- data/spec/password_expiration_spec.rb +243 -0
- data/spec/password_grace_period_spec.rb +93 -0
- data/spec/remember_spec.rb +424 -0
- data/spec/reset_password_spec.rb +185 -0
- data/spec/rodauth_spec.rb +57 -980
- data/spec/session_expiration_spec.rb +58 -0
- data/spec/single_session_spec.rb +107 -0
- data/spec/spec_helper.rb +202 -0
- data/spec/two_factor_spec.rb +1310 -0
- data/spec/verify_account_grace_period_spec.rb +135 -0
- data/spec/verify_account_spec.rb +142 -0
- data/spec/verify_change_login_spec.rb +46 -0
- data/spec/views/login.str +2 -2
- data/templates/add-recovery-codes.str +2 -0
- data/templates/button.str +5 -0
- data/templates/change-login.str +5 -18
- data/templates/change-password.str +6 -14
- data/templates/close-account.str +3 -6
- data/templates/confirm-password.str +4 -14
- data/templates/create-account.str +6 -30
- data/templates/login-confirm-field.str +6 -0
- data/templates/login-field.str +6 -0
- data/templates/login.str +5 -19
- data/templates/logout.str +2 -6
- data/templates/otp-auth-code-field.str +6 -0
- data/templates/otp-auth.str +8 -0
- data/templates/otp-disable.str +6 -0
- data/templates/otp-setup.str +21 -0
- data/templates/password-confirm-field.str +6 -0
- data/templates/password-field.str +6 -0
- data/templates/recovery-auth.str +12 -0
- data/templates/recovery-codes.str +6 -0
- data/templates/remember.str +8 -12
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +4 -18
- data/templates/sms-auth.str +6 -0
- data/templates/sms-code-field.str +6 -0
- data/templates/sms-confirm.str +7 -0
- data/templates/sms-disable.str +7 -0
- data/templates/sms-request.str +5 -0
- data/templates/sms-setup.str +12 -0
- data/templates/unlock-account-request.str +3 -7
- data/templates/unlock-account.str +4 -7
- data/templates/verify-account-resend.str +2 -2
- data/templates/verify-account.str +2 -6
- metadata +191 -29
- data/lib/roda/plugins/rodauth/base.rb +0 -428
- data/lib/roda/plugins/rodauth/change_login.rb +0 -48
- data/lib/roda/plugins/rodauth/change_password.rb +0 -42
- data/lib/roda/plugins/rodauth/close_account.rb +0 -42
- data/lib/roda/plugins/rodauth/create_account.rb +0 -92
- data/lib/roda/plugins/rodauth/lockout.rb +0 -292
- data/lib/roda/plugins/rodauth/login.rb +0 -81
- data/lib/roda/plugins/rodauth/logout.rb +0 -36
- data/lib/roda/plugins/rodauth/remember.rb +0 -226
- data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
- data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
PasswordComplexity = Feature.define(:password_complexity) do
|
|
5
|
+
depends :login_password_requirements_base
|
|
6
|
+
|
|
7
|
+
auth_value_method :password_dictionary_file, nil
|
|
8
|
+
auth_value_method :password_dictionary, nil
|
|
9
|
+
auth_value_method :password_character_groups, [/[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z\d]/]
|
|
10
|
+
auth_value_method :password_min_groups, 3
|
|
11
|
+
auth_value_method :password_max_length_for_groups_check, 11
|
|
12
|
+
auth_value_method :password_max_repeating_characters, 3
|
|
13
|
+
auth_value_method :password_invalid_pattern, Regexp.union([/qwerty/i, /azerty/i, /asdf/i, /zxcv/i] + (1..8).map{|i| /#{i}#{i+1}#{(i+2)%10}/})
|
|
14
|
+
auth_value_method :password_not_enough_character_groups_message, "does not include uppercase letters, lowercase letters, and numbers"
|
|
15
|
+
auth_value_method :password_invalid_pattern_message, "includes common character sequence"
|
|
16
|
+
auth_value_method :password_in_dictionary_message, "is a word in a dictionary"
|
|
17
|
+
|
|
18
|
+
auth_value_methods(
|
|
19
|
+
:password_too_many_repeating_characters_message
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def password_meets_requirements?(password)
|
|
23
|
+
super && \
|
|
24
|
+
password_has_enough_character_groups?(password) && \
|
|
25
|
+
password_has_no_invalid_pattern?(password) && \
|
|
26
|
+
password_not_too_many_repeating_characters?(password) && \
|
|
27
|
+
password_not_in_dictionary?(password)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def post_configure
|
|
31
|
+
super
|
|
32
|
+
return if singleton_methods.map(&:to_sym).include?(:password_dictionary)
|
|
33
|
+
|
|
34
|
+
case dictionary_file = password_dictionary_file
|
|
35
|
+
when false
|
|
36
|
+
return
|
|
37
|
+
when nil
|
|
38
|
+
default_dictionary_file = '/usr/share/dict/words'
|
|
39
|
+
if File.file?(default_dictionary_file)
|
|
40
|
+
words = File.read(default_dictionary_file)
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
words = File.read(password_dictionary_file)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return unless words
|
|
47
|
+
|
|
48
|
+
require 'set'
|
|
49
|
+
dict = Set.new(words.downcase.split)
|
|
50
|
+
self.class.send(:define_method, :password_dictionary){dict}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def password_has_enough_character_groups?(password)
|
|
56
|
+
return true if password.length > password_max_length_for_groups_check
|
|
57
|
+
return true if password_character_groups.select{|re| password =~ re}.length >= password_min_groups
|
|
58
|
+
@password_requirement_message = password_not_enough_character_groups_message
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def password_has_no_invalid_pattern?(password)
|
|
63
|
+
return true unless password_invalid_pattern
|
|
64
|
+
return true if password !~ password_invalid_pattern
|
|
65
|
+
@password_requirement_message = password_invalid_pattern_message
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def password_not_too_many_repeating_characters?(password)
|
|
70
|
+
return true if password_max_repeating_characters < 2
|
|
71
|
+
return true if password !~ /(.)(\1){#{password_max_repeating_characters-1}}/
|
|
72
|
+
@password_requirement_message = password_too_many_repeating_characters_message
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def password_too_many_repeating_characters_message
|
|
77
|
+
"contains #{password_max_repeating_characters} or more of the same character in a row"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def password_not_in_dictionary?(password)
|
|
81
|
+
return true unless dict = password_dictionary
|
|
82
|
+
return true unless password =~ /\A(?:\d*)([A-Za-z!@$+|][A-Za-z!@$+|0134578]+[A-Za-z!@$+|])(?:\d*)\z/
|
|
83
|
+
word = $1.downcase.tr('!@$+|0134578', 'iastloleastb')
|
|
84
|
+
return true if !dict.include?(word)
|
|
85
|
+
@password_requirement_message = password_in_dictionary_message
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
PasswordExpiration = Feature.define(:password_expiration) do
|
|
5
|
+
depends :login, :change_password
|
|
6
|
+
|
|
7
|
+
error_flash "Your password has expired and needs to be changed"
|
|
8
|
+
error_flash "Your password cannot be changed yet", 'password_not_changeable_yet'
|
|
9
|
+
|
|
10
|
+
redirect :password_not_changeable_yet
|
|
11
|
+
redirect(:password_change_needed){"#{prefix}/#{change_password_route}"}
|
|
12
|
+
|
|
13
|
+
auth_value_method :allow_password_change_after, 0
|
|
14
|
+
auth_value_method :require_password_change_after, 90*86400
|
|
15
|
+
auth_value_method :password_expiration_table, :account_password_change_times
|
|
16
|
+
auth_value_method :password_expiration_id_column, :id
|
|
17
|
+
auth_value_method :password_expiration_changed_at_column, :changed_at
|
|
18
|
+
auth_value_method :password_changed_at_session_key, :password_changed_at
|
|
19
|
+
auth_value_method :password_expiration_default, false
|
|
20
|
+
|
|
21
|
+
auth_methods(
|
|
22
|
+
:password_expired?,
|
|
23
|
+
:update_password_changed_at
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def get_password_changed_at
|
|
27
|
+
convert_timestamp(password_expiration_ds.get(password_expiration_changed_at_column))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_password_change_allowed
|
|
31
|
+
if password_changed_at = get_password_changed_at
|
|
32
|
+
if password_changed_at > Time.now - allow_password_change_after
|
|
33
|
+
set_redirect_error_flash password_not_changeable_yet_error_flash
|
|
34
|
+
redirect password_not_changeable_yet_redirect
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_password(password)
|
|
40
|
+
update_password_changed_at
|
|
41
|
+
session[password_changed_at_session_key] = Time.now.to_i
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def account_from_reset_password_key(key)
|
|
46
|
+
if a = super
|
|
47
|
+
check_password_change_allowed
|
|
48
|
+
end
|
|
49
|
+
a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update_password_changed_at
|
|
53
|
+
ds = password_expiration_ds
|
|
54
|
+
if ds.update(password_expiration_changed_at_column=>Sequel::CURRENT_TIMESTAMP) == 0
|
|
55
|
+
# Ignoring the violation is safe here, since a concurrent insert would also set it to the
|
|
56
|
+
# current timestamp.
|
|
57
|
+
ignore_uniqueness_violation{ds.insert(password_expiration_id_column=>account_id)}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def require_current_password
|
|
62
|
+
if authenticated? && password_expired? && password_change_needed_redirect != request.path_info
|
|
63
|
+
set_redirect_error_flash password_expiration_error_flash
|
|
64
|
+
redirect password_change_needed_redirect
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def password_expired?
|
|
69
|
+
if password_changed_at = session[password_changed_at_session_key]
|
|
70
|
+
return password_changed_at + require_password_change_after < Time.now.to_i
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
account_from_session
|
|
74
|
+
if password_changed_at = get_password_changed_at
|
|
75
|
+
set_session_value(password_changed_at_session_key, password_changed_at.to_i)
|
|
76
|
+
password_changed_at + require_password_change_after < Time.now
|
|
77
|
+
else
|
|
78
|
+
set_session_value(password_changed_at_session_key, password_expiration_default ? 0 : 2147483647)
|
|
79
|
+
password_expiration_default
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def after_close_account
|
|
86
|
+
super if defined?(super)
|
|
87
|
+
password_expiration_ds.delete
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def before_change_password_route
|
|
91
|
+
check_password_change_allowed
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def after_create_account
|
|
96
|
+
if account_password_hash_column
|
|
97
|
+
update_password_changed_at
|
|
98
|
+
end
|
|
99
|
+
super if defined?(super)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def after_login
|
|
103
|
+
require_current_password
|
|
104
|
+
super
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def password_expiration_ds
|
|
108
|
+
db[password_expiration_table].where(password_expiration_id_column=>account_id)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
PasswordGracePeriod = Feature.define(:password_grace_period) do
|
|
5
|
+
auth_value_method :password_grace_period, 300
|
|
6
|
+
auth_value_method :last_password_entry_session_key, :last_password_entry
|
|
7
|
+
|
|
8
|
+
def modifications_require_password?
|
|
9
|
+
return false unless super
|
|
10
|
+
!password_recently_entered?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def password_match?(_)
|
|
14
|
+
if v = super
|
|
15
|
+
@last_password_entry = set_last_password_entry
|
|
16
|
+
end
|
|
17
|
+
v
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def after_create_account
|
|
23
|
+
super if defined?(super)
|
|
24
|
+
@last_password_entry = Time.now.to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def after_reset_password
|
|
28
|
+
super if defined?(super)
|
|
29
|
+
@last_password_entry = Time.now.to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update_session
|
|
33
|
+
super
|
|
34
|
+
session[last_password_entry_session_key] = @last_password_entry if defined?(@last_password_entry)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def password_recently_entered?
|
|
38
|
+
return false unless last_password_entry = session[last_password_entry_session_key]
|
|
39
|
+
last_password_entry + password_grace_period > Time.now.to_i
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_last_password_entry
|
|
43
|
+
session[last_password_entry_session_key] = Time.now.to_i
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
RecoveryCodes = Feature.define(:recovery_codes) do
|
|
5
|
+
depends :two_factor_base
|
|
6
|
+
|
|
7
|
+
additional_form_tags 'recovery_auth'
|
|
8
|
+
additional_form_tags 'recovery_codes'
|
|
9
|
+
|
|
10
|
+
before 'add_recovery_codes'
|
|
11
|
+
before 'view_recovery_codes'
|
|
12
|
+
before 'recovery_auth'
|
|
13
|
+
before 'recovery_auth_route'
|
|
14
|
+
before 'recovery_codes_route'
|
|
15
|
+
|
|
16
|
+
after 'add_recovery_codes'
|
|
17
|
+
|
|
18
|
+
button 'Add Authentication Recovery Codes', 'add_recovery_codes'
|
|
19
|
+
button 'Authenticate via Recovery Code', 'recovery_auth'
|
|
20
|
+
button 'View Authentication Recovery Codes', 'view_recovery_codes'
|
|
21
|
+
|
|
22
|
+
error_flash "Error authenticating via recovery code.", 'invalid_recovery_code'
|
|
23
|
+
error_flash "Unable to add recovery codes.", 'add_recovery_codes'
|
|
24
|
+
error_flash "Unable to view recovery codes.", 'view_recovery_codes'
|
|
25
|
+
|
|
26
|
+
notice_flash "Additional authentication recovery codes have been added.", 'recovery_codes_added'
|
|
27
|
+
|
|
28
|
+
redirect(:recovery_auth){"#{prefix}/#{recovery_auth_route}"}
|
|
29
|
+
redirect(:add_recovery_codes){"#{prefix}/#{recovery_codes_route}"}
|
|
30
|
+
|
|
31
|
+
view 'add-recovery-codes', 'Authentication Recovery Codes', 'add_recovery_codes'
|
|
32
|
+
view 'recovery-auth', 'Enter Authentication Recovery Code', 'recovery_auth'
|
|
33
|
+
view 'recovery-codes', 'View Authentication Recovery Codes', 'recovery_codes'
|
|
34
|
+
|
|
35
|
+
auth_value_method :add_recovery_codes_param, 'add'
|
|
36
|
+
auth_value_method :invalid_recovery_code_message, "Invalid recovery code"
|
|
37
|
+
auth_value_method :recovery_codes_limit, 16
|
|
38
|
+
auth_value_method :recovery_codes_column, :code
|
|
39
|
+
auth_value_method :recovery_codes_id_column, :id
|
|
40
|
+
auth_value_method :recovery_codes_label, 'Recovery Code'
|
|
41
|
+
auth_value_method :recovery_codes_param, 'recovery-code'
|
|
42
|
+
auth_value_method :recovery_codes_table, :account_recovery_codes
|
|
43
|
+
|
|
44
|
+
auth_cached_method :recovery_codes
|
|
45
|
+
|
|
46
|
+
auth_value_methods(
|
|
47
|
+
:recovery_codes_primary?
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
auth_methods(
|
|
51
|
+
:add_recovery_code,
|
|
52
|
+
:can_add_recovery_codes?,
|
|
53
|
+
:new_recovery_code,
|
|
54
|
+
:recovery_code_match?,
|
|
55
|
+
:recovery_codes
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
route(:recovery_auth) do |r|
|
|
59
|
+
require_login
|
|
60
|
+
require_account_session
|
|
61
|
+
require_two_factor_setup
|
|
62
|
+
require_two_factor_not_authenticated
|
|
63
|
+
before_recovery_auth_route
|
|
64
|
+
|
|
65
|
+
r.get do
|
|
66
|
+
recovery_auth_view
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
r.post do
|
|
70
|
+
if recovery_code_match?(param(recovery_codes_param))
|
|
71
|
+
before_recovery_auth
|
|
72
|
+
two_factor_authenticate(:recovery_code)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
set_field_error(recovery_codes_param, invalid_recovery_code_message)
|
|
76
|
+
set_error_flash invalid_recovery_code_error_flash
|
|
77
|
+
recovery_auth_view
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
route(:recovery_codes) do |r|
|
|
82
|
+
require_account
|
|
83
|
+
unless recovery_codes_primary?
|
|
84
|
+
require_two_factor_setup
|
|
85
|
+
require_two_factor_authenticated
|
|
86
|
+
end
|
|
87
|
+
before_recovery_codes_route
|
|
88
|
+
|
|
89
|
+
r.get do
|
|
90
|
+
recovery_codes_view
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
r.post do
|
|
94
|
+
if two_factor_password_match?(param(password_param))
|
|
95
|
+
if can_add_recovery_codes?
|
|
96
|
+
if param_or_nil(add_recovery_codes_param)
|
|
97
|
+
transaction do
|
|
98
|
+
before_add_recovery_codes
|
|
99
|
+
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
|
|
100
|
+
after_add_recovery_codes
|
|
101
|
+
end
|
|
102
|
+
set_notice_now_flash recovery_codes_added_notice_flash
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
self.recovery_codes_button = add_recovery_codes_button
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
before_view_recovery_codes
|
|
109
|
+
add_recovery_codes_view
|
|
110
|
+
else
|
|
111
|
+
if param_or_nil(add_recovery_codes_param)
|
|
112
|
+
set_error_flash add_recovery_codes_error_flash
|
|
113
|
+
else
|
|
114
|
+
set_error_flash view_recovery_codes_error_flash
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
set_field_error(password_param, invalid_password_message)
|
|
118
|
+
recovery_codes_view
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
attr_accessor :recovery_codes_button
|
|
124
|
+
|
|
125
|
+
def two_factor_need_setup_redirect
|
|
126
|
+
super || (add_recovery_codes_redirect if recovery_codes_primary?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def two_factor_auth_required_redirect
|
|
130
|
+
super || (recovery_auth_redirect if recovery_codes_primary?)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def two_factor_auth_fallback_redirect
|
|
134
|
+
recovery_auth_redirect
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def two_factor_remove
|
|
138
|
+
super
|
|
139
|
+
recovery_codes_remove
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def two_factor_authentication_setup?
|
|
143
|
+
super || (recovery_codes_primary? && !recovery_codes.empty?)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def otp_auth_form_footer
|
|
147
|
+
"#{super if defined?(super)}<p><a href=\"#{recovery_auth_route}\">Authenticate using recovery code</a></p>"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def otp_lockout_redirect
|
|
151
|
+
recovery_auth_redirect
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def otp_lockout_error_flash
|
|
155
|
+
"#{super if defined?(super)} Can use recovery code to unlock."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def otp_add_key
|
|
159
|
+
super if defined?(super)
|
|
160
|
+
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def sms_confirm
|
|
164
|
+
super if defined?(super)
|
|
165
|
+
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def otp_remove
|
|
169
|
+
super if defined?(super)
|
|
170
|
+
unless recovery_codes_primary?
|
|
171
|
+
recovery_codes_remove
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def sms_disable
|
|
176
|
+
super if defined?(super)
|
|
177
|
+
unless recovery_codes_primary?
|
|
178
|
+
recovery_codes_remove
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def recovery_codes_remove
|
|
183
|
+
recovery_codes_ds.delete
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def recovery_code_match?(code)
|
|
187
|
+
recovery_codes.each do |s|
|
|
188
|
+
if timing_safe_eql?(code, s)
|
|
189
|
+
recovery_codes_ds.where(recovery_codes_column=>code).delete
|
|
190
|
+
if recovery_codes_primary?
|
|
191
|
+
add_recovery_code
|
|
192
|
+
end
|
|
193
|
+
return true
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def can_add_recovery_codes?
|
|
200
|
+
recovery_codes.length < recovery_codes_limit
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def add_recovery_codes(number)
|
|
204
|
+
return if number <= 0
|
|
205
|
+
transaction do
|
|
206
|
+
number.times do
|
|
207
|
+
add_recovery_code
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
remove_instance_variable(:@recovery_codes)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def add_recovery_code
|
|
214
|
+
# This should never raise uniqueness violations unless the recovery code is the same, and the odds of that
|
|
215
|
+
# are 1/256**32 assuming a good random number generator. Still, attempt to handle that case by retrying
|
|
216
|
+
# on such a uniqueness violation.
|
|
217
|
+
retry_on_uniqueness_violation do
|
|
218
|
+
recovery_codes_ds.insert(recovery_codes_id_column=>session_value, recovery_codes_column=>new_recovery_code)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def new_recovery_code
|
|
225
|
+
random_key
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def recovery_codes_primary?
|
|
229
|
+
(features & [:otp, :sms_codes]).empty?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def _recovery_codes
|
|
233
|
+
recovery_codes_ds.select_map(recovery_codes_column)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def recovery_codes_ds
|
|
237
|
+
db[recovery_codes_table].where(recovery_codes_id_column=>session_value)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|