rodauth 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4562cb5ac9001ed624c4a17b266e248bda416bfa
4
- data.tar.gz: 83b071943791302efef9980bef8d04629ea26d8a
3
+ metadata.gz: 9f7b621217958c77c148ee85c271e7e39548b02f
4
+ data.tar.gz: f4fa75ad99266693763134533d5f7f0dc90de8ba
5
5
  SHA512:
6
- metadata.gz: 46012c509f3e3fe2df2c27c56d7f0ecb6f950c6f7227faa056f8578f940908d20cd2d7757c5a6c63a387f7da066216eba6ab7e1553fd70d1a92882a8d2a2ced4
7
- data.tar.gz: 88d313c1a7739bd54ac588e25046e90eed284502ce119c69862e20cc49028bcee2999e8c0ce93874bec4fefcdb2d92bc1de4183359b395cd9e7a8df8af272bde
6
+ metadata.gz: 9ce46e8a0b1d1b5f5a2994d4ab3d70503fd5da9f968fa0dc1cbd938cd9ce2d327c8dd427fd09a4c8242b2dbdb8be4f428c40277008352021c91b97ad438689fb
7
+ data.tar.gz: 6535d95d9d3573f5c7190b990d448f983757d674a4df73662de2567ca197fcc63867773a86a4f3617edc26c4a2cd54f3bb741ba3153f503a8f69094c8a89a3d3
data/CHANGELOG CHANGED
@@ -1,3 +1,149 @@
1
+ === HEAD
2
+
3
+ * Remove invalid remember cookies to prevent unnecessary future database checks (jeremyevans)
4
+
5
+ * Extend remember deadline in cookie in addition to database (jeremyevans)
6
+
7
+ * Make tokens work with string account ids (jeremyevans)
8
+
9
+ * Add verify_change_login feature for requiring account reverification on login changes (jeremyevans)
10
+
11
+ * Set correct cookie expiration in the remember feature (jeremyevans)
12
+
13
+ * Split confirm_password feature from remember feature (jeremyevans)
14
+
15
+ * Add verify_account_grace_period feature, for allowing logins into unverified accounts for a certain period after creation (jeremyevans)
16
+
17
+ * Move login/password requirements settings to login password requirements base feature (jeremyevans)
18
+
19
+ * Add session_expiration feature, expiring sessions based on inactivity and max lifetime checks (jeremyevans)
20
+
21
+ * Add password_grace_period feature, for not requiring password entry if password was recently entered (jeremyevans)
22
+
23
+ * Make create/verify account autologin true by default (jeremyevans)
24
+
25
+ * Optimize routing using a hash table, disallow per-request routes (jeremyevans)
26
+
27
+ * Add ability to turn off login/password confirmations (jeremyevans)
28
+
29
+ * Don't allow changing login to the same as the current login (jeremyevans)
30
+
31
+ * Only allow requesting account unlocks if the account is current locked out (jeremyevans)
32
+
33
+ * Use separate routes for unlock account/reset password/verify account requests (jeremyevans)
34
+
35
+ * Use separate routes for confirming passwords and changing remember settings (jeremyevans)
36
+
37
+ * Add JWT feature for JSON API support using JWT tokens (jeremyevans)
38
+
39
+ * Add account_select configuration option for setting which columns to select from accounts_table (jeremyevans)
40
+
41
+ * Execute get_block and post_block in the Rodauth::Auth instance scope (jeremyevans)
42
+
43
+ * Store field errors in the rodauth object instead of instance variables in the Roda scope (jeremyevans)
44
+
45
+ * Add rodauth.redirect to abstract redirection code (jeremyevans)
46
+
47
+ * Only use flash notices for successful requests, other requests that redirect now use an error flash (jeremyevans)
48
+
49
+ * The before_* configuration methods now run directly before making the related database changes (jeremyevans)
50
+
51
+ * Before hooks run before routes now use before_*_route instead of before_* configuration methods (jeremyevans)
52
+
53
+ * Add token_separator configuration method to replace the default of _ (jeremyevans)
54
+
55
+ * Rename account_id_value to account_id (jeremyevans)
56
+
57
+ * Rename account_id to account_id_column and account_session_id to account_session_column (jeremyevans)
58
+
59
+ * Make skip_status_checks? default to true unless loading verify_account or close_account features (jeremyevans)
60
+
61
+ * Replace account_model with accounts_table and db, removing use of Sequel models (jeremyevans)
62
+
63
+ * Extract shared email-related code into email_base feature (jeremyevans)
64
+
65
+ * Add auth_class_eval to configuration block for adding custom methods (jeremyevans)
66
+
67
+ * Add configuration_eval to feature definitions for adding custom configuration methods (jeremyevans)
68
+
69
+ * Allow close_account feature to optionally delete accounts (jeremyevans)
70
+
71
+ * Make close_account feature work when skipping status checks or when using account_password_hash_column (jeremyevans)
72
+
73
+ * Add sms_codes feature, for codes received via SMS that can be used if TOTP authentication is not available (jeremyevans)
74
+
75
+ * Attempt to handle unique constraint violations raised in race conditions where possible (jeremyevans)
76
+
77
+ * Add _before and _after internal methods, make ununderscored methods only for users (jeremyevans)
78
+
79
+ * Add single_session feature, for only allowing a single active session per account (jeremyevans)
80
+
81
+ * Add account_expiration feature, for disallowing access to accounts after an amount of time since last login/activity (jeremyevans)
82
+
83
+ * Check account status in rodauth.load_memory in remember plugin (jeremyevans)
84
+
85
+ * Use csrf plugin automatically, depend on Roda >=2.6.0 (jeremyevans)
86
+
87
+ * Make bcrypt and mail development dependencies instead of runtime dependencies in the gem (jeremyevans)
88
+
89
+ * Add password_expiration feature, requiring users to change their password after a given amount of time (jeremyevans)
90
+
91
+ * Add disallow_password_reuse feature, checking that a new password doesn't match previous passwords (jeremyevans)
92
+
93
+ * Add password_complexity feature, allowing more sophisticated password complexity checks (jeremyevans)
94
+
95
+ * Add rodauth.remember_param and .remember_confirm_param for overriding parameter names (jeremyevans)
96
+
97
+ * Check that new password is not the same as existing password in change password and reset password features (jeremyevans)
98
+
99
+ * Add rodauth.login_meets_requirements? for checking if a login is valid, by default a valid email address (jeremyevans)
100
+
101
+ * Allow unlock account to optionally require the user's current password (jeremyevans)
102
+
103
+ * Add support for running on Microsoft SQL Server with database functions for authentication (jeremyevans)
104
+
105
+ * Make change password, change login, and close account require the user's current password by default (jeremyevans)
106
+
107
+ * Add rodauth.csrf_tag to make it easy to replace the CSRF tag implementation (jeremyevans)
108
+
109
+ * Switch unlock_account_autologin? to be true by default (jeremyevans)
110
+
111
+ * Add rodauth.authenticated? and .require_authentication (jeremyevans)
112
+
113
+ * Add recovery_codes feature, for single use codes that can be used if TOTP authentication is not available (jeremyevans)
114
+
115
+ * Add otp feature, for 2 factor authentication via TOTP (jeremyevans)
116
+
117
+ * Add support for running on MySQL with database functions for authentication (jeremyevans)
118
+
119
+ * Add *_interval and set_deadline_values? methods for setting deadline intervals on a per-request basis (jeremyevans)
120
+
121
+ * Add remember_deadline_column method for overriding the column used for storing the deadline (jeremyevans)
122
+
123
+ * Add rodauth/migrations file for DRYing up the database function creation (jeremyevans)
124
+
125
+ * Add Rodauth.version for getting the version (jeremyevans)
126
+
127
+ * External features should now be requirable via rodauth/features/feature_name instead of roda/plugins/rodauth/feature_name (jeremyevans)
128
+
129
+ * Make Rodauth top level module instead of under Roda::RodaPlugins (jeremyevans)
130
+
131
+ * Require mail at configure time instead of run time if using a feature that sends email, use require_mail? false to disable (jeremyevans)
132
+
133
+ * Require bcrypt at configure time instead of run time, use require_bcrypt? false to disable (jeremyevans)
134
+
135
+ * Always require securerandom (jeremyevans)
136
+
137
+ * Make remember, password reset, and lockout features work on non-PostgreSQL databases (jeremyevans)
138
+
139
+ * Support authentication without database functions when password hashes are stored in separate table (jeremyevans)
140
+
141
+ * Remove overriding of route/get/post blocks (jeremyevans)
142
+
143
+ * Make lockout feature work on databases not supporting UPDATE RETURNING (jeremyevans)
144
+
145
+ * Add timing safe comparison of tokens (jeremyevans)
146
+
1
147
  === 0.10.0 (2016-02-17)
