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,61 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Login = Feature.define(:login) do
|
|
5
|
+
notice_flash "You have been logged in"
|
|
6
|
+
error_flash "There was an error logging in"
|
|
7
|
+
view 'login', 'Login'
|
|
8
|
+
after
|
|
9
|
+
after 'login_failure'
|
|
10
|
+
before
|
|
11
|
+
before 'login_attempt'
|
|
12
|
+
additional_form_tags
|
|
13
|
+
button 'Login'
|
|
14
|
+
redirect
|
|
15
|
+
|
|
16
|
+
auth_value_method :login_form_footer, ''
|
|
17
|
+
|
|
18
|
+
route do |r|
|
|
19
|
+
check_already_logged_in
|
|
20
|
+
before_login_route
|
|
21
|
+
|
|
22
|
+
r.get do
|
|
23
|
+
login_view
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
r.post do
|
|
27
|
+
clear_session
|
|
28
|
+
|
|
29
|
+
catch_error do
|
|
30
|
+
unless account_from_login(param(login_param))
|
|
31
|
+
throw_error(login_param, no_matching_login_message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
before_login_attempt
|
|
35
|
+
|
|
36
|
+
unless open_account?
|
|
37
|
+
throw_error(login_param, unverified_account_message)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
unless password_match?(param(password_param))
|
|
41
|
+
after_login_failure
|
|
42
|
+
throw_error(password_param, invalid_password_message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
transaction do
|
|
46
|
+
before_login
|
|
47
|
+
update_session
|
|
48
|
+
after_login
|
|
49
|
+
end
|
|
50
|
+
set_notice_flash login_notice_flash
|
|
51
|
+
redirect login_redirect
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
set_error_flash login_error_flash
|
|
55
|
+
login_view
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :login_form_header
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
LoginPasswordRequirementsBase = Feature.define(:login_password_requirements_base) do
|
|
5
|
+
auth_value_method :login_confirm_param, 'login-confirm'
|
|
6
|
+
auth_value_method :login_minimum_length, 3
|
|
7
|
+
auth_value_method :logins_do_not_match_message, 'logins do not match'
|
|
8
|
+
auth_value_method :password_confirm_param, 'password-confirm'
|
|
9
|
+
auth_value_method :password_minimum_length, 6
|
|
10
|
+
auth_value_method :passwords_do_not_match_message, 'passwords do not match'
|
|
11
|
+
auth_value_method :require_email_address_logins?, true
|
|
12
|
+
auth_value_method :require_login_confirmation?, true
|
|
13
|
+
auth_value_method :require_password_confirmation?, true
|
|
14
|
+
auth_value_method :same_as_existing_password_message, "invalid password, same as current password"
|
|
15
|
+
|
|
16
|
+
auth_value_methods(
|
|
17
|
+
:login_confirm_label,
|
|
18
|
+
:login_does_not_meet_requirements_message,
|
|
19
|
+
:login_too_short_message,
|
|
20
|
+
:password_confirm_label,
|
|
21
|
+
:password_does_not_meet_requirements_message,
|
|
22
|
+
:password_hash_cost,
|
|
23
|
+
:password_too_short_message
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
auth_methods(
|
|
27
|
+
:login_meets_requirements?,
|
|
28
|
+
:password_hash,
|
|
29
|
+
:password_meets_requirements?,
|
|
30
|
+
:set_password
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def login_confirm_label
|
|
34
|
+
"Confirm #{login_label}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def password_confirm_label
|
|
38
|
+
"Confirm #{password_label}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def login_meets_requirements?(login)
|
|
42
|
+
login_meets_length_requirements?(login) && \
|
|
43
|
+
login_meets_email_requirements?(login)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def password_meets_requirements?(password)
|
|
47
|
+
password_meets_length_requirements?(password)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_password(password)
|
|
51
|
+
hash = password_hash(password)
|
|
52
|
+
if account_password_hash_column
|
|
53
|
+
update_account(account_password_hash_column=>hash)
|
|
54
|
+
elsif password_hash_ds.update(password_hash_column=>hash) == 0
|
|
55
|
+
# This shouldn't raise a uniqueness error, as the update should only fail for a new user,
|
|
56
|
+
# and an existing user shouldn't always havae a valid password hash row. If this does
|
|
57
|
+
# fail, retrying it will cause problems, it will override a concurrently running update
|
|
58
|
+
# with potentially a different password.
|
|
59
|
+
db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
|
|
60
|
+
end
|
|
61
|
+
hash
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
attr_reader :login_requirement_message
|
|
67
|
+
attr_reader :password_requirement_message
|
|
68
|
+
|
|
69
|
+
def password_does_not_meet_requirements_message
|
|
70
|
+
"invalid password, does not meet requirements#{" (#{password_requirement_message})" if password_requirement_message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def password_too_short_message
|
|
74
|
+
"minimum #{password_minimum_length} characters"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def login_does_not_meet_requirements_message
|
|
78
|
+
"invalid login#{", #{login_requirement_message}" if login_requirement_message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def login_too_short_message
|
|
82
|
+
"minimum #{login_minimum_length} characters"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def login_meets_length_requirements?(login)
|
|
86
|
+
return true if login_minimum_length <= login.length
|
|
87
|
+
@login_requirement_message = login_too_short_message
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def login_meets_email_requirements?(login)
|
|
92
|
+
return true unless require_email_address_logins?
|
|
93
|
+
if login =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
|
|
94
|
+
return true
|
|
95
|
+
end
|
|
96
|
+
@login_requirement_message = 'not a valid email address'
|
|
97
|
+
return false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def password_meets_length_requirements?(password)
|
|
101
|
+
return true if password_minimum_length <= password.length
|
|
102
|
+
@password_requirement_message = password_too_short_message
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if ENV['RACK_ENV'] == 'test'
|
|
107
|
+
def password_hash_cost
|
|
108
|
+
BCrypt::Engine::MIN_COST
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
# :nocov:
|
|
112
|
+
def password_hash_cost
|
|
113
|
+
BCrypt::Engine::DEFAULT_COST
|
|
114
|
+
end
|
|
115
|
+
# :nocov:
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def password_hash(password)
|
|
119
|
+
BCrypt::Password.create(password, :cost=>password_hash_cost)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Logout = Feature.define(:logout) do
|
|
5
|
+
notice_flash "You have been logged out"
|
|
6
|
+
view 'logout', 'Logout'
|
|
7
|
+
additional_form_tags
|
|
8
|
+
before
|
|
9
|
+
after
|
|
10
|
+
button 'Logout'
|
|
11
|
+
redirect{require_login_redirect}
|
|
12
|
+
|
|
13
|
+
auth_methods :logout
|
|
14
|
+
|
|
15
|
+
route do |r|
|
|
16
|
+
before_logout_route
|
|
17
|
+
|
|
18
|
+
r.get do
|
|
19
|
+
logout_view
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
r.post do
|
|
23
|
+
transaction do
|
|
24
|
+
before_logout
|
|
25
|
+
logout
|
|
26
|
+
after_logout
|
|
27
|
+
end
|
|
28
|
+
set_notice_flash logout_notice_flash
|
|
29
|
+
redirect logout_redirect
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def logout
|
|
34
|
+
clear_session
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'rotp'
|
|
4
|
+
require 'rqrcode'
|
|
5
|
+
|
|
6
|
+
module Rodauth
|
|
7
|
+
Otp = Feature.define(:otp) do
|
|
8
|
+
depends :two_factor_base
|
|
9
|
+
|
|
10
|
+
additional_form_tags 'otp_disable'
|
|
11
|
+
additional_form_tags 'otp_auth'
|
|
12
|
+
additional_form_tags 'otp_setup'
|
|
13
|
+
|
|
14
|
+
after 'otp_authentication_failure'
|
|
15
|
+
after 'otp_disable'
|
|
16
|
+
after 'otp_setup'
|
|
17
|
+
|
|
18
|
+
before 'otp_authentication'
|
|
19
|
+
before 'otp_setup'
|
|
20
|
+
before 'otp_disable'
|
|
21
|
+
before 'otp_authentication_route'
|
|
22
|
+
before 'otp_setup_route'
|
|
23
|
+
before 'otp_disable_route'
|
|
24
|
+
|
|
25
|
+
button 'Authenticate via 2nd Factor', 'otp_auth'
|
|
26
|
+
button 'Disable Two Factor Authentication', 'otp_disable'
|
|
27
|
+
button 'Setup Two Factor Authentication', 'otp_setup'
|
|
28
|
+
|
|
29
|
+
error_flash "Error disabling up two factor authentication", 'otp_disable'
|
|
30
|
+
error_flash "Error logging in via two factor authentication", 'otp_auth'
|
|
31
|
+
error_flash "Error setting up two factor authentication", 'otp_setup'
|
|
32
|
+
error_flash "You have already setup two factor authentication", :otp_already_setup
|
|
33
|
+
|
|
34
|
+
notice_flash "Two factor authentication has been disabled", 'otp_disable'
|
|
35
|
+
notice_flash "Two factor authentication is now setup", 'otp_setup'
|
|
36
|
+
|
|
37
|
+
redirect :otp_disable
|
|
38
|
+
redirect :otp_already_setup
|
|
39
|
+
redirect :otp_setup
|
|
40
|
+
|
|
41
|
+
view 'otp-disable', 'Disable Two Factor Authentication', 'otp_disable'
|
|
42
|
+
view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
|
|
43
|
+
view 'otp-setup', 'Setup Two Factor Authentication', 'otp_setup'
|
|
44
|
+
|
|
45
|
+
auth_value_method :otp_auth_failures_limit, 5
|
|
46
|
+
auth_value_method :otp_auth_label, 'Authentication Code'
|
|
47
|
+
auth_value_method :otp_auth_param, 'otp'
|
|
48
|
+
auth_value_method :otp_class, ROTP::TOTP
|
|
49
|
+
auth_value_method :otp_digits, nil
|
|
50
|
+
auth_value_method :otp_interval, nil
|
|
51
|
+
auth_value_method :otp_invalid_auth_code_message, "Invalid authentication code"
|
|
52
|
+
auth_value_method :otp_invalid_secret_message, "invalid secret"
|
|
53
|
+
auth_value_method :otp_keys_column, :key
|
|
54
|
+
auth_value_method :otp_keys_id_column, :id
|
|
55
|
+
auth_value_method :otp_keys_failures_column, :num_failures
|
|
56
|
+
auth_value_method :otp_keys_table, :account_otp_keys
|
|
57
|
+
auth_value_method :otp_keys_last_use_column, :last_use
|
|
58
|
+
auth_value_method :otp_setup_param, 'otp_secret'
|
|
59
|
+
|
|
60
|
+
auth_cached_method :otp_key
|
|
61
|
+
auth_cached_method :otp
|
|
62
|
+
private :otp
|
|
63
|
+
|
|
64
|
+
auth_value_methods(
|
|
65
|
+
:otp_auth_form_footer,
|
|
66
|
+
:otp_issuer,
|
|
67
|
+
:otp_lockout_error_flash,
|
|
68
|
+
:otp_lockout_redirect
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
auth_methods(
|
|
72
|
+
:otp,
|
|
73
|
+
:otp_exists?,
|
|
74
|
+
:otp_key,
|
|
75
|
+
:otp_locked_out?,
|
|
76
|
+
:otp_new_secret,
|
|
77
|
+
:otp_provisioning_name,
|
|
78
|
+
:otp_provisioning_uri,
|
|
79
|
+
:otp_qr_code,
|
|
80
|
+
:otp_record_authentication_failure,
|
|
81
|
+
:otp_remove,
|
|
82
|
+
:otp_remove_auth_failures,
|
|
83
|
+
:otp_update_last_use,
|
|
84
|
+
:otp_valid_code?,
|
|
85
|
+
:otp_valid_key?
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
auth_private_methods(
|
|
89
|
+
:otp_add_key,
|
|
90
|
+
:otp_tmp_key
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
route(:otp_auth) do |r|
|
|
94
|
+
require_login
|
|
95
|
+
require_account_session
|
|
96
|
+
require_two_factor_not_authenticated
|
|
97
|
+
require_otp_setup
|
|
98
|
+
|
|
99
|
+
if otp_locked_out?
|
|
100
|
+
set_redirect_error_flash otp_lockout_error_flash
|
|
101
|
+
redirect otp_lockout_redirect
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
before_otp_authentication_route
|
|
105
|
+
|
|
106
|
+
r.get do
|
|
107
|
+
otp_auth_view
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
r.post do
|
|
111
|
+
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
|
|
112
|
+
before_otp_authentication
|
|
113
|
+
two_factor_authenticate(:totp)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
otp_record_authentication_failure
|
|
117
|
+
after_otp_authentication_failure
|
|
118
|
+
set_field_error(otp_auth_param, otp_invalid_auth_code_message)
|
|
119
|
+
set_error_flash otp_auth_error_flash
|
|
120
|
+
otp_auth_view
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
route(:otp_setup) do |r|
|
|
125
|
+
require_account
|
|
126
|
+
|
|
127
|
+
if otp_exists?
|
|
128
|
+
set_redirect_error_flash otp_already_setup_error_flash
|
|
129
|
+
redirect otp_already_setup_redirect
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
before_otp_setup_route
|
|
133
|
+
|
|
134
|
+
r.get do
|
|
135
|
+
otp_tmp_key(otp_new_secret)
|
|
136
|
+
otp_setup_view
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
r.post do
|
|
140
|
+
secret = param(otp_setup_param)
|
|
141
|
+
catch_error do
|
|
142
|
+
unless otp_valid_key?(secret)
|
|
143
|
+
throw_error(otp_setup_param, otp_invalid_secret_message)
|
|
144
|
+
end
|
|
145
|
+
otp_tmp_key(secret)
|
|
146
|
+
|
|
147
|
+
unless two_factor_password_match?(param(password_param))
|
|
148
|
+
throw_error(password_param, invalid_password_message)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
unless otp_valid_code?(param(otp_auth_param))
|
|
152
|
+
throw_error(otp_auth_param, otp_invalid_auth_code_message)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
transaction do
|
|
156
|
+
before_otp_setup
|
|
157
|
+
otp_add_key
|
|
158
|
+
two_factor_update_session(:totp)
|
|
159
|
+
after_otp_setup
|
|
160
|
+
end
|
|
161
|
+
set_notice_flash otp_setup_notice_flash
|
|
162
|
+
redirect otp_setup_redirect
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
set_error_flash otp_setup_error_flash
|
|
166
|
+
otp_setup_view
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
route(:otp_disable) do |r|
|
|
171
|
+
require_account
|
|
172
|
+
require_otp_setup
|
|
173
|
+
before_otp_disable_route
|
|
174
|
+
|
|
175
|
+
r.get do
|
|
176
|
+
otp_disable_view
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
r.post do
|
|
180
|
+
if two_factor_password_match?(param(password_param))
|
|
181
|
+
transaction do
|
|
182
|
+
before_otp_disable
|
|
183
|
+
otp_remove
|
|
184
|
+
two_factor_remove_session
|
|
185
|
+
after_otp_disable
|
|
186
|
+
end
|
|
187
|
+
set_notice_flash otp_disable_notice_flash
|
|
188
|
+
redirect otp_disable_redirect
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
set_field_error(password_param, invalid_password_message)
|
|
192
|
+
set_error_flash otp_disable_error_flash
|
|
193
|
+
otp_disable_view
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def two_factor_authentication_setup?
|
|
198
|
+
super || otp_exists?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def two_factor_need_setup_redirect
|
|
202
|
+
"#{prefix}/#{otp_setup_route}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def two_factor_auth_required_redirect
|
|
206
|
+
"#{prefix}/#{otp_auth_route}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def two_factor_remove
|
|
210
|
+
super
|
|
211
|
+
otp_remove
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def two_factor_remove_auth_failures
|
|
215
|
+
super
|
|
216
|
+
otp_remove_auth_failures
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def otp_auth_form_footer
|
|
220
|
+
super if defined?(super)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def otp_lockout_redirect
|
|
224
|
+
return super if defined?(super)
|
|
225
|
+
default_redirect
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def otp_lockout_error_flash
|
|
229
|
+
"Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def require_otp_setup
|
|
233
|
+
unless otp_exists?
|
|
234
|
+
set_redirect_error_flash two_factor_not_setup_error_flash
|
|
235
|
+
redirect two_factor_need_setup_redirect
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def otp_exists?
|
|
240
|
+
!otp_key.nil?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def otp_valid_code?(ot_pass)
|
|
244
|
+
if otp_exists?
|
|
245
|
+
otp.verify(ot_pass.gsub(/\s+/, ''))
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def otp_remove
|
|
250
|
+
otp_key_ds.delete
|
|
251
|
+
super if defined?(super)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def otp_add_key
|
|
255
|
+
_otp_add_key(otp_key)
|
|
256
|
+
super if defined?(super)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def otp_update_last_use
|
|
260
|
+
otp_key_ds.
|
|
261
|
+
where(Sequel.date_add(otp_keys_last_use_column, :seconds=>(otp_interval||30)) < Sequel::CURRENT_TIMESTAMP).
|
|
262
|
+
update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def otp_record_authentication_failure
|
|
266
|
+
otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def otp_remove_auth_failures
|
|
270
|
+
otp_key_ds.update(otp_keys_failures_column=>0)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def otp_locked_out?
|
|
274
|
+
otp_key_ds.get(otp_keys_failures_column) >= otp_auth_failures_limit
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def otp_provisioning_uri
|
|
278
|
+
otp.provisioning_uri(otp_provisioning_name)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def otp_issuer
|
|
282
|
+
request.host
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def otp_provisioning_name
|
|
286
|
+
account[login_column]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def otp_qr_code
|
|
290
|
+
RQRCode::QRCode.new(otp_provisioning_uri).as_svg(:module_size=>8)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
def clear_cached_otp
|
|
296
|
+
remove_instance_variable(:@otp) if defined?(@otp)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def otp_tmp_key(secret)
|
|
300
|
+
_otp_tmp_key(secret)
|
|
301
|
+
clear_cached_otp
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def otp_valid_key?(secret)
|
|
305
|
+
secret =~ /\A[a-z2-7]{16}\z/
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def otp_new_secret
|
|
309
|
+
ROTP::Base32.random_base32
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def _otp_tmp_key(secret)
|
|
313
|
+
@otp_key = secret
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def _otp_add_key(secret)
|
|
317
|
+
# Uniqueness errors can't be handled here, as we can't be sure the secret provided
|
|
318
|
+
# is the same as the current secret.
|
|
319
|
+
otp_key_ds.insert(otp_keys_id_column=>session_value, otp_keys_column=>secret)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def _otp_key
|
|
323
|
+
otp_key_ds.get(otp_keys_column)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def _otp
|
|
327
|
+
otp_class.new(otp_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def otp_key_ds
|
|
331
|
+
db[otp_keys_table].where(otp_keys_id_column=>session_value)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def use_date_arithmetic?
|
|
335
|
+
true
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|