rodauth 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +18 -0
  4. data/README.rdoc +484 -0
  5. data/Rakefile +91 -0
  6. data/lib/roda/plugins/rodauth.rb +265 -0
  7. data/lib/roda/plugins/rodauth/base.rb +428 -0
  8. data/lib/roda/plugins/rodauth/change_login.rb +48 -0
  9. data/lib/roda/plugins/rodauth/change_password.rb +42 -0
  10. data/lib/roda/plugins/rodauth/close_account.rb +42 -0
  11. data/lib/roda/plugins/rodauth/create_account.rb +92 -0
  12. data/lib/roda/plugins/rodauth/lockout.rb +292 -0
  13. data/lib/roda/plugins/rodauth/login.rb +77 -0
  14. data/lib/roda/plugins/rodauth/logout.rb +36 -0
  15. data/lib/roda/plugins/rodauth/remember.rb +226 -0
  16. data/lib/roda/plugins/rodauth/reset_password.rb +205 -0
  17. data/lib/roda/plugins/rodauth/verify_account.rb +228 -0
  18. data/spec/migrate/001_tables.rb +64 -0
  19. data/spec/migrate_password/001_tables.rb +38 -0
  20. data/spec/rodauth_spec.rb +1114 -0
  21. data/spec/views/layout.str +11 -0
  22. data/spec/views/login.str +21 -0
  23. data/templates/change-login.str +22 -0
  24. data/templates/change-password.str +21 -0
  25. data/templates/close-account.str +9 -0
  26. data/templates/confirm-password.str +16 -0
  27. data/templates/create-account.str +33 -0
  28. data/templates/login.str +25 -0
  29. data/templates/logout.str +9 -0
  30. data/templates/remember.str +28 -0
  31. data/templates/reset-password-email.str +5 -0
  32. data/templates/reset-password-request.str +7 -0
  33. data/templates/reset-password.str +23 -0
  34. data/templates/unlock-account-email.str +5 -0
  35. data/templates/unlock-account-request.str +11 -0
  36. data/templates/unlock-account.str +11 -0
  37. data/templates/verify-account-email.str +4 -0
  38. data/templates/verify-account-resend.str +7 -0
  39. data/templates/verify-account.str +11 -0
  40. metadata +227 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8a4422adf6022a2840a86586b6e453bd3661e951