2
148
 
3
149
  * Retrieve salt from database and compute hash client side, instead of computing hash on server (jeremyevans)
@@ -1,6 +1,11 @@
1
1
  = Rodauth
2
2
 
3
- Rodauth is an authentication framework using Roda, Sequel, and PostgreSQL.
3
+ Rodauth is an authentication and account management framework for
4
+ rack applications. It's built using Roda and Sequel, but it can
5
+ be used with other web frameworks, database libraries, and databases.
6
+ When used with PostgreSQL, MySQL, and Microsoft SQL Server in the
7
+ default configuration, it offers additional security for password
8
+ hashes by protecting access via database functions.
4
9
 
5
10
  == Design Goals
6
11
 
@@ -18,57 +23,100 @@ Rodauth is an authentication framework using Roda, Sequel, and PostgreSQL.
18
23
  * Create Account
19
24
  * Close Account
20
25
  * Verify Account
26
+ * Confirm Password
21
27
  * Remember (Autologin via token)
22
28
  * Lockout (Bruteforce protection)
29
+ * OTP (2 factor authentication via TOTP)
30
+ * Recovery Codes (2 factor authentication via backup codes)
31
+ * SMS Codes (2 factor authentication via SMS)
32
+ * Verify Change Login (Reverify accounts after login changes)
33
+ * Verify Account Grace Period (Don't require verification before login)
34
+ * Password Grace Period (Don't require password entry if recently entered)
35
+ * Password Complexity (More sophisticated checks)
36
+ * Disallow Password Reuse
37
+ * Password Expiration
38
+ * Account Expiration
39
+ * Session Expiration
40
+ * Single Session (Only one active session per account)
41
+ * JWT (JSON API support for all other features)
23
42
 
24
43
  == Resources
25
44
 
26
- RDoc :: http://rodauth.jeremyevans.net
45
+ Website :: http://rodauth.jeremyevans.net
27
46
  Demo Site :: http://rodauth-demo.jeremyevans.net
28
47
  Source :: http://github.com/jeremyevans/rodauth
29
48
  Bugs :: http://github.com/jeremyevans/rodauth/issues
49
+ Google Group :: https://groups.google.com/forum/#!forum/rodauth
50
+ IRC :: irc://chat.freenode.net/#rodauth
51
+
52
+ == Dependencies
53
+
54
+ There are some dependencies that Rodauth uses by default, but are
55
+ development dependencies instead of runtime dependencies in the
56
+ gem as it is possible to run without them:
57
+
58
+ tilt, rack_csrf :: Used by all features unless in JSON API only mode.
59
+ bcrypt :: Used by default for password matching, can be skipped
60
+ if password_match? is overridden for custom authentication.
61
+ mail :: Used by default for mailing in the reset password, verify
62
+ account, and lockout features.
63
+ rotp, rqrcode :: Used by the otp feature
64
+ jwt :: Used by the jwt feature
30
65
 
31
66
  == Security
32
67
 
33
- === Passwords
68
+ === Password Hash Access Via Database Functions
69
+
70
+ By default on PostgreSQL, MySQL, and Microsoft SQL Server, Rodauth
71
+ uses database functions to access password hashes, with the user
72
+ running the application unable to get direct access to password
73
+ hashes. This reduces the risk of an attacker being able to access
74
+ password hashes and use them to attack other sites.
75
+
76
+ The rest of this section describes this feature in more detail, but
77
+ note that Rodauth does not require this feature be used and works
78
+ correctly without it. There may be cases where you cannot use
79
+ this feature, such as when using a different database or when you
80
+ do not have full control over the database you are using.
34
81
 
35
82
  Passwords are hashed using bcrypt, and the password hashes are
36
83
  kept in a separate table from the accounts table, with a foreign key
37
- referencing the accounts table. Two PostgreSQL functions are added,
84
+ referencing the accounts table. Two database functions are added,
38
85
  one to retrieve the salt for a password, and the other to check
39
86
  if a given password hash matches the password hash for the user.
40
87
 
41
- A separate database account owns the table containing the password
42
- hashes, which the application database account cannot access.
43
- The application database account has the ability to execute the
44
- functions to get the salt and check the password hash, but not the
45
- ability to access the password hashes directly, making it much more
46
- difficult for an attacker to access the password hashes even if they
47
- are able to exploit an SQL injection or remote code execution
48
- vulnerability in the application. Even if an attacker was able to
49
- exploit a vulnerability in the application, the only additional
50
- information they would have is the salt for the password, which
51
- is much less sensitive than the entire password hash.
52
-
53
- While the application database account is not be able to read
54
- password hashes, it is still be able to insert password hashes,
55
- update passwords hashes, and delete password hashes, so the
56
- additional security is not that painful.
88
+ Two database accounts are used. The first is the account that the
89
+ application uses, which is referred to as the +app+ account. The +app+
90
+ account does not have access to read the password hashes. The other
91
+ account handles password hashes and is referred to as the +ph+
92
+ account. The +ph+ account sets up the database functions that can
93
+ retrieve the salt for a given account's password, and check if a
94
+ password hash matches for for a given account. The +ph+ account
95
+ sets these functions up so that the +app+ account can execute the
96
+ functions using the +ph+ account's permissions. This allows the
97
+ +app+ account to check passwords without having access to read
98
+ password hashes.
99
+
100
+ While the +app+ account is not be able to read password hashes, it
101
+ is still be able to insert password hashes, update passwords hashes,
102
+ and delete password hashes, so the additional security is not that
103
+ painful.
104
+
105
+ By disallowing the +app+ account access to the password hashes,
106
+ it is much more difficult for an attacker to access the password
107
+ hashes, even if they are able to exploit an SQL injection or remote
108
+ code execution vulnerability in the application.
57
109
 
58
110
  The reason for extra security in regards to password hashes stems from
59
- the fact that people tend to reuse passwords, so a compromise of one
60
- database containing password hashes can result in account access on
61
- other sites, making password hash storage of critical importance even
62
- if the other data stored is not that important.
111
+ the fact that people tend to choose poor passwords and reuse passwords,
112
+ so a compromise of one database containing password hashes can result
113
+ in account access on other sites, making password hash storage of
114
+ critical importance even if the other data stored is not that important.
63
115
 
64
- If you are storing other important information in your database, you
116
+ If you are storing other sensitive information in your database, you
65
117
  should consider using a similar approach in other areas (or all areas)
66
118
  of your application.
67
119
 
68
- Rodauth can still be used if you are using a more conventional approach
69
- of storing the password hash in a column in the same table, with
70
- a single configuration setting.
71
-
72
120
  === Tokens
73
121
 
74
122
  Account verification, password resets, remember, and lockout tokens
@@ -79,18 +127,17 @@ for a single account, instead of being able to bruteforce tokens for
79
127
  all accounts at once (which would be possible if the token was just a
80
128
  random string).
81
129
 
82
- There is a maximum of 1 token per account for each of these features
83
- at a time. This prevents attackers from creating an arbitrary number
84
- of requests in order to make bruteforcing easier.
130
+ Additionally, all comparisons of tokens use a timing-safe comparison
131
+ function to reduce the risk of timing attacks.
85
132
 
86
- == Database Setup
133
+ == PostgreSQL Database Setup
87
134
 
88
- In order to get full advantages of Rodauth's security design, multiple
89
- database accounts are involved:
135
+ In order to get full advantages of Rodauth's security design on PostgreSQL,
136
+ multiple database accounts are involved:
90
137
 
91
- 1) database superuser account (usually postgres)
92
- 2) application database account
93
- 3) secondary database account
138
+ 1. database superuser account (usually postgres)
139
+ 2. +app+ account (same name as application)
140
+ 3. +ph+ account (application name with +_password+ appended)
94
141
 
