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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +146 -0
  3. data/README.rdoc +644 -220
  4. data/Rakefile +99 -11
  5. data/doc/account_expiration.rdoc +55 -0
  6. data/doc/base.rdoc +104 -0
  7. data/doc/change_login.rdoc +29 -0
  8. data/doc/change_password.rdoc +26 -0
  9. data/doc/close_account.rdoc +31 -0
  10. data/doc/confirm_password.rdoc +22 -0
  11. data/doc/create_account.rdoc +34 -0
  12. data/doc/disallow_password_reuse.rdoc +37 -0
  13. data/doc/email_base.rdoc +19 -0
  14. data/doc/jwt.rdoc +35 -0
  15. data/doc/lockout.rdoc +83 -0
  16. data/doc/login.rdoc +27 -0
  17. data/doc/login_password_requirements_base.rdoc +50 -0
  18. data/doc/logout.rdoc +21 -0
  19. data/doc/otp.rdoc +100 -0
  20. data/doc/password_complexity.rdoc +50 -0
  21. data/doc/password_expiration.rdoc +52 -0
  22. data/doc/password_grace_period.rdoc +10 -0
  23. data/doc/recovery_codes.rdoc +60 -0
  24. data/doc/release_notes/1.0.0.txt +443 -0
  25. data/doc/remember.rdoc +82 -0
  26. data/doc/reset_password.rdoc +70 -0
  27. data/doc/session_expiration.rdoc +27 -0
  28. data/doc/single_session.rdoc +43 -0
  29. data/doc/sms_codes.rdoc +119 -0
  30. data/doc/two_factor_base.rdoc +27 -0
  31. data/doc/verify_account.rdoc +70 -0
  32. data/doc/verify_account_grace_period.rdoc +15 -0
  33. data/doc/verify_change_login.rdoc +9 -0
  34. data/lib/roda/plugins/rodauth.rb +3 -262
  35. data/lib/rodauth.rb +260 -0
  36. data/lib/rodauth/features/account_expiration.rb +108 -0
  37. data/lib/rodauth/features/base.rb +479 -0
  38. data/lib/rodauth/features/change_login.rb +77 -0
  39. data/lib/rodauth/features/change_password.rb +66 -0
  40. data/lib/rodauth/features/close_account.rb +82 -0
  41. data/lib/rodauth/features/confirm_password.rb +51 -0
  42. data/lib/rodauth/features/create_account.rb +128 -0
  43. data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
  44. data/lib/rodauth/features/email_base.rb +63 -0
  45. data/lib/rodauth/features/jwt.rb +151 -0
  46. data/lib/rodauth/features/lockout.rb +262 -0
  47. data/lib/rodauth/features/login.rb +61 -0
  48. data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
  49. data/lib/rodauth/features/logout.rb +37 -0
  50. data/lib/rodauth/features/otp.rb +338 -0
  51. data/lib/rodauth/features/password_complexity.rb +89 -0
  52. data/lib/rodauth/features/password_expiration.rb +111 -0
  53. data/lib/rodauth/features/password_grace_period.rb +46 -0
  54. data/lib/rodauth/features/recovery_codes.rb +240 -0
  55. data/lib/rodauth/features/remember.rb +200 -0
  56. data/lib/rodauth/features/reset_password.rb +207 -0
  57. data/lib/rodauth/features/session_expiration.rb +55 -0
  58. data/lib/rodauth/features/single_session.rb +87 -0
  59. data/lib/rodauth/features/sms_codes.rb +498 -0
  60. data/lib/rodauth/features/two_factor_base.rb +135 -0
  61. data/lib/rodauth/features/verify_account.rb +232 -0
  62. data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
  63. data/lib/rodauth/features/verify_change_login.rb +20 -0
  64. data/lib/rodauth/migrations.rb +130 -0
  65. data/lib/rodauth/version.rb +9 -0
  66. data/spec/account_expiration_spec.rb +90 -0
  67. data/spec/all.rb +1 -0
  68. data/spec/change_login_spec.rb +149 -0
  69. data/spec/change_password_spec.rb +177 -0
  70. data/spec/close_account_spec.rb +162 -0
  71. data/spec/confirm_password_spec.rb +70 -0
  72. data/spec/create_account_spec.rb +127 -0
  73. data/spec/disallow_password_reuse_spec.rb +84 -0
  74. data/spec/lockout_spec.rb +228 -0
  75. data/spec/login_spec.rb +188 -0
  76. data/spec/migrate/001_tables.rb +103 -16
  77. data/spec/migrate/002_account_password_hash_column.rb +11 -0
  78. data/spec/migrate_password/001_tables.rb +60 -42
  79. data/spec/migrate_travis/001_tables.rb +116 -0
  80. data/spec/password_complexity_spec.rb +108 -0
  81. data/spec/password_expiration_spec.rb +243 -0
  82. data/spec/password_grace_period_spec.rb +93 -0
  83. data/spec/remember_spec.rb +424 -0
  84. data/spec/reset_password_spec.rb +185 -0
  85. data/spec/rodauth_spec.rb +57 -980
  86. data/spec/session_expiration_spec.rb +58 -0
  87. data/spec/single_session_spec.rb +107 -0
  88. data/spec/spec_helper.rb +202 -0
  89. data/spec/two_factor_spec.rb +1310 -0
  90. data/spec/verify_account_grace_period_spec.rb +135 -0
  91. data/spec/verify_account_spec.rb +142 -0
  92. data/spec/verify_change_login_spec.rb +46 -0
  93. data/spec/views/login.str +2 -2
  94. data/templates/add-recovery-codes.str +2 -0
  95. data/templates/button.str +5 -0
  96. data/templates/change-login.str +5 -18
  97. data/templates/change-password.str +6 -14
  98. data/templates/close-account.str +3 -6
  99. data/templates/confirm-password.str +4 -14
  100. data/templates/create-account.str +6 -30
  101. data/templates/login-confirm-field.str +6 -0
  102. data/templates/login-field.str +6 -0
  103. data/templates/login.str +5 -19
  104. data/templates/logout.str +2 -6
  105. data/templates/otp-auth-code-field.str +6 -0
  106. data/templates/otp-auth.str +8 -0
  107. data/templates/otp-disable.str +6 -0
  108. data/templates/otp-setup.str +21 -0
  109. data/templates/password-confirm-field.str +6 -0
  110. data/templates/password-field.str +6 -0
  111. data/templates/recovery-auth.str +12 -0
  112. data/templates/recovery-codes.str +6 -0
  113. data/templates/remember.str +8 -12
  114. data/templates/reset-password-request.str +2 -2
  115. data/templates/reset-password.str +4 -18
  116. data/templates/sms-auth.str +6 -0
  117. data/templates/sms-code-field.str +6 -0
  118. data/templates/sms-confirm.str +7 -0
  119. data/templates/sms-disable.str +7 -0
  120. data/templates/sms-request.str +5 -0
  121. data/templates/sms-setup.str +12 -0
  122. data/templates/unlock-account-request.str +3 -7
  123. data/templates/unlock-account.str +4 -7
  124. data/templates/verify-account-resend.str +2 -2
  125. data/templates/verify-account.str +2 -6
  126. metadata +191 -29
  127. data/lib/roda/plugins/rodauth/base.rb +0 -428
  128. data/lib/roda/plugins/rodauth/change_login.rb +0 -48
  129. data/lib/roda/plugins/rodauth/change_password.rb +0 -42
  130. data/lib/roda/plugins/rodauth/close_account.rb +0 -42
  131. data/lib/roda/plugins/rodauth/create_account.rb +0 -92
  132. data/lib/roda/plugins/rodauth/lockout.rb +0 -292
  133. data/lib/roda/plugins/rodauth/login.rb +0 -81
  134. data/lib/roda/plugins/rodauth/logout.rb +0 -36
  135. data/lib/roda/plugins/rodauth/remember.rb +0 -226
  136. data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
  137. data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