4
+ data.tar.gz: 9b97988eff1528ca51c74e7b1407f7b92c147eca
5
+ SHA512:
6
+ metadata.gz: 4e6b4d2b641bd15b6db50267636e1e3fccbb202d26be23e597df098da039238d090c5afd16f1d7d66b5aee23fa161df7bdf9cde45730971b2f2466b2b4be610d
7
+ data.tar.gz: d0660ec47396732cdf6d4bb70765fe09d5c02eeb27a6f09811cb3a519c7befd83c86af4c7b178313991160ee10d798f4e100d682d901b46cf2242ea8c50d9b14
@@ -0,0 +1,3 @@
1
+ === 0.9.0 (2015-08-12)
2
+
3
+ * Initial public release
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2015 Jeremy Evans
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,484 @@
1
+ = Rodauth
2
+
3
+ Rodauth is an authentication framework using Roda, Sequel, and PostgreSQL.
4
+
5
+ == Design Goals
6
+
7
+ * Security: Ship in a maximum security by default configuration
8
+ * Simplicity: Allow for easy configuration via a DSL
9
+ * Flexibility: Allow for easy overriding of any part of the framework
10
+
11
+ == Features
12
+
13
+ * Login
14
+ * Logout
15
+ * Change Password
16
+ * Change Login
17
+ * Reset Password
18
+ * Create Account
19
+ * Close Account
20
+ * Verify Account
21
+ * Remember (Autologin via token)
22
+ * Lockout (Bruteforce protection)
23
+
24
+ == Resources
25
+
26
+ RDoc :: http://rodauth.jeremyevans.net
27
+ Demo Site :: http://rodauth-demo.jeremyevans.net
28
+ Source :: http://github.com/jeremyevans/rodauth
29
+ Bugs :: http://github.com/jeremyevans/rodauth/issues
30
+
31
+ == Security
32
+
33
+ === Passwords
34
+
35
+ Passwords are hashed using bcrypt, and the password hashes are
36
+ kept in a separate table from the accounts table, with a foreign key
37
+ referencing the accounts table. A PostgreSQL function is added to
38
+ check the password for a given application account matches.
39
+
40
+ A separate database account owns the table containing the password
41
+ hashes, which the application database account cannot access.
42
+ The application database account has the ability to execute the
43
+ function to check the password, but not the ability to access the
44
+ password hashes directly, making it much more difficult for an
45
+ attacker to access the password hashes even if they are able to
46
+ exploit an SQL injection or remote code execution vulnerability
47
+ in the application. Even if an attacker was able to exploit a
48
+ vulnerability in the application, they would only be to check if
49
+ a specific password matches for a given user, which is the same
50
+ access an attacker would have anyway if they just tried to login.
51
+
52
+ While the application database account is not be able to read
53
+ password hashes, it is still be able to insert password hashes,
54
+ update passwords hashes, and delete password hashes, so the
55
+ additional security is not that painful.
56
+
57
+ The reason for extra security in regards to password hashes stems from
58
+ the fact that people tend to reuse passwords, so a compromise of one
59
+ site can result in account access on other sites, making password hash
60
+ storage of critical importance even if the other data stored is not
61
+ that important.
62
+
63
+ If you are storing other important information in your database, you
64
+ should consider using a similar approach in other areas (or all areas)
65
+ of your application.
66
+
67
+ Rodauth can still be used if you are using a more convential approach
68
+ of storing the password hash in a column in the same table, with
69
+ a single configuration setting.
70
+
71
+ === Tokens
72
+
73
+ Account verification, password resets, remember, and lockout tokens
74
+ all use a similar approach. They all provide a token, in the format
75
+ "account-id_long-random-string". By including the id of the account
76
+ in the token, an attacker can only attempt to bruteforce the token
77
+ for a single account, instead of being able to bruteforce tokens for
78
+ all accounts at once (which would be possible if the token was just a
79
+ random string).
80
+
81
+ There is a maximum of 1 token per account for each of these features
82
+ at a time. This prevents attackers from creating an arbitrary number
83
+ of requests in order to make bruteforcing easier.
84
+
85
+ == Database Setup
86
+
87
+ In order to get full advantages of Rodauth's security design, multiple
88
+ database accounts are involved:
89
+
90
+ 1) database superuser account (usually postgres)
91
+ 2) application database account
92
+ 3) secondary database account
93
+
94
+ The database superuser account is used to load extensions related to the
95
+ database. The application should never be run using the database
96
+ superuser account.
97
+
98
+ Note that there is not a simple way to use multiple database accounts in
99
+ the same PostgreSQL database on Heroku. You can still use Rodauth on
100
+ Heroku, it just won't have the same security benefits. That's not to say
101
+ it is insecure, just that it drops the security level for password hash
102
+ storage to the same level as other common authentication solutions.
103
+
104
+ === Load extensions
105
+
106
+ If you want to use the login features for Rodauth, you need to load the
107
+ pgcrypto extension with the database superuser account, and load the
108
+ citext extension if you want to support case insensitive logins.
109
+
110
+ Example:
111
+
112
+ echo "CREATE EXTENSION pgcrypto" | psql -U postgres $database_name
113
+ echo "CREATE EXTENSION citext" | psql -U postgres $database_name
114
+
115
+ Note that on Heroku, both of these extensions can be loaded using a
116
+ standard database account.
117
+
118
+ === Create database accounts
119
+
120
+ If you are currently running your application using the database superuser
121
+ account, the first thing you need to do is to create a database account for
122
+ the application. It's often best to name this account the same as the
123
+ database name.
124
+
125
+ You should also create a second database account which will own the password
126
+ hash table.
127
+
128
+ Example:
129
+
130
+ createuser -U postgres $database_name
131
+ createuser -U postgres $database_name_password_hashes
132
+
133
+ Note that if the database superuser account owns all of the items in the
134
+ database, you'll need to change the ownership to the database account you
135
+ just created. See https://gist.github.com/jeremyevans/8483320
136
+ for a way to do that.
137
+
138
+ === Create tables
139
+
140
+ Because two different database accounts are used, two different migrations
141
+ are required, one for each database account. Here are example migrations.
142
+ You can modify them to add support for additional columns, or remove tables
143
+ or columns related to features that you don't need.
144
+
145
+ First migration, run using the application database account:
146
+
147
+ Sequel.migration do
148
+ up do
149
+ # Used by the account verification and close account features
150
+ create_table(:account_statuses) do
151
+ Integer :id, :primary_key=>true
152
+ String :name, :null=>false, :unique=>true
153
+ end
154
+ from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
155
+
156
+ # Used by the create account, account verification,
157
+ # and close account features.
158
+ create_table(:accounts) do
159
+ primary_key :id, :type=>Bignum
160
+ foreign_key :status_id, :account_statuses, :null=>false, :default=>1
161
+ citext :email, :null=>false
162
+
163
+ constraint :valid_email, :email=>/^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
164
+ index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
165
+ end
166
+
167
+ # Used by the password reset feature
168
+ create_table(:account_password_reset_keys) do
169
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
170
+ String :key, :null=>false
171
+ DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '1 day'")
172
+ end
173
+
174
+ # Used by the account verification feature
175
+ create_table(:account_verification_keys) do
176
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
177
+ String :key, :null=>false
178
+ end
179
+
180
+ # Used by the remember me feature
181
+ create_table(:account_remember_keys) do
182
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
183
+ String :key, :null=>false
184
+ DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '2 weeks'")
185
+ end
186
+
187
+ # Used by the lockout feature
188
+ create_table(:account_login_failures) do
189
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
190
+ Integer :number, :null=>false, :default=>1
191
+ end
192
+ create_table(:account_lockouts) do
193
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
194
+ String :key, :null=>false
195
+ DateTime :deadline, :null=>false, :default=>Sequel.lit("CURRENT_TIMESTAMP + '1 day'")
196
+ end
197
+
198
+ # Grant password user access to reference accounts
199
+ pw_user = get{Sequel.lit('current_user')} + '_password'
200
+ run "GRANT REFERENCES ON accounts TO #{pw_user}"
201
+ end
202
+
203
+ down do
204
+ drop_table(:account_lockouts, :account_login_failures, :account_remember_keys,
205
+ :account_verification_keys, :account_password_reset_keys, :accounts, :account_statuses)
206
+ end
207
+ end
208
+
209
+ Second migration, run using the secondary database account:
210
+
211
+ Sequel.migration do
212
+ up do
213
+ # Used by the login and change password features
214
+ create_table(:account_password_hashes) do
215
+ foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
216
+ String :password_hash, :null=>false
217
+ end
218
+
219
+ # Function used to check if a password is valid. Takes the related account id
220
+ # and unencrypted password, checks if password matches password hash.
221
+ run <<END
222
+ CREATE OR REPLACE FUNCTION account_valid_password(account_id int8, password text) RETURNS boolean AS $$
223
+ DECLARE valid boolean;
224
+ BEGIN
225
+ SELECT password_hash = crypt($2, password_hash) INTO valid
226
+ FROM account_password_hashes
227
+ WHERE account_id = id;
228
+ RETURN valid;
229
+ END;
230
+ $$ LANGUAGE plpgsql
231
+ SECURITY DEFINER
232
+ SET search_path = public, pg_temp;
233
+ END
234
+
235
+ # Restrict access to the password hash table
236
+ app_user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
237
+ run "REVOKE ALL ON account_password_hashes FROM public"
238
+ run "REVOKE ALL ON FUNCTION account_valid_password(int8, text) FROM public"
239
+ run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{app_user}"
240
+ run "GRANT SELECT(id) ON account_password_hashes TO #{app_user}"
241
+ run "GRANT EXECUTE ON FUNCTION account_valid_password(int8, text) TO #{app_user}"
242
+ end
243
+
244
+ down do
245
+ run "DROP FUNCTION account_valid_password(int8, text)"
246
+ drop_table(:account_password_hashes)
247
+ end
248
+ end
249
+
250
+ If you are using a non-PostgreSQL database or cannot use multiple user
251
+ accounts, just combine the two migrations into a single migration and
252
+ exclude the GRANT/REVOKE statements.
253
+
254
+ One thing to notice in the above migrations is that Rodauth uses additional
255
+ tables for additional features, instead of additional columns in a single
256
+ table.
257
+
258
+ == Usage
259
+
260
+ === Basic Usage
261
+
262
+ Rodauth is a Roda plugin and loaded the same way other Roda plugins
263
+ are loaded:
264
+
265
+ plugin :rodauth do
266
+ end
267
+
268
+ The block passed to the plugin call uses the Rodauth configuration DSL.
269
+ The one configuration method that should always be used is enable,
270
+ which chooses which features you would like to load:
271
+
272
+ plugin :rodauth do
273
+ enable :login, :logout
274
+ end
275
+
276
+ Once features are loaded, you can use any of the configuration methods
277
+ supported by the features. There are three types of configuration
278
+ methods. The first type are called auth methods, and they take a
279
+ block, and overrides the default method that Rodauth uses. Inside the
280
+ block, you can call super if you want to get the default behavior. For
281
+ example, if you want to add additional logging when a user logs in:
282
+
283
+ plugin :rodauth do
284
+ enable :login, :logout
285
+ after_login do
286
+ logger.info "#{account.email} logged in!"
287
+ super
288
+ end
289
+ end
290
+
291
+ Inside the block, you are in the context of the Rodauth::Auth
292
+ instance related to the request. This object has access to everything
293
+ related to the request via methods:
294
+
295
+ request :: RodaRequest instance
296
+ response :: RodaResponse instance
297
+ scope :: Roda instance
298
+ session :: session hash
299
+ flash :: flash message hash
300
+ account :: account model instance (if set by an earlier Rodauth method)
301
+
302
+ So if you want to log the IP address for the user during login:
303
+
304
+ plugin :rodauth do
305
+ enable :login, :logout
306
+ after_login do
307
+ logger.info "#{account.email} logged in from #{request.ip}"
308
+ super
309
+ end
310
+ end
311
+
312
+ The second type of configuration methods are called auth value
313
+ methods. They are similar to auth methods, but instead of just
314
+ accepting a block, they can optionally accept a single argument
315
+ without a block, which will be treated as a block that just returns
316
+ that value. For example, the account_model method sets the model
317
+ class to use for the account, so to override it, you can call the
318
+ method with another class:
319
+
320
+ plugin :rodauth do
321
+ enable :login, :logout
322
+ account_model User
323
+ end
324
+
325
+ The third type of configuration methods are called auth block methods,
326
+ and there are three of them per feature, one for handling the route
327
+ itself, one for handling just the GET route, and one for handling just
328
+ the POST route. For the login feature, login_route_block would set
329
+ the routing block to use if the login route matches, login_get_block
330
+ would set the routing block to use if the login route matches and it
331
+ is a GET request, and login_post_block would set the routing block to
332
+ use if the login route matches and it is a POST request. As auth block
333
+ methods specify the route blocks, they are executed in the context
334
+ of the Roda instance, and are passed two arguments, the first being the
335
+ RodaRequest instance, and the second being the Rodauth::Auth instance.
336
+ For example, if you wanted to override how a POST request to the login
337
+ route is handled:
338
+
339
+ plugin :rodauth do
340
+ enable :login
341
+ login_post_route do |r, auth|
342
+ # ...
343
+ end
344
+ end
345
+
346
+ By allowing every configuration method to take a block, Rodauth
347
+ should be flexible enough to integrate into most legacy
348
+ authentication systems.
349
+
350
+ === Feature Documentation
351
+
352
+ The options/methods for the supported features are listed on a
353
+ separate page per feature. If these links are not active, please
354
+ view the appropriate file in the doc directory.
355
+
356
+ * {Base}[rdoc-ref:doc/base.rdoc] (this feature is autoloaded)
357
+ * {Login}[rdoc-ref:doc/login.rdoc]
358
+ * {Logout}[rdoc-ref:doc/logout.rdoc]
359
+ * {Change Password}[rdoc-ref:doc/change_password.rdoc]
360
+ * {Change Login}[rdoc-ref:doc/change_login.rdoc]
361
+ * {Reset Password}[rdoc-ref:doc/reset_password.rdoc]
362
+ * {Create Account}[rdoc-ref:doc/create_account.rdoc]
363
+ * {Close Account}[rdoc-ref:doc/close_account.rdoc]
364
+ * {Verify Account}[rdoc-ref:doc/verify_account.rdoc]
365
+ * {Remember}[rdoc-ref:doc/remember.rdoc]
366
+ * {Lockout}[rdoc-ref:doc/lockout.rdoc]
367
+
368
+ Since the auth block methods work the same way for each of these
369
+ features, they are not documented on the feature pages. Additionally,
370
+ all features have a before auth method (e.g. before_login) that is
371
+ called before either the GET or POST route blocks are handled.
372
+
373
+ === With Multiple Configurations
374
+
375
+ Rodauth supports using multiple rodauth configurations in the same
376
+ application. You just need to load the plugin a second time,
377
+ providing a name for any alternate configuration:
378
+
379
+ plugin :rodauth do
380
+ end
381
+ plugin :rodauth, :name=>:secondary do
382
+ end
383
+
384
+ Then in your routing code, any time you call rodauth, you can provide
385
+ the name as an argument to use that configuration:
386
+
387
+ route do |r|
388
+ r.on 'secondary' do
389
+ r.rodauth(:secondary)
390
+ end
391
+
392
+ r.rodauth
393
+ end
394
+
395
+ === With Other Databases
396
+
397
+ You can use Rodauth with other databases besides PostgreSQL. Assuming
398
+ you are storing the password hashes in the same table as the account
399
+ information, you can just do:
400
+
401
+ plugin :rodauth do
402
+ account_password_hash_column :password_hash
403
+ end
404
+
405
+ When this option is set, Rodauth will not use a database function
406
+ to authenticate, it will do the check in ruby. This feature can
407
+ also be used if you are using PostgreSQL, but for legacy reasons
408
+ are storing the password hashes in the same table as the account
409
+ information.
410
+
411
+ The Rodauth lockout feature also uses UPDATE RETURNING to update
412
+ a row and return the new value, so if you are not using PostgreSQL
413
+ and wish to use the lockout feature, you'll need to override the
414
+ invalid_login_attempted method.
415
+
416
+ === With Other Web Frameworks
417
+
418
+ You can use Rodauth even if your application does not use the Roda web
419
+ framework. This is possible by adding a Roda middleware that uses
420
+ Rodauth:
421
+
422
+ require 'roda'
423
+
424
+ class RodauthApp < Roda
425
+ plugin :middleware
426
+ plugin :rodauth
427
+ route do |r|
428
+ r.rodauth
429
+ end
430
+ end
431
+
432
+ use RodauthApp
433
+
434
+ === Using External Features
435
+
436
+ The enable configuration method is able to load features external to
437
+ Rodauth. You need to place the external feature file where it can be
438
+ required via roda/plugins/rodauth/feature_name. That file should
439
+ use the following basic structure
440
+
441
+ class Roda
442
+ module RodaPlugins
443
+ module Rodauth
444
+ # :feature_name will be the argument given to enable to
445
+ # load the feature
446
+ FeatureName = Feature.define(:feature_name) do
447
+ auth_value_methods # one argument per auth value method
448
+ auth_methods # one argument per auth method
449
+
450
+ get_block do |r, auth|
451
+ # r is the RodaRequest instance
452
+ # auth is the Rodauth::Auth instance
453
+ # This block is evaluated in the scope of the Roda instance
454
+ # ...
455
+ end
456
+
457
+ post_block do |r, auth|
458
+ # ...
459
+ end
460
+
461
+ # define the default behavior for the auth methods
462
+ # and auth value methods
463
+ # ...
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ == Possible Future Directions
470
+
471
+ * OmniAuth support. This is not something I plan to work on myself,
472
+ but I will consider patches that add it.
473
+
474
+ == Similar Projects
475
+
476
+ All of these are Rails-specific:
477
+
478
+ * Devise
479
+ * Authlogic
480
+ * Sorcery
481
+
482
+ == Author
483
+
484
+ Jeremy Evans <code@jeremyevans.net>