95
142
  The database superuser account is used to load extensions related to the
96
143
  database. The application should never be run using the database
@@ -109,42 +156,81 @@ citext extension if you want to support case insensitive logins.
109
156
 
110
157
  Example:
111
158
 
112
- echo "CREATE EXTENSION citext" | psql -U postgres $database_name
159
+ psql -U postgres -c "CREATE EXTENSION citext" ${DATABASE_NAME}
113
160
 
114
161
  Note that on Heroku, this extension can be loaded using a standard database
115
- account.
162
+ account. If you don't want to support case sensitive logins, you don't
163
+ need to use the PostgreSQL citext extension. Just remember to modify the
164
+ migration below to use +String+ instead of +citext+ for the email.
116
165
 
117
166
  === Create database accounts
118
167
 
119
168
  If you are currently running your application using the database superuser
120
- account, the first thing you need to do is to create a database account for
121
- the application. It's often best to name this account the same as the
169
+ account, the first thing you need to do is to create the +app+ database
170
+ account. It's often best to name this account the same as the
122
171
  database name.
123
172
 
124
- You should also create a second database account which will own the password
125
- hash table.
173
+ You should also create the +ph+ database account which will handle access
174
+ to the password hashes.
126
175
 
127
- Example:
176
+ Example for PostgreSQL:
128
177
 
129
- createuser -U postgres $database_name
130
- createuser -U postgres $database_name_password_hashes
178
+ createuser -U postgres ${DATABASE_NAME}
179
+ createuser -U postgres ${DATABASE_NAME}_password
131
180
 
132
181
  Note that if the database superuser account owns all of the items in the
133
182
  database, you'll need to change the ownership to the database account you
134
183
  just created. See https://gist.github.com/jeremyevans/8483320
135
184
  for a way to do that.
136
185
 
137
- === Create tables
186
+ == MySQL Database Setup
187
+
188
+ MySQL does not have the concept of object owners, and MySQL's GRANT/REVOKE
189
+ support is much more limited than PostgreSQL's. When using MySQL, it is
190
+ recommended to GRANT the +ph+ account ALL privileges on the database,
191
+ including the ability to GRANT permissions to the +app+ account:
192
+
193
+ CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';
194
+ CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';
195
+ GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;
196
+
197
+ You should run all migrations as the +ph+ account, and GRANT specific access
198
+ to the +app+ account as needed.
199
+
200
+ Adding the database functions on MySQL may require setting the
201
+ <tt>log_bin_trust_function_creators=1</tt> setting in the MySQL configuration.
202
+
203
+ == Microsoft SQL Server Database Setup
204
+
205
+ Microsoft SQL Server has a concept of database owners, but similar to MySQL
206
+ usage it's recommended to use the +ph+ account as the superuser for the
207
+ database, and have it GRANT permissions to the +app+ account:
208
+
209
+ CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';
210
+ CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';
211
+ CREATE DATABASE rodauth_test;
212
+ USE rodauth_test;
213
+ CREATE USER rodauth_test FOR LOGIN rodauth_test;
214
+ GRANT CONNECT, EXECUTE TO rodauth_test;
215
+ EXECUTE sp_changedbowner 'rodauth_test_password';
216
+
217
+ You should run all migrations as the +ph+ account, and GRANT specific access
218
+ to the +app+ account as needed.
219
+
220
+ == Creating tables
138
221
 