@@ -0,0 +1,200 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Remember = Feature.define(:remember) do
5
+ depends :confirm_password
6
+
7
+ notice_flash "Your remember setting has been updated"
8
+ error_flash "There was an error updating your remember setting"
9
+ view 'remember', 'Change Remember Setting'
10
+ additional_form_tags
11
+ button 'Change Remember Setting'
12
+ before
13
+ before 'load_memory'
14
+ after
15
+ after 'load_memory'
16
+ redirect
17
+
18
+ auth_value_method :remember_cookie_options, {}
19
+ auth_value_method :extend_remember_deadline?, false
20
+ auth_value_method :remember_period, {:days=>14}
21
+ auth_value_method :remembered_session_key, :remembered
22
+ auth_value_method :remember_deadline_interval, {:days=>14}
23
+ auth_value_method :remember_id_column, :id
24
+ auth_value_method :remember_key_column, :key
25
+ auth_value_method :remember_deadline_column, :deadline
26
+ auth_value_method :remember_table, :account_remember_keys
27
+ auth_value_method :remember_cookie_key, '_remember'
28
+ auth_value_method :remember_param, 'remember'
29
+ auth_value_method :remember_remember_param_value, 'remember'
30
+ auth_value_method :remember_forget_param_value, 'forget'
31
+ auth_value_method :remember_disable_param_value, 'disable'
32
+ auth_value_method :remember_remember_label, 'Remember Me'
33
+ auth_value_method :remember_forget_label, 'Forget Me'
34
+ auth_value_method :remember_disable_label, 'Disable Remember Me'
35
+
36
+ auth_methods(
37
+ :add_remember_key,
38
+ :clear_remembered_session_key,
39
+ :disable_remember_login,
40
+ :forget_login,
41
+ :generate_remember_key_value,
42
+ :get_remember_key,
43
+ :load_memory,
44
+ :logged_in_via_remember_key?,
45
+ :remember_key_value,
46
+ :remember_login,
47
+ :remove_remember_key
48
+ )
49
+
50
+ route do |r|
51
+ require_account
52
+ before_remember_route
53
+
54
+ r.get do
55
+ remember_view
56
+ end
57
+
58
+ r.post do
59
+ remember = param(remember_param)
60
+ if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
61
+ transaction do
62
+ before_remember
63
+ case remember
64
+ when remember_remember_param_value
65
+ remember_login
66
+ when remember_forget_param_value
67
+ forget_login
68
+ when remember_disable_param_value
69
+ disable_remember_login
70
+ end
71
+ after_remember
72
+ end
73
+
74
+ set_notice_flash remember_notice_flash
75
+ redirect remember_redirect
76
+ else
77
+ set_error_flash remember_error_flash
78
+ remember_view
79
+ end
80
+ end
81
+ end
82
+
83
+ def load_memory
84
+ return if session[session_key]
85
+ return unless cookie = request.cookies[remember_cookie_key]
86
+ id, key = cookie.split('_', 2)
87
+ return unless id && key
88
+
89
+ unless (actual = active_remember_key_ds(id).get(remember_key_column)) && timing_safe_eql?(key, actual)
90
+ forget_login
91
+ return
92
+ end
93
+
94
+ session[session_key] = id
95
+ account = account_from_session
96
+ session.delete(session_key)
97
+
98
+ unless account
99
+ remove_remember_key(id)
100
+ forget_login
101
+ return
102
+ end
103
+
104
+ before_load_memory
105
+ update_session
106
+
107
+ set_session_value(remembered_session_key, true)
108
+ if extend_remember_deadline?
109
+ active_remember_key_ds(id).update(remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period))
110
+ remember_login
111
+ end
112
+ after_load_memory
113
+ end
114
+
115
+ def remember_login
116
+ get_remember_key
117
+ opts = Hash[remember_cookie_options]
118
+ opts[:value] = "#{account_id}_#{remember_key_value}"
119
+ opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
120
+ ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
121
+ end
122
+
123
+ def forget_login
124
+ ::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, remember_cookie_options)
125
+ end
126
+
127
+ def get_remember_key
128
+ unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
129
+ generate_remember_key_value
130
+ transaction do
131
+ remove_remember_key
132
+ add_remember_key
133
+ end
134
+ end
135
+ nil
136
+ end
137
+
138
+ def disable_remember_login
139
+ remove_remember_key
140
+ end
141
+
142
+ def add_remember_key
143
+ hash = {remember_id_column=>account_id, remember_key_column=>remember_key_value}
144
+ set_deadline_value(hash, remember_deadline_column, remember_deadline_interval)
145
+
146
+ if e = raised_uniqueness_violation{remember_key_ds.insert(hash)}
147
+ # If inserting into the remember key table causes a violation, we can pull the
148
+ # existing row from the table. If there is no invalid row, we can then reraise.
149
+ raise e unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
150
+ end
151
+ end
152
+
153
+ def remove_remember_key(id=account_id)
154
+ remember_key_ds(id).delete
155
+ end
156
+
157
+ def clear_remembered_session_key
158
+ session.delete(remembered_session_key)
159
+ end
160
+
161
+ def logged_in_via_remember_key?
162
+ !!session[remembered_session_key]
163
+ end
164
+
165
+ private
166
+
167
+ def after_logout
168
+ forget_login
169
+ super if defined?(super)
170
+ end
171
+
172
+ def after_close_account
173
+ remove_remember_key
174
+ super if defined?(super)
175
+ end
176
+
177
+ def after_confirm_password
178
+ super
179
+ clear_remembered_session_key
180
+ end
181
+
182
+ attr_reader :remember_key_value
183
+
184
+ def generate_remember_key_value
185
+ @remember_key_value = random_key
186
+ end
187
+
188
+ def use_date_arithmetic?
189
+ extend_remember_deadline? || db.database_type == :mysql
190
+ end
191
+
192
+ def remember_key_ds(id=account_id)
193
+ db[remember_table].where(remember_id_column=>id)
194
+ end
195
+
196
+ def active_remember_key_ds(id=account_id)
197
+ remember_key_ds(id).where(Sequel.expr(remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,207 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ ResetPassword = Feature.define(:reset_password) do
5
+ depends :login, :email_base, :login_password_requirements_base
6
+
7
+ notice_flash "Your password has been reset"
8
+ notice_flash "An email has been sent to you with a link to reset the password for your account", 'reset_password_email_sent'
9
+ error_flash "There was an error resetting your password"
10
+ error_flash "There was an error requesting a password reset", 'reset_password_request'
11
+ view 'reset-password', 'Reset Password'
12
+ additional_form_tags
13
+ additional_form_tags 'reset_password_request'
14
+ before
15
+ before 'reset_password_request'
16
+ after
17
+ after 'reset_password_request'
18
+ button 'Reset Password'
19
+ button 'Request Password Reset', 'reset_password_request'
20
+ redirect
21
+ redirect :reset_password_email_sent
22
+
23
+ auth_value_method :reset_password_deadline_column, :deadline
24
+ auth_value_method :reset_password_deadline_interval, {:days=>1}
25
+ auth_value_method :no_matching_reset_password_key_message, "invalid password reset key"
26
+ auth_value_method :reset_password_email_subject, 'Reset Password'
27
+ auth_value_method :reset_password_key_param, 'key'
28
+ auth_value_method :reset_password_autologin?, false
29
+ auth_value_method :reset_password_table, :account_password_reset_keys
30
+ auth_value_method :reset_password_id_column, :id
31
+ auth_value_method :reset_password_key_column, :key
32
+
33
+ auth_value_methods :reset_password_email_sent_redirect
34
+
35
+ auth_methods(
36
+ :create_reset_password_key,
37
+ :create_reset_password_email,
38
+ :get_reset_password_key,
39
+ :remove_reset_password_key,
40
+ :reset_password_email_body,
41
+ :reset_password_email_link,
42
+ :reset_password_key_insert_hash,
43
+ :reset_password_key_value,
44
+ :send_reset_password_email
45
+ )
46
+ auth_private_methods(
47
+ :account_from_reset_password_key
48
+ )
49
+
50
+ route(:reset_password_request) do |r|
51
+ check_already_logged_in
52
+ before_reset_password_request_route
53
+
54
+ r.post do
55
+ if account_from_login(param(login_param)) && open_account?
56
+ generate_reset_password_key_value
57
+ transaction do
58
+ before_reset_password_request
59
+ create_reset_password_key
60
+ send_reset_password_email
61
+ after_reset_password_request
62
+ end
63
+
64
+ set_notice_flash reset_password_email_sent_notice_flash
65
+ else
66
+ set_redirect_error_flash reset_password_request_error_flash
67
+ end
68
+
69
+ redirect reset_password_email_sent_redirect
70
+ end
71
+ end
72
+
73
+ route do |r|
74
+ check_already_logged_in
75
+ before_reset_password_route
76
+
77
+ r.get do
78
+ if key = param_or_nil(reset_password_key_param)
79
+ if account_from_reset_password_key(key)
80
+ reset_password_view
81
+ else
82
+ set_redirect_error_flash no_matching_reset_password_key_message
83
+ redirect require_login_redirect
84
+ end
85
+ end
86
+ end
87
+
88
+ r.post do
89
+ key = param(reset_password_key_param)
90
+ unless account_from_reset_password_key(key)
91
+ set_redirect_error_flash reset_password_error_flash
92
+ redirect reset_password_email_sent_redirect
93
+ end
94
+
95
+ password = param(password_param)
96
+ catch_error do
97
+ if password_match?(password)
98
+ throw_error(password_param, same_as_existing_password_message)
99
+ end
100
+
101
+ if require_password_confirmation? && password != param(password_confirm_param)
102
+ throw_error(password_param, passwords_do_not_match_message)
103
+ end
104
+
105
+ unless password_meets_requirements?(password)
106
+ throw_error(password_param, password_does_not_meet_requirements_message)
107
+ end
108
+
109
+ transaction do
110
+ before_reset_password
111
+ set_password(password)
112
+ remove_reset_password_key
113
+ after_reset_password
114
+ end
115
+
116
+ if reset_password_autologin?
117
+ update_session
118
+ end
119
+
120
+ set_notice_flash reset_password_notice_flash
121
+ redirect reset_password_redirect
122
+ end
123
+
124
+ set_error_flash reset_password_error_flash
125
+ reset_password_view
126
+ end
127
+ end
128
+
129
+ def create_reset_password_key
130
+ ds = password_reset_ds
131
+ transaction do
132
+ ds.where(Sequel::CURRENT_TIMESTAMP > reset_password_deadline_column).delete
133
+ if ds.empty?
134
+ if e = raised_uniqueness_violation{ds.insert(reset_password_key_insert_hash)}
135
+ # If inserting into the reset password table causes a violation, we can pull the
136
+ # existing reset password key from the table, or reraise.
137
+ raise e unless @reset_password_key_value = get_password_reset_key(account_id)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def remove_reset_password_key
144
+ password_reset_ds.delete
145
+ end
146
+
147
+ def account_from_reset_password_key(key)
148
+ @account = _account_from_reset_password_key(key)
149
+ end
150
+
151
+ def send_reset_password_email
152
+ create_reset_password_email.deliver!
153
+ end
154
+
155
+ def reset_password_email_link
156
+ token_link(reset_password_route, reset_password_key_param, reset_password_key_value)
157
+ end
158
+
159
+ def get_password_reset_key(id)
160
+ password_reset_ds(id).get(reset_password_key_column)
161
+ end
162
+
163
+ private
164
+
165
+ attr_reader :reset_password_key_value
166
+
167
+ def after_login_failure
168
+ @login_form_header = render("reset-password-request")
169
+ super
170
+ end
171
+
172
+ def after_close_account
173
+ remove_reset_password_key
174
+ super if defined?(super)
175
+ end
176
+
177
+ def generate_reset_password_key_value
178
+ @reset_password_key_value = random_key
179
+ end
180
+
181
+ def create_reset_password_email
182
+ create_email(reset_password_email_subject, reset_password_email_body)
183
+ end
184
+
185
+ def reset_password_email_body
186
+ render('reset-password-email')
187
+ end
188
+
189
+ def use_date_arithmetic?
190
+ db.database_type == :mysql
191
+ end
192
+
193
+ def reset_password_key_insert_hash
194
+ hash = {reset_password_id_column=>account_id, reset_password_key_column=>reset_password_key_value}
195
+ set_deadline_value(hash, reset_password_deadline_column, reset_password_deadline_interval)
196
+ hash
197
+ end
198
+
199
+ def password_reset_ds(id=account_id)
200
+ db[reset_password_table].where(reset_password_id_column=>id)
201
+ end
202
+
203
+ def _account_from_reset_password_key(token)
204
+ account_from_key(token, account_open_status_value){|id| get_password_reset_key(id)}
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,55 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ SessionExpiration = Feature.define(:session_expiration) do
5
+ error_flash "This session has expired, please login again."
6
+
7
+ auth_value_method :max_session_lifetime, 86400
8
+ auth_value_method :session_created_session_key, :session_created_at
9
+ auth_value_method :session_expiration_default, true
10
+ auth_value_method :session_inactivity_timeout, 1800
11
+ auth_value_method :session_last_activity_session_key, :last_session_activity_at
12
+
13
+ auth_value_methods :session_expiration_redirect
14
+
15
+ def check_session_expiration
16
+ return unless logged_in?
17
+
18
+ unless session.has_key?(session_last_activity_session_key) && session.has_key?(session_created_session_key)
19
+ if session_expiration_default
20
+ expire_session
21
+ end
22
+
23
+ return
24
+ end
25
+
26
+ time = Time.now.to_i
27
+
28
+ if session[session_last_activity_session_key] + session_inactivity_timeout < time
29
+ expire_session
30
+ end
31
+ set_session_value(session_last_activity_session_key, time)
32
+
33
+ if session[session_created_session_key] + max_session_lifetime < time
34
+ expire_session
35
+ end
36
+ end
37
+
38
+ def expire_session
39
+ clear_session
40
+ set_redirect_error_flash session_expiration_error_flash
41
+ redirect session_expiration_redirect
42
+ end
43
+
44
+ def session_expiration_redirect
45
+ require_login_redirect
46
+ end
47
+
48
+ private
49
+
50
+ def update_session
51
+ super
52
+ session[session_last_activity_session_key] = session[session_created_session_key] = Time.now.to_i
53
+ end
54
+ end
55
+ end