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,77 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ ChangeLogin = Feature.define(:change_login) do
5
+ depends :login_password_requirements_base
6
+
7
+ notice_flash 'Your login has been changed'
8
+ error_flash 'There was an error changing your login'
9
+ view 'change-login', 'Change Login'
10
+ after
11
+ before
12
+ additional_form_tags
13
+ button 'Change Login'
14
+ redirect
15
+
16
+ auth_value_methods :change_login_requires_password?
17
+
18
+ auth_methods :change_login
19
+
20
+ route do |r|
21
+ require_account
22
+ before_change_login_route
23
+
24
+ r.get do
25
+ change_login_view
26
+ end
27
+
28
+ r.post do
29
+ catch_error do
30
+ if change_login_requires_password? && !password_match?(param(password_param))
31
+ throw_error(password_param, invalid_password_message)
32
+ end
33
+
34
+ login = param(login_param)
35
+ unless login_meets_requirements?(login)
36
+ throw_error(login_param, login_does_not_meet_requirements_message)
37
+ end
38
+
39
+ if require_login_confirmation? && login != param(login_confirm_param)
40
+ throw_error(login_param, logins_do_not_match_message)
41
+ end
42
+
43
+ transaction do
44
+ before_change_login
45
+ unless change_login(login)
46
+ throw_error(login_param, login_does_not_meet_requirements_message)
47
+ end
48
+
49
+ after_change_login
50
+ set_notice_flash change_login_notice_flash
51
+ redirect change_login_redirect
52
+ end
53
+ end
54
+
55
+ set_error_flash change_login_error_flash
56
+ change_login_view
57
+ end
58
+ end
59
+
60
+ def change_login_requires_password?
61
+ modifications_require_password?
62
+ end
63
+
64
+ def change_login(login)
65
+ updated = nil
66
+ if account_ds.get(login_column).downcase == login.downcase
67
+ @login_requirement_message = 'same as current login'
68
+ return false
69
+ end
70
+ raised = raises_uniqueness_violation?{updated = update_account({login_column=>login}, account_ds.exclude(login_column=>login)) == 1}
71
+ if raised
72
+ @login_requirement_message = 'already an account with this login'
73
+ end
74
+ updated && !raised
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,66 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ ChangePassword = Feature.define(:change_password) do
5
+ depends :login_password_requirements_base
6
+
7
+ notice_flash 'Your password has been changed'
8
+ error_flash 'There was an error changing your password'
9
+ view 'change-password', 'Change Password'
10
+ after
11
+ before
12
+ additional_form_tags
13
+ button 'Change Password'
14
+ redirect
15
+
16
+ auth_value_method :new_password_label, 'New Password'
17
+ auth_value_method :new_password_param, 'new-password'
18
+
19
+ auth_value_methods :change_password_requires_password?
20
+
21
+ route do |r|
22
+ require_account
23
+ before_change_password_route
24
+
25
+ r.get do
26
+ change_password_view
27
+ end
28
+
29
+ r.post do
30
+ catch_error do
31
+ if change_password_requires_password? && !password_match?(param(password_param))
32
+ throw_error(password_param, invalid_password_message)
33
+ end
34
+
35
+ password = param(new_password_param)
36
+ if require_password_confirmation? && password != param(password_confirm_param)
37
+ throw_error(new_password_param, passwords_do_not_match_message)
38
+ end
39
+
40
+ if password_match?(password)
41
+ throw_error(new_password_param, same_as_existing_password_message)
42
+ end
43
+
44
+ unless password_meets_requirements?(password)
45
+ throw_error(new_password_param, password_does_not_meet_requirements_message)
46
+ end
47
+
48
+ transaction do
49
+ before_change_password
50
+ set_password(password)
51
+ after_change_password
52
+ end
53
+ set_notice_flash change_password_notice_flash
54
+ redirect change_password_redirect
55
+ end
56
+
57
+ set_error_flash change_password_error_flash
58
+ change_password_view
59
+ end
60
+ end
61
+
62
+ def change_password_requires_password?
63
+ modifications_require_password?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,82 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ CloseAccount = Feature.define(:close_account) do
5
+ notice_flash 'Your account has been closed'
6
+ error_flash 'There was an error closing your account'
7
+ view 'close-account', 'Close Account'
8
+ additional_form_tags
9
+ button 'Close Account'
10
+ after
11
+ before
12
+ redirect
13
+
14
+ auth_value_method :account_closed_status_value, 3
15
+
16
+ auth_value_methods(
17
+ :close_account_requires_password?,
18
+ :delete_account_on_close?
19
+ )
20
+
21
+ auth_methods(
22
+ :close_account,
23
+ :delete_account
24
+ )
25
+
26
+ route do |r|
27
+ require_account
28
+ before_close_account_route
29
+
30
+ r.get do
31
+ close_account_view
32
+ end
33
+
34
+ r.post do
35
+ if !close_account_requires_password? || password_match?(param(password_param))
36
+ transaction do
37
+ before_close_account
38
+ close_account
39
+ after_close_account
40
+ if delete_account_on_close?
41
+ delete_account
42
+ end
43
+ end
44
+ clear_session
45
+
46
+ set_notice_flash close_account_notice_flash
47
+ redirect close_account_redirect
48
+ else
49
+ set_field_error(password_param, invalid_password_message)
50
+ set_error_flash close_account_error_flash
51
+ close_account_view
52
+ end
53
+ end
54
+ end
55
+
56
+ def close_account_requires_password?
57
+ modifications_require_password?
58
+ end
59
+
60
+ def close_account
61
+ unless skip_status_checks?
62
+ update_account(account_status_column=>account_closed_status_value)
63
+ end
64
+
65
+ unless account_password_hash_column
66
+ password_hash_ds.delete
67
+ end
68
+ end
69
+
70
+ def delete_account
71
+ account_ds.delete
72
+ end
73
+
74
+ def delete_account_on_close?
75
+ skip_status_checks?
76
+ end
77
+
78
+ def skip_status_checks?
79
+ false
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,51 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ ConfirmPassword = Feature.define(:confirm_password) do
5
+ notice_flash "Your password has been confirmed"
6
+ error_flash "There was an error confirming your password"
7
+ view 'confirm-password', 'Confirm Password'
8
+ additional_form_tags
9
+ button 'Confirm Password'
10
+ before
11
+ after
12
+
13
+ auth_value_methods :confirm_password_redirect
14
+
15
+ auth_methods :confirm_password
16
+
17
+ route do
18
+ require_account
19
+ before_confirm_password_route
20
+
21
+ request.get do
22
+ confirm_password_view
23
+ end
24
+
25
+ request.post do
26
+ if password_match?(param(password_param))
27
+ transaction do
28
+ before_confirm_password
29
+ confirm_password
30
+ after_confirm_password
31
+ end
32
+ set_notice_flash confirm_password_notice_flash
33
+ redirect confirm_password_redirect
34
+ else
35
+ set_field_error(password_param, invalid_password_message)
36
+ set_error_flash confirm_password_error_flash
37
+ confirm_password_view
38
+ end
39
+ end
40
+ end
41
+
42
+ def confirm_password
43
+ nil
44
+ end
45
+
46
+ def confirm_password_redirect
47
+ session.delete(:confirm_password_redirect) || default_redirect
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,128 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ CreateAccount = Feature.define(:create_account) do
5
+ depends :login_password_requirements_base
6
+
7
+ depends :login
8
+ notice_flash 'Your account has been created'
9
+ error_flash "There was an error creating your account"
10
+ view 'create-account', 'Create Account'
11
+ after
12
+ before
13
+ button 'Create Account'
14
+ additional_form_tags
15
+ redirect
16
+
17
+ auth_value_method :create_account_autologin?, true
18
+
19
+ auth_value_methods :create_account_link
20
+
21
+ auth_methods(
22
+ :save_account,
23
+ :set_new_account_password
24
+ )
25
+
26
+ auth_private_methods(
27
+ :new_account
28
+ )
29
+
30
+ route do |r|
31
+ check_already_logged_in
32
+ before_create_account_route
33
+
34
+ r.get do
35
+ create_account_view
36
+ end
37
+
38
+ r.post do
39
+ login = param(login_param)
40
+ password = param(password_param)
41
+ new_account(login)
42
+
43
+ if account_password_hash_column
44
+ set_new_account_password(param(password_param))
45
+ end
46
+
47
+ catch_error do
48
+ if require_login_confirmation? && login != param(login_confirm_param)
49
+ throw_error(login_param, logins_do_not_match_message)
50
+ end
51
+
52
+ unless login_meets_requirements?(login)
53
+ throw_error(login_param, login_does_not_meet_requirements_message)
54
+ end
55
+
56
+ if require_password_confirmation? && password != param(password_confirm_param)
57
+ throw_error(password_param, passwords_do_not_match_message)
58
+ end
59
+
60
+ unless password_meets_requirements?(password)
61
+ throw_error(password_param, password_does_not_meet_requirements_message)
62
+ end
63
+
64
+ transaction do
65
+ before_create_account
66
+ unless save_account
67
+ throw_error(login_param, login_does_not_meet_requirements_message)
68
+ end
69
+
70
+ unless account_password_hash_column
71
+ set_password(password)
72
+ end
73
+ after_create_account
74
+ if create_account_autologin?
75
+ update_session
76
+ end
77
+ set_notice_flash create_account_notice_flash
78
+ redirect create_account_redirect
79
+ end
80
+ end
81
+
82
+ set_error_flash create_account_error_flash
83
+ create_account_view
84
+ end
85
+ end
86
+
87
+ def create_account_link
88
+ "<p><a href=\"#{prefix}/#{create_account_route}\">Create a New Account</a></p>"
89
+ end
90
+
91
+ def login_form_footer
92
+ super + create_account_link
93
+ end
94
+
95
+ def set_new_account_password(password)
96
+ account[account_password_hash_column] = password_hash(password)
97
+ end
98
+
99
+ def new_account(login)
100
+ @account = _new_account(login)
101
+ end
102
+
103
+ def save_account
104
+ id = nil
105
+ raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
106
+
107
+ if raised
108
+ @login_requirement_message = 'already an account with this login'
109
+ end
110
+
111
+ if id
112
+ account[account_id_column] = id
113
+ end
114
+
115
+ id && !raised
116
+ end
117
+
118
+ private
119
+
120
+ def _new_account(login)
121
+ acc = {login_column=>login}
122
+ unless skip_status_checks?
123
+ acc[account_status_column] = account_initial_status_value
124
+ end
125
+ acc
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,82 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ DisallowPasswordReuse = Feature.define(:disallow_password_reuse) do
5
+ depends :login_password_requirements_base
6
+
7
+ auth_value_method :password_same_as_previous_password_message, "same as previous password"
8
+ auth_value_method :previous_password_account_id_column, :account_id
9
+ auth_value_method :previous_password_hash_column, :password_hash
10
+ auth_value_method :previous_password_hash_table, :account_previous_password_hashes
11
+ auth_value_method :previous_password_id_column, :id
12
+ auth_value_method :previous_passwords_to_check, 6
13
+
14
+ auth_methods(
15
+ :add_previous_password_hash,
16
+ :password_doesnt_match_previous_password?
17
+ )
18
+
19
+ def set_password(password)
20
+ hash = super
21
+ add_previous_password_hash(hash)
22
+ hash
23
+ end
24
+
25
+ def add_previous_password_hash(hash)
26
+ ds = previous_password_ds
27
+ keep_before = ds.reverse(previous_password_id_column).
28
+ limit(nil, previous_passwords_to_check).
29
+ get(previous_password_id_column)
30
+
31
+ ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
32
+ delete
33
+
34
+ # This should never raise uniqueness violations, as it uses a serial primary key
35
+ ds.insert(previous_password_account_id_column=>account_id, previous_password_hash_column=>hash)
36
+ end
37
+
38
+ def password_meets_requirements?(password)
39
+ super &&
40
+ password_doesnt_match_previous_password?(password)
41
+ end
42
+
43
+ private
44
+
45
+ def password_doesnt_match_previous_password?(password)
46
+ id = account_id
47
+ match = if use_database_authentication_functions?
48
+ salts = previous_password_ds.
49
+ select_map([previous_password_id_column, Sequel.function(function_name(:rodauth_get_previous_salt), previous_password_id_column).as(:salt)])
50
+ return true if salts.empty?
51
+
52
+ salts.any? do |hash_id, salt|
53
+ db.get(Sequel.function(function_name(:rodauth_previous_password_hash_match), hash_id, BCrypt::Engine.hash_secret(password, salt)))
54
+ end
55
+ else
56
+ # :nocov:
57
+ previous_password_ds.select_map(previous_password_hash_column).any?{|hash| BCrypt::Password.new(hash) == password}
58
+ # :nocov:
59
+ end
60
+
61
+ return true unless match
62
+ @password_requirement_message = password_same_as_previous_password_message
63
+ false
64
+ end
65
+
66
+ def after_close_account
67
+ super if defined?(super)
68
+ previous_password_ds.delete
69
+ end
70
+
71
+ def after_create_account
72
+ if account_password_hash_column
73
+ add_previous_password_hash(password_hash(request[password_param]))
74
+ end
75
+ super if defined?(super)
76
+ end
77
+
78
+ def previous_password_ds
79
+ db[previous_password_hash_table].where(previous_password_account_id_column=>account_id)
80
+ end
81
+ end
82
+ end