139
222
  Because two different database accounts are used, two different migrations
140
223
  are required, one for each database account. Here are example migrations.
141
224
  You can modify them to add support for additional columns, or remove tables
142
225
  or columns related to features that you don't need.
143
226
 
144
- First migration, run using the application database account:
227
+ First migration. On PostgreSQL, this should be run with the +app+ account,
228
+ on MySQL and Microsoft SQL Server this should be run with the +ph+ account.
145
229
 
146
230
  Sequel.migration do
147
231
  up do
232
+ extension :date_arithmetic
233
+
148
234
  # Used by the account verification and close account features
149
235
  create_table(:account_statuses) do
150
236
  Integer :id, :primary_key=>true
@@ -152,35 +238,47 @@ First migration, run using the application database account:
152
238
  end
153
239
  from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
154
240
 
155
- # Used by the create account, account verification,
156
- # and close account features.
241
+ db = self
157
242
  create_table(:accounts) do
158
243
  primary_key :id, :type=>Bignum
159
244
  foreign_key :status_id, :account_statuses, :null=>false, :default=>1
160
- citext :email, :null=>false
245
+ if db.database_type == :postgres
246
+ citext :email, :null=>false
247
+ constraint :valid_email, :email=>/^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
248
+ index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
249
+ else
250
+ String :email, :null=>false
251
+ index :email, :unique=>true
252
+ end
253
+ end
161
254
 
