rodauth 0.10.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +146 -0
- data/README.rdoc +644 -220
- data/Rakefile +99 -11
- data/doc/account_expiration.rdoc +55 -0
- data/doc/base.rdoc +104 -0
- data/doc/change_login.rdoc +29 -0
- data/doc/change_password.rdoc +26 -0
- data/doc/close_account.rdoc +31 -0
- data/doc/confirm_password.rdoc +22 -0
- data/doc/create_account.rdoc +34 -0
- data/doc/disallow_password_reuse.rdoc +37 -0
- data/doc/email_base.rdoc +19 -0
- data/doc/jwt.rdoc +35 -0
- data/doc/lockout.rdoc +83 -0
- data/doc/login.rdoc +27 -0
- data/doc/login_password_requirements_base.rdoc +50 -0
- data/doc/logout.rdoc +21 -0
- data/doc/otp.rdoc +100 -0
- data/doc/password_complexity.rdoc +50 -0
- data/doc/password_expiration.rdoc +52 -0
- data/doc/password_grace_period.rdoc +10 -0
- data/doc/recovery_codes.rdoc +60 -0
- data/doc/release_notes/1.0.0.txt +443 -0
- data/doc/remember.rdoc +82 -0
- data/doc/reset_password.rdoc +70 -0
- data/doc/session_expiration.rdoc +27 -0
- data/doc/single_session.rdoc +43 -0
- data/doc/sms_codes.rdoc +119 -0
- data/doc/two_factor_base.rdoc +27 -0
- data/doc/verify_account.rdoc +70 -0
- data/doc/verify_account_grace_period.rdoc +15 -0
- data/doc/verify_change_login.rdoc +9 -0
- data/lib/roda/plugins/rodauth.rb +3 -262
- data/lib/rodauth.rb +260 -0
- data/lib/rodauth/features/account_expiration.rb +108 -0
- data/lib/rodauth/features/base.rb +479 -0
- data/lib/rodauth/features/change_login.rb +77 -0
- data/lib/rodauth/features/change_password.rb +66 -0
- data/lib/rodauth/features/close_account.rb +82 -0
- data/lib/rodauth/features/confirm_password.rb +51 -0
- data/lib/rodauth/features/create_account.rb +128 -0
- data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
- data/lib/rodauth/features/email_base.rb +63 -0
- data/lib/rodauth/features/jwt.rb +151 -0
- data/lib/rodauth/features/lockout.rb +262 -0
- data/lib/rodauth/features/login.rb +61 -0
- data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
- data/lib/rodauth/features/logout.rb +37 -0
- data/lib/rodauth/features/otp.rb +338 -0
- data/lib/rodauth/features/password_complexity.rb +89 -0
- data/lib/rodauth/features/password_expiration.rb +111 -0
- data/lib/rodauth/features/password_grace_period.rb +46 -0
- data/lib/rodauth/features/recovery_codes.rb +240 -0
- data/lib/rodauth/features/remember.rb +200 -0
- data/lib/rodauth/features/reset_password.rb +207 -0
- data/lib/rodauth/features/session_expiration.rb +55 -0
- data/lib/rodauth/features/single_session.rb +87 -0
- data/lib/rodauth/features/sms_codes.rb +498 -0
- data/lib/rodauth/features/two_factor_base.rb +135 -0
- data/lib/rodauth/features/verify_account.rb +232 -0
- data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
- data/lib/rodauth/features/verify_change_login.rb +20 -0
- data/lib/rodauth/migrations.rb +130 -0
- data/lib/rodauth/version.rb +9 -0
- data/spec/account_expiration_spec.rb +90 -0
- data/spec/all.rb +1 -0
- data/spec/change_login_spec.rb +149 -0
- data/spec/change_password_spec.rb +177 -0
- data/spec/close_account_spec.rb +162 -0
- data/spec/confirm_password_spec.rb +70 -0
- data/spec/create_account_spec.rb +127 -0
- data/spec/disallow_password_reuse_spec.rb +84 -0
- data/spec/lockout_spec.rb +228 -0
- data/spec/login_spec.rb +188 -0
- data/spec/migrate/001_tables.rb +103 -16
- data/spec/migrate/002_account_password_hash_column.rb +11 -0
- data/spec/migrate_password/001_tables.rb +60 -42
- data/spec/migrate_travis/001_tables.rb +116 -0
- data/spec/password_complexity_spec.rb +108 -0
- data/spec/password_expiration_spec.rb +243 -0
- data/spec/password_grace_period_spec.rb +93 -0
- data/spec/remember_spec.rb +424 -0
- data/spec/reset_password_spec.rb +185 -0
- data/spec/rodauth_spec.rb +57 -980
- data/spec/session_expiration_spec.rb +58 -0
- data/spec/single_session_spec.rb +107 -0
- data/spec/spec_helper.rb +202 -0
- data/spec/two_factor_spec.rb +1310 -0
- data/spec/verify_account_grace_period_spec.rb +135 -0
- data/spec/verify_account_spec.rb +142 -0
- data/spec/verify_change_login_spec.rb +46 -0
- data/spec/views/login.str +2 -2
- data/templates/add-recovery-codes.str +2 -0
- data/templates/button.str +5 -0
- data/templates/change-login.str +5 -18
- data/templates/change-password.str +6 -14
- data/templates/close-account.str +3 -6
- data/templates/confirm-password.str +4 -14
- data/templates/create-account.str +6 -30
- data/templates/login-confirm-field.str +6 -0
- data/templates/login-field.str +6 -0
- data/templates/login.str +5 -19
- data/templates/logout.str +2 -6
- data/templates/otp-auth-code-field.str +6 -0
- data/templates/otp-auth.str +8 -0
- data/templates/otp-disable.str +6 -0
- data/templates/otp-setup.str +21 -0
- data/templates/password-confirm-field.str +6 -0
- data/templates/password-field.str +6 -0
- data/templates/recovery-auth.str +12 -0
- data/templates/recovery-codes.str +6 -0
- data/templates/remember.str +8 -12
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +4 -18
- data/templates/sms-auth.str +6 -0
- data/templates/sms-code-field.str +6 -0
- data/templates/sms-confirm.str +7 -0
- data/templates/sms-disable.str +7 -0
- data/templates/sms-request.str +5 -0
- data/templates/sms-setup.str +12 -0
- data/templates/unlock-account-request.str +3 -7
- data/templates/unlock-account.str +4 -7
- data/templates/verify-account-resend.str +2 -2
- data/templates/verify-account.str +2 -6
- metadata +191 -29
- data/lib/roda/plugins/rodauth/base.rb +0 -428
- data/lib/roda/plugins/rodauth/change_login.rb +0 -48
- data/lib/roda/plugins/rodauth/change_password.rb +0 -42
- data/lib/roda/plugins/rodauth/close_account.rb +0 -42
- data/lib/roda/plugins/rodauth/create_account.rb +0 -92
- data/lib/roda/plugins/rodauth/lockout.rb +0 -292
- data/lib/roda/plugins/rodauth/login.rb +0 -81
- data/lib/roda/plugins/rodauth/logout.rb +0 -36
- data/lib/roda/plugins/rodauth/remember.rb +0 -226
- data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
- data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f7b621217958c77c148ee85c271e7e39548b02f
|
4
|
+
data.tar.gz: f4fa75ad99266693763134533d5f7f0dc90de8ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
data/README.rdoc
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
= Rodauth
|
2
2
|
|
3
|
-
Rodauth is an authentication
|
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
|
-
|
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
|
-
===
|
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
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
While the
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
60
|
-
database containing password hashes can result
|
61
|
-
other sites, making password hash storage of
|
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
|
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
|
-
|
83
|
-
|
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,
|
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
|
92
|
-
2
|
93
|
-
3
|
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
|
-
|
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
|
121
|
-
|
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
|
125
|
-
|
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 $
|
130
|
-
createuser -U postgres $
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
163
|
-
|
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,
|
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,
|
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,
|
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
|
-
#
|
198
|
-
|
199
|
-
|
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(:
|
204
|
-
|
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
|
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
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
261
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
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
|
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
|
296
|
-
block, you can call super if you want to get the default behavior
|
297
|
-
|
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
|
-
|
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
|
-
|
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
|
333
|
-
|
334
|
-
|
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
|
-
|
339
|
-
end
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
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
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
670
|
+
=== With Password Hashes Inside the Accounts Table
|
411
671
|
|
412
|
-
You can use Rodauth
|
413
|
-
|
414
|
-
|
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
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
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
|
888
|
+
required via rodauth/features/feature_name. That file should
|
454
889
|
use the following basic structure
|
455
890
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
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
|
527
|
-
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO
|
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
|
|