rodauth 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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>