162
- constraint :valid_email, :email=>/^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
163
- index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
255
+ deadline_opts = proc do |days|
256
+ if database_type == :mysql
257
+ {:null=>false}
258
+ else
259
+ {:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
260
+ end
164
261
  end
165
262
 
166
263
  # Used by the password reset feature
167
264
  create_table(:account_password_reset_keys) do
168
265
  foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
169
266
  String :key, :null=>false
170
- DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '1 day'")
267
+ DateTime :deadline, deadline_opts[1]
171
268
  end
172
269
 
173
270
  # Used by the account verification feature
174
271
  create_table(:account_verification_keys) do
175
272
  foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
176
273
  String :key, :null=>false
274
+ DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
177
275
  end
178
276
 
179
277
  # Used by the remember me feature
180
278
  create_table(:account_remember_keys) do
181
279
  foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
182
280
  String :key, :null=>false
183
- DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '2 weeks'")
281
+ DateTime :deadline, deadline_opts[14]
184
282
  end
185
283
 
186
284
  # Used by the lockout feature
@@ -191,81 +289,183 @@ First migration, run using the application database account:
191
289
  create_table(:account_lockouts) do
192
290
  foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
193
291
  String :key, :null=>false
194
- DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '1 day'")
292
+ DateTime :deadline, deadline_opts[1]
293
+ end
294
+
295
+ # Used by the password expiration feature
296
+ create_table(:account_password_change_times) do
297
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
298
+ DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
299
+ end
300
+
301
+ # Used by the account expiration feature
302
+ create_table(:account_activity_times) do
303
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
304
+ DateTime :last_activity_at, :null=>false
305
+ DateTime :last_login_at, :null=>false
306
+ DateTime :expired_at
307
+ end
308
+
309
+ # Used by the single session feature
310
+ create_table(:account_session_keys) do
311
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
312
+ String :key, :null=>false
313
+ end
314
+
315
+ # Used by the otp feature
316
+ create_table(:account_otp_keys) do
317
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
318
+ String :key, :null=>false
319
+ Integer :num_failures, :null=>false, :default=>0
320
+ Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
195
321
  end
196
322
 
197
- # Grant password user access to reference accounts
198
- pw_user = get{Sequel.lit('current_user')} + '_password'
199
- run "GRANT REFERENCES ON accounts TO #{pw_user}"
323
+ # Used by the recovery codes feature
324
+ create_table(:account_recovery_codes) do
325
+ foreign_key :id, :accounts, :type=>Bignum
326
+ String :code
327
+ primary_key [:id, :code]
328
+ end
329
+
330
+ # Used by the sms codes feature
331
+ create_table(:account_sms_codes) do
332
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
333
+ String :phone_number, :null=>false
334
+ Integer :num_failures
335
+ String :code
336
+ DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
337
+ end
338
+
339
+ case database_type
340
+ when :postgres
341
+ user = get{Sequel.lit('current_user')} + '_password'
342
+ run "GRANT REFERENCES ON accounts TO #{user}"
343
+ when :mysql, :mssql
344
+ user = if database_type == :mysql
345
+ get{Sequel.lit('current_user')}.sub(/_password@/, '@')
346
+ else
347
+ get{DB_NAME{}}
348
+ end
349
+ run "GRANT ALL ON account_statuses TO #{user}"
350
+ run "GRANT ALL ON accounts TO #{user}"
351
+ run "GRANT ALL ON account_password_reset_keys TO #{user}"
352
+ run "GRANT ALL ON account_verification_keys TO #{user}"
353
+ run "GRANT ALL ON account_remember_keys TO #{user}"
354
+ run "GRANT ALL ON account_login_failures TO #{user}"
355
+ run "GRANT ALL ON account_lockouts TO #{user}"
356
+ run "GRANT ALL ON account_password_change_times TO #{user}"
357
+ run "GRANT ALL ON account_activity_times TO #{user}"
358
+ run "GRANT ALL ON account_session_keys TO #{user}"
359
+ run "GRANT ALL ON account_otp_keys TO #{user}"
360
+ run "GRANT ALL ON account_recovery_codes TO #{user}"
361
+ run "GRANT ALL ON account_sms_codes TO #{user}"
362
+ end
200
363
  end
201
364
 
202
365
  down do
203
- drop_table(:account_lockouts, :account_login_failures, :account_remember_keys,
204
- :account_verification_keys, :account_password_reset_keys, :accounts, :account_statuses)
366
+ drop_table(:account_sms_codes,
367
+ :account_recovery_codes,
368
+ :account_otp_keys,
369
+ :account_session_keys,
370
+ :account_activity_times,
371
+ :account_password_change_times,
372
+ :account_lockouts,
373
+ :account_login_failures,
374
+ :account_remember_keys,
375
+ :account_verification_keys,
376
+ :account_password_reset_keys,
377
+ :accounts,
378
+ :account_statuses)
205
379
  end
206
380
  end
207
381
 
208
- Second migration, run using the secondary database account:
382
+ Second migration, run using the +ph+ account:
383
+
384
+ require 'rodauth/migrations'
209
385
 
210
386
  Sequel.migration do
211
387
  up do
212
- # Used by the login and change password features
213
388
  create_table(:account_password_hashes) do
214
389
  foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
215
390
  String :password_hash, :null=>false
216
391
  end
392
+ Rodauth.create_database_authentication_functions(self)
393
+ case database_type
394
+ when :postgres
395
+ user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
396
+ run "REVOKE ALL ON account_password_hashes FROM public"
397
+ run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
398
+ run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
399
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
400
+ run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
401
+ run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
402
+ run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
403
+ when :mysql
404
+ user = get{Sequel.lit('current_user')}.sub(/_password@/, '@')
405
+ db_name = get{database{}}
406
+ run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
407
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
408
+ run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
409
+ when :mssql
410
+ user = get{DB_NAME{}}
411
+ run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
412
+ run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
413
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
414
+ run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
415
+ end
217
416
 
218
- # Function that returns salt for current password.
219
- run <<END
220
- CREATE OR REPLACE FUNCTION rodauth_get_salt(account_id int8) RETURNS text AS $$
221
- DECLARE salt text;
222
- BEGIN
223
- SELECT substr(password_hash, 0, 30) INTO salt
224
- FROM account_password_hashes
225
- WHERE account_id = id;
226
- RETURN salt;
227
- END;
228
- $$ LANGUAGE plpgsql
229
- SECURITY DEFINER
230
- SET search_path = public, pg_temp;
231
- END
232
-
233
- # Function that checks if password hash is valid for given user.
234
- run <<END
235
- CREATE OR REPLACE FUNCTION rodauth_valid_password_hash(account_id int8, hash text) RETURNS boolean AS $$
236
- DECLARE valid boolean;
237
- BEGIN
238
- SELECT password_hash = hash INTO valid
239
- FROM account_password_hashes
240
- WHERE account_id = id;
241
- RETURN valid;
242
- END;
243
- $$ LANGUAGE plpgsql
244
- SECURITY DEFINER
245
- SET search_path = public, pg_temp;
246
- END
247
-
248
- # Restrict access to the password hash table
249
- app_user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
250
- run "REVOKE ALL ON account_password_hashes FROM public"
251
- run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
252
- run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
253
- run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{app_user}"
254
- run "GRANT SELECT(id) ON account_password_hashes TO #{app_user}"
255
- run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{app_user}"
256
- run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{app_user}"
417
+ # Used by the disallow_password_reuse feature
418
+ create_table(:account_previous_password_hashes) do
419
+ primary_key :id, :type=>Bignum
420
+ foreign_key :account_id, :accounts, :type=>Bignum
421
+ String :password_hash, :null=>false
422
+ end
423
+ Rodauth.create_database_previous_password_check_functions(self)
424
+
425
+ case database_type
426
+ when :postgres
427
+ user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
428
+ run "REVOKE ALL ON account_previous_password_hashes FROM public"
429
+ run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
430
+ run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
431
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
432
+ run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
433
+ run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
434
+ run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
435
+ run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
436
+ when :mysql
437
+ user = get{Sequel.lit('current_user')}.sub(/_password@/, '@')
438
+ db_name = get{database{}}
439
+ run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
440
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
441
+ run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
442
+ when :mssql
443
+ user = get{DB_NAME{}}
444
+ run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
445
+ run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
446
+ run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
447
+ run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
448
+ end
257
449
  end
258
450
 
259
451
  down do
260
- run "DROP FUNCTION rodauth_get_salt(int8)"
261
- run "DROP FUNCTION rodauth_valid_password_hash(int8, text)"
262
- drop_table(:account_password_hashes)
452
+ Rodauth.drop_database_previous_password_check_functions(self)
453
+ Rodauth.drop_database_authentication_functions(self)
454
+ drop_table(:account_previous_password_hashes, :account_password_hashes)
263
455
  end
264
456
  end
265
457
 
266
- If you are using a non-PostgreSQL database or cannot use multiple user
267
- accounts, just combine the two migrations into a single migration and
268
- exclude the GRANT/REVOKE statements.
458
+ To support multiple separate migration users, you can run the migration
459
+ for the password user using Sequel's migration API:
460
+
461
+ Sequel.extension :migration
462
+ Sequel.postgres('DATABASE_NAME', :user=>'PASSWORD_USER_NAME') do |db|
463
+ Sequel::Migrator.run(db, 'path/to/password_user/migrations', :table=>'schema_info_password')
464
+ end
465
+
466
+ If the database is not PostgreSQL, MySQL, or Microsoft SQL Server, or you
467
+ cannot use multiple user accounts, just combine the two migrations into a
468
+ single migration.
269
469
 
270
470
  One thing to notice in the above migrations is that Rodauth uses additional
271
471
  tables for additional features, instead of additional columns in a single
@@ -282,7 +482,7 @@ are loaded:
282
482
  end
283
483
 
284
484
  The block passed to the plugin call uses the Rodauth configuration DSL.
285
- The one configuration method that should always be used is enable,
485
+ The one configuration method that should always be used is +enable+,
286
486
  which chooses which features you would like to load:
287
487
 
288
488
  plugin :rodauth do
@@ -290,17 +490,18 @@ which chooses which features you would like to load:
290
490
  end
291
491
 
292
492
  Once features are loaded, you can use any of the configuration methods
293
- supported by the features. There are three types of configuration
493
+ supported by the features. There are two types of configuration
294
494
  methods. The first type are called auth methods, and they take a
295
- block, and overrides the default method that Rodauth uses. Inside the
296
- block, you can call super if you want to get the default behavior. For
297
- example, if you want to add additional logging when a user logs in:
495
+ block which overrides the default method that Rodauth uses. Inside the
496
+ block, you can call super if you want to get the default behavior, though
497
+ you must provide explicit arguments to super. There is no need to
498
+ call super in before or after hooks, though. For example, if you want to
499
+ add additional logging when a user logs in:
298
500
 
299
501
  plugin :rodauth do
300
502
  enable :login, :logout
301
503
  after_login do
302
- logger.info "#{account.email} logged in!"
303
- super
504
+ LOGGER.info "#{account.email} logged in!"
304
505
  end
305
506
  end
306
507
 
@@ -320,8 +521,7 @@ So if you want to log the IP address for the user during login:
320
521
  plugin :rodauth do
321
522
  enable :login, :logout
322
523
  after_login do
323
- logger.info "#{account.email} logged in from #{request.ip}"
324
- super
524
+ LOGGER.info "#{account.email} logged in from #{request.ip}"
325
525
  end
326
526
  end
327
527
 
@@ -329,33 +529,22 @@ The second type of configuration methods are called auth value
329
529
  methods. They are similar to auth methods, but instead of just
330
530
  accepting a block, they can optionally accept a single argument
331
531
  without a block, which will be treated as a block that just returns
332
- that value. For example, the account_model method sets the model
333
- class to use for the account, so to override it, you can call the
334
- method with another class:
532
+ that value. For example, the accounts_table method sets the database
533
+ table storing accounts, so to override it, you can call the method
534
+ with a symbol for the table:
335
535
 
336
536
  plugin :rodauth do
337
537
  enable :login, :logout
338
- account_model User
339
- end
340
-
341
- The third type of configuration methods are called auth block methods,
342
- and there are three of them per feature, one for handling the route
343
- itself, one for handling just the GET route, and one for handling just
344
- the POST route. For the login feature, login_route_block would set
345
- the routing block to use if the login route matches, login_get_block
346
- would set the routing block to use if the login route matches and it
347
- is a GET request, and login_post_block would set the routing block to
348
- use if the login route matches and it is a POST request. As auth block
349
- methods specify the route blocks, they are executed in the context
350
- of the Roda instance, and are passed two arguments, the first being the
351
- RodaRequest instance, and the second being the Rodauth::Auth instance.
352
- For example, if you wanted to override how a POST request to the login
353
- route is handled:
538
+ accounts_table :users
539
+ end
540
+
541
+ Note that all auth value methods can still take a block, allowing
542
+ overriding for all behavior, using any information from the request:
354
543
 
355
544
  plugin :rodauth do
356
- enable :login
357
- login_post_route do |r, auth|
358
- # ...
545
+ enable :login, :logout
546
+ accounts_table do
547
+ request.ip.start_with?("192.168.1") ? :admins : :users
359
548
  end
360
549
  end
361
550
 
@@ -369,6 +558,9 @@ separate page per feature. If these links are not active, please
369
558
  view the appropriate file in the doc directory.
370
559
 
371
560
  * {Base}[rdoc-ref:doc/base.rdoc] (this feature is autoloaded)
561
+ * {Login Password Requirements Base}[rdoc-ref:doc/login_password_requirements_base.rdoc] (this feature is autoloaded by features that set logins/passwords)
562
+ * {Email Base}[rdoc-ref:doc/email_base.rdoc] (this feature is autoloaded by features that send email)
563
+ * {Two Factor Base}[rdoc-ref:doc/two_factor_base.rdoc] (this feature is autoloaded by 2 factor authentication features)
372
564
  * {Login}[rdoc-ref:doc/login.rdoc]
373
565
  * {Logout}[rdoc-ref:doc/logout.rdoc]
374
566
  * {Change Password}[rdoc-ref:doc/change_password.rdoc]
@@ -377,13 +569,81 @@ view the appropriate file in the doc directory.
377
569
  * {Create Account}[rdoc-ref:doc/create_account.rdoc]
378
570
  * {Close Account}[rdoc-ref:doc/close_account.rdoc]
379
571
  * {Verify Account}[rdoc-ref:doc/verify_account.rdoc]
572
+ * {Confirm Password}[rdoc-ref:doc/confirm_password.rdoc]
380
573
  * {Remember}[rdoc-ref:doc/remember.rdoc]
381
574
  * {Lockout}[rdoc-ref:doc/lockout.rdoc]
575
+ * {OTP}[rdoc-ref:doc/otp.rdoc]
576
+ * {Recovery Codes}[rdoc-ref:doc/recovery_codes.rdoc]
577
+ * {SMS Codes}[rdoc-ref:doc/sms_codes.rdoc]
578
+ * {Verify Change Login}[rdoc-ref:doc/verify_change_login.rdoc]
579
+ * {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]
580
+ * {Password Grace Period}[rdoc-ref:doc/password_grace_period.rdoc]
581
+ * {Password Complexity}[rdoc-ref:doc/password_complexity.rdoc]
582
+ * {Disallow Password Reuse}[rdoc-ref:doc/disallow_password_reuse.rdoc]
583
+ * {Password Expiration}[rdoc-ref:doc/password_expiration.rdoc]
584
+ * {Account Expiration}[rdoc-ref:doc/account_expiration.rdoc]
585
+ * {Session Expiration}[rdoc-ref:doc/session_expiration.rdoc]
586
+ * {Single Session}[rdoc-ref:doc/single_session.rdoc]
587
+ * {JWT}[rdoc-ref:doc/jwt.rdoc]
588
+
589
+ === Calling Rodauth in the Routing Tree
590
+
591
+ In general, you will usually want to call rodauth early in your
592
+ route block:
593
+
594
+ route do |r|
595
+ r.rodauth
596
+
597
+ # ...
598
+ end
599
+
600
+ Note that will allow Rodauth to run, but it won't force people
601
+ to login or add any security to your site. If you want to force
602
+ all users to login, you need to redirect to them login page if
603
+ they are not already logged in:
604
+
605
+ route do |r|
606
+ r.rodauth
607
+ rodauth.require_authentication
608
+
609
+ # ...
610
+ end
611
+
612
+ If only certain parts of your site require logins, then you can
613
+ only redirect if they are not logged in certain branches of the
614
+ routing tree:
615
+
616
+ route do |r|
617
+ r.rodauth
618
+
619
+ r.on "admin" do
620
+ rodauth.require_authentication
621
+
622
+ # ...
623
+ end
624
+
625
+ # ...
626
+ end
627
+
628
+ In some cases you may want to have rodauth run inside a branch of
629
+ the routing tree, instead of in the root. You can do this by
630
+ setting a +:prefix+ when configuring Rodauth, and calling +r.rodauth+
631
+ inside a matching routing tree branch:
382
632
 
383
- Since the auth block methods work the same way for each of these
384
- features, they are not documented on the feature pages. Additionally,
385
- all features have a before auth method (e.g. before_login) that is
386
- called before either the GET or POST route blocks are handled.
633
+ plugin :rodauth do
634
+ enable :login, :logout
635
+ prefix "auth"
636
+ end
637
+
638
+ route do |r|
639
+ r.on "auth" do
640
+ r.rodauth
641
+ end
642
+
643
+ rodauth.require_authentication
644
+
645
+ # ...
646
+ end
387
647
 
388
648
  === With Multiple Configurations
389
649
 
@@ -407,26 +667,79 @@ the name as an argument to use that configuration:
407
667
  r.rodauth
408
668
  end
409
669
 
410
- === With Other Databases
670
+ === With Password Hashes Inside the Accounts Table
411
671
 
412
- You can use Rodauth with other databases besides PostgreSQL. Assuming
413
- you are storing the password hashes in the same table as the account
414
- information, you can just do:
672
+ You can use Rodauth if you are storing password hashes in the same
673
+ table as the accounts. You just need to specify which column
674
+ stores the password hash:
415
675
 
416
676
  plugin :rodauth do
417
677
  account_password_hash_column :password_hash
418
678
  end
419
679
 
420
- When this option is set, Rodauth will not use a database function
421
- to authenticate, it will do the check in ruby. This feature can
422
- also be used if you are using PostgreSQL, but for legacy reasons
423
- are storing the password hashes in the same table as the account
424
- information.
680
+ When this option is set, Rodauth will do the password hash check
681
+ in ruby.
682
+
683
+ === When Using PostgreSQL/MySQL/Microsoft SQL Server without Database Functions
684
+
685
+ If you want to use Rodauth on PostgreSQL, MySQL, or Microsoft SQL Server
686
+ without using database functions for authentication, but still storing password
687
+ hashes in a separate table, you can do so:
688
+
689
+ plugin :rodauth do
690
+ use_database_authentication_functions? false
691
+ end
425
692
 
426
- The Rodauth lockout feature also uses UPDATE RETURNING to update
427
- a row and return the new value, so if you are not using PostgreSQL
428
- and wish to use the lockout feature, you'll need to override the
429
- invalid_login_attempted method.
693
+ Conversely, if you implement the rodauth_get_salt and
694
+ rodauth_valid_password_hash functions on a database that isn't
695
+ PostgreSQL, MySQL, or Microsoft SQL Server, you can set this value to true.
696
+
697
+ === With Custom Authentication (such as LDAP)
698
+
699
+ You can use Rodauth with other authentication types, by overriding
700
+ a single configuration setting. For example, if you have accounts
701
+ stored in the database, but authentication happens via LDAP, you
702
+ can use the +simple_ldap_authenticator+ library:
703
+
704
+ require 'simple_ldap_authenticator'
705
+ plugin :rodauth do
706
+ enable :login, :logout
707
+ require_bcrypt? false
708
+ password_match? do |password|
709
+ SimpleLdapAuthenticator.valid?(account.username, password)
710
+ end
711
+ end
712
+
713
+ If you aren't storing accounts in the database, but want to allow
714
+ any valid LDAP user to login, you can do something like this:
715
+
716
+ require 'simple_ldap_authenticator'
717
+ plugin :rodauth do
718
+ enable :login, :logout
719
+
720
+ # Don't require the bcrypt library, since using LDAP for auth
721
+ require_bcrypt? false
722
+
723
+ # Treat the login itself as the account
724
+ account_from_login{|l| l.to_s}
725
+
726
+ # Use the login provided as the session value
727
+ account_session_value{account}
728
+
729
+ # Store session value in :login key, since the :account_id
730
+ # default wouldn't make sense
731
+ session_key :login
732
+
733
+ password_match? do |password|
734
+ SimpleLdapAuthenticator.valid?(account, password)
735
+ end
736
+ end
737
+
738
+ Note that when using custom authentication, using some of Rodauth's
739
+ features such as change login and change password either would not
740
+ make sense or would require some additional custom configuration.
741
+ The login and logout features should work correctly with the examples
742
+ above, though.
430
743
 
431
744
  === With Other Web Frameworks
432
745
 
@@ -438,47 +751,192 @@ Rodauth:
438
751
 
439
752
  class RodauthApp < Roda
440
753
  plugin :middleware
441
- plugin :rodauth
754
+ plugin :rodauth do
755
+ enable :login
756
+ end
757
+
442
758
  route do |r|
443
759
  r.rodauth
760
+ env['rodauth'] = rodauth
761
+ rodauth.require_authentication
444
762
  end
445
763
  end
446
764
 
447
765
  use RodauthApp
448
766
 
767
+ Note that Rodauth expects the Roda app it is used in to provide a
768
+ layout. So if you are using Rodauth as middleware for another app,
769
+ if you don't have a +views/layout.erb+ file that Rodauth can use,
770
+ you should probably also add load Roda's +render+ plugin
771
+ with the appropriate settings that allow Rodauth to use the same
772
+ layout as the application.
773
+
774
+ By setting <tt>env['rodauth'] = rodauth</tt> in the route block
775
+ inside the middleware, you can easily provide a way for your
776
+ application to call Rodauth methods.
777
+
778
+ For an example of integrating Rodauth into a real application that
779
+ doesn't use Roda, see
780
+ {this example integrating Rodauth into Ginatra, a Sinatra-based git repository viewer}[https://github.com/jeremyevans/ginatra/commit/28108ebec96e8d42596ee55b01c3f7b50c155dd1].
781
+
782
+ === Using 2 Factor Authentication
783
+
784
+ Rodauth ships with 2 factor authentication support via TOTP (Time-Based
785
+ One-Time Passwords, RFC 6238). There are a wide variety of ways in
786
+ which to integrate 2 factor authentication into your site with Rodauth,
787
+ based on the needs of the application.
788
+
789
+ The 2 factor authentication support is part of the OTP feature, which
790
+ needs to be enabled in addition to the login feature. In addition,
791
+ when implementing 2 factor authentication, you should generally
792
+ provide a backup 2nd factor if the primary second factor is not
793
+ available. Rodauth supports SMS codes and recovery codes as other
794
+ 2nd factors.
795
+
796
+ If you want to support but not require 2 factor authentication:
797
+
798
+ plugin :rodauth do
799
+ enable :login, :logout, :otp, :recovery_codes, :sms_codes
800
+ end
801
+ route do |r|
802
+ r.rodauth
803
+ rodauth.require_authentication
804
+
805
+ # ...
806
+ end
807
+
808
+ If you want to force all users to use OTP authentication, requiring users
809
+ that don't currently have an account to set one up:
810
+
811
+ route do |r|
812
+ r.rodauth
813
+ rodauth.require_authentication
814
+ rodauth.require_two_factor_authentication_setup
815
+
816
+ # ...
817
+ end
818
+
819
+ Similarly to requiring authentication in general, it's possible to require
820
+ login authentication for most of the site, but require 2 factor
821
+ authentication only for particular branches:
822
+
823
+ route do |r|
824
+ r.rodauth
825
+ rodauth.require_login
826
+
827
+ r.on "admin" do
828
+ rodauth.require_two_factor_authenticated
829
+ end
830
+
831
+ # ...
832
+ end
833
+
834
+ == JSON API Support
835
+
836
+ To add support for handling JSON responses, you can pass the +:json+
837
+ option to the plugin, and enable the JWT feature in addition to
838
+ other features you plan to use:
839
+
840
+ plugin :rodauth, :json=>true do
841
+ enable :login, :logout, :jwt
842
+ end
843
+
844
+ If you do not want to load the HTML plugins that Rodauth usually loads
845
+ (render, csrf, flash, h), because you are building a JSON-only API,
846
+ pass <tt>:json => :only</tt>
847
+
848
+ plugin :rodauth, :json=>:only do
849
+ enable :login, :logout, :jwt
850
+ end
851
+
852
+ Note that by default, the features that send email depend on the
853
+ render plugin, so if using the <tt>:json=>:only</tt> option, you
854
+ either need to load the render plugin manually or you need to
855
+ use the necessary *_email_body configuration options to specify
856
+ the body of the emails.
857
+
858
+ The JWT feature enables JSON API support for all of the other features
859
+ that Rodauth ships with.
860
+
861
+ === Adding Custom Methods to the +rodauth+ Object
862
+
863
+ Inside the configuration block, you can use +auth_class_eval+ to add
864
+ custom methods that will be callable on the +rodauth+ object.
865
+
866
+ plugin :rodauth do
867
+ enable :login
868
+
869
+ auth_class_eval do
870
+ def require_admin
871
+ request.redirect("/") unless account[:admin]
872
+ end
873
+ end
874
+ end
875
+
876
+ route do |r|
877
+ r.rodauth
878
+
879
+ r.on "admin"
880
+ rodauth.require_admin
881
+ end
882
+ end
883
+
449
884
  === Using External Features
450
885
 
451
886
  The enable configuration method is able to load features external to
452
887
  Rodauth. You need to place the external feature file where it can be
453
- required via roda/plugins/rodauth/feature_name. That file should
888
+ required via rodauth/features/feature_name. That file should
454
889
  use the following basic structure
455
890
 
456
- class Roda
457
- module RodaPlugins
458
- module Rodauth
459
- # :feature_name will be the argument given to enable to
460
- # load the feature
461
- FeatureName = Feature.define(:feature_name) do
462
- auth_value_methods # one argument per auth value method
463
- auth_methods # one argument per auth method
464
-
465
- get_block do |r, auth|
466
- # r is the RodaRequest instance
467
- # auth is the Rodauth::Auth instance
468
- # This block is evaluated in the scope of the Roda instance
469
- # ...
470
- end
471
-
472
- post_block do |r, auth|
473
- # ...
474
- end
475
-
476
- # define the default behavior for the auth methods
477
- # and auth value methods
478
- # ...
891
+ module Rodauth
892
+ # :feature_name will be the argument given to enable to
893
+ # load the feature
894
+ FeatureName = Feature.define(:feature_name) do
895
+ # Shortcut for defining auth value methods with static values
896
+ auth_value_method :method_name, 1 # method_value
897
+
898
+ auth_value_methods # one argument per auth value method
899
+
900
+ auth_methods # one argument per auth method
901
+
902
+ route do |r|
903
+ # This block is taken for requests to the feature's route.
904
+ # This block is evaluated in the scope of the Rodauth::Auth instance.
905
+ # r is the Roda::RodaRequest instance for the request
906
+
907
+ r.get do
908
+ end
909
+
910
+ r.post do
479
911
  end
480
912
  end
913
+
914
+ configuration_eval do
915
+ # define additional configuration specific methods here, if any
916
+ end
917
+
918
+ # define the default behavior for the auth_methods
919
+ # and auth_value_methods
920
+ # ...
921
+ end
922
+ end
923
+
924
+ See the source code for the features that ship with Rodauth for an
925
+ example of how to construct features.
926
+
927
+ === Overriding Route-Level Behavior
928
+
929
+ All of Rodauth's configuration methods change the behavior of the
930
+ Rodauth::Auth instance. However, in some cases you may want to
931
+ overriding handling at the routing layer. You can do this easily
932
+ by adding an appropriate route before calling +r.rodauth+:
933
+
934
+ route do |r|
935
+ r.post 'login' do
936
+ # Custom POST /login handling here
481
937
  end
938
+
939
+ r.rodauth
482
940
  end
483
941
 
484
942
  == Upgrading from 0.9.x
@@ -489,47 +947,13 @@ it and add the two database functions listed in the migration
489
947
  section above. You can add the following code to a migration to
490
948
  accomplish that:
491
949
 
950
+ require 'rodauth/migrations'
492
951
  run "DROP FUNCTION account_valid_password(int8, text);"
493
-
494
- run <<END
495
- CREATE OR REPLACE FUNCTION rodauth_get_salt(account_id int8) RETURNS text AS $$
496
- DECLARE salt text;
497
- BEGIN
498
- SELECT substr(password_hash, 0, 30) INTO salt
499
- FROM account_password_hashes
500
- WHERE account_id = id;
501
- RETURN salt;
502
- END;
503
- $$ LANGUAGE plpgsql
504
- SECURITY DEFINER
505
- SET search_path = public, pg_temp;
506
- END
507
-
508
- run <<END
509
- CREATE OR REPLACE FUNCTION rodauth_valid_password_hash(account_id int8, hash text) RETURNS boolean AS $$
510
- DECLARE valid boolean;
511
- BEGIN
512
- SELECT password_hash = hash INTO valid
513
- FROM account_password_hashes
514
- WHERE account_id = id;
515
- RETURN valid;
516
- END;
517
- $$ LANGUAGE plpgsql
518
- SECURITY DEFINER
519
- SET search_path = public, pg_temp;
520
- END
521
-
522
- # Restrict access to the password hash table
523
- app_user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
952
+ Rodauth.create_database_authentication_functions(self)
524
953
  run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
525
954
  run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
526
- run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{app_user}"
527
- run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{app_user}"
528
-
529
- == Possible Future Directions
530
-
531
- * OmniAuth support. This is not something I plan to work on myself,
532
- but I will consider patches that add it.
955
+ run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO ${DATABASE_NAME}"
956
+ run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO ${DATABASE_NAME}"
533
957
 
534
958
  == Similar Projects
535
959