rodauth-rails 0.11.0 → 0.15.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +316 -218
  4. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +8 -4
  5. data/lib/generators/rodauth/templates/app/models/account.rb +1 -0
  6. data/lib/generators/rodauth/templates/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  7. data/lib/generators/rodauth/templates/app/views/rodauth/_field.html.erb +2 -2
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_field_error.html.erb +2 -2
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +3 -3
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +3 -3
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +3 -3
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form.html.erb +3 -3
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_footer.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_login_form_header.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_login_hidden_field.html.erb +1 -1
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +3 -3
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +3 -3
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +3 -3
  20. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +3 -3
  21. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +3 -3
  22. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_codes_form.html.erb +4 -4
  23. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +3 -3
  24. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +3 -3
  25. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  26. data/lib/generators/rodauth/templates/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  27. data/lib/generators/rodauth/templates/app/views/rodauth/change_login.html.erb +3 -3
  28. data/lib/generators/rodauth/templates/app/views/rodauth/change_password.html.erb +3 -3
  29. data/lib/generators/rodauth/templates/app/views/rodauth/close_account.html.erb +2 -2
  30. data/lib/generators/rodauth/templates/app/views/rodauth/confirm_password.html.erb +1 -1
  31. data/lib/generators/rodauth/templates/app/views/rodauth/create_account.html.erb +4 -4
  32. data/lib/generators/rodauth/templates/app/views/rodauth/email_auth.html.erb +1 -1
  33. data/lib/generators/rodauth/templates/app/views/rodauth/logout.html.erb +2 -2
  34. data/lib/generators/rodauth/templates/app/views/rodauth/multi_phase_login.html.erb +1 -1
  35. data/lib/generators/rodauth/templates/app/views/rodauth/otp_auth.html.erb +1 -1
  36. data/lib/generators/rodauth/templates/app/views/rodauth/otp_disable.html.erb +2 -2
  37. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +9 -9
  38. data/lib/generators/rodauth/templates/app/views/rodauth/recovery_auth.html.erb +1 -1
  39. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +5 -5
  40. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password.html.erb +2 -2
  41. data/lib/generators/rodauth/templates/app/views/rodauth/reset_password_request.html.erb +2 -2
  42. data/lib/generators/rodauth/templates/app/views/rodauth/sms_auth.html.erb +1 -1
  43. data/lib/generators/rodauth/templates/app/views/rodauth/sms_confirm.html.erb +1 -1
  44. data/lib/generators/rodauth/templates/app/views/rodauth/sms_disable.html.erb +2 -2
  45. data/lib/generators/rodauth/templates/app/views/rodauth/sms_request.html.erb +1 -1
  46. data/lib/generators/rodauth/templates/app/views/rodauth/sms_setup.html.erb +2 -2
  47. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_auth.html.erb +1 -1
  48. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_disable.html.erb +2 -2
  49. data/lib/generators/rodauth/templates/app/views/rodauth/two_factor_manage.html.erb +6 -6
  50. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account.html.erb +2 -2
  51. data/lib/generators/rodauth/templates/app/views/rodauth/unlock_account_request.html.erb +1 -1
  52. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account.html.erb +3 -3
  53. data/lib/generators/rodauth/templates/app/views/rodauth/verify_account_resend.html.erb +2 -2
  54. data/lib/generators/rodauth/templates/app/views/rodauth/verify_login_change.html.erb +1 -1
  55. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_auth.html.erb +7 -7
  56. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +6 -6
  57. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_setup.html.erb +7 -7
  58. data/lib/generators/rodauth/views_generator.rb +26 -4
  59. data/lib/rodauth/rails.rb +32 -13
  60. data/lib/rodauth/rails/auth.rb +9 -12
  61. data/lib/rodauth/rails/feature.rb +19 -230
  62. data/lib/rodauth/rails/feature/base.rb +62 -0
  63. data/lib/rodauth/rails/feature/callbacks.rb +65 -0
  64. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  65. data/lib/rodauth/rails/feature/email.rb +30 -0
  66. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  67. data/lib/rodauth/rails/feature/internal_request.rb +50 -0
  68. data/lib/rodauth/rails/feature/render.rb +48 -0
  69. data/lib/rodauth/rails/model.rb +101 -0
  70. data/lib/rodauth/rails/model/associations.rb +195 -0
  71. data/lib/rodauth/rails/railtie.rb +0 -5
  72. data/lib/rodauth/rails/tasks.rake +5 -5
  73. data/lib/rodauth/rails/version.rb +1 -1
  74. data/rodauth-rails.gemspec +4 -1
  75. metadata +56 -6
  76. data/lib/rodauth/rails/log_subscriber.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8063be8ad00634114f74f0eb549c672e2b62cd1fa81cb7f124cc9cd12505e3f
4
- data.tar.gz: 6f466e29420f9e4bacb58c855e942cc20289d2c3fc69a12638b97628d25dbbfb
3
+ metadata.gz: '097625662e9cefbf7484ea775c9b1930968fbbc3a1b8ad24df9e229e2194e301'
4
+ data.tar.gz: fbbfe9dd849646e859aecbf3c600168fc0b65255f7b2bf933a2e42591f8b0b79
5
5
  SHA512:
6
- metadata.gz: 8cc0af59c6ce29837fbc8a3401d456fd407ef76b74493b08ee9b4f2dfc8807d4a95c86f9bb0266401013d5162c009d46b7d07e3f741654af2cc267c0ee2c135e
7
- data.tar.gz: 78c098dbaed458d5764ca2e7ee61f4710e01b2386d0cc04831b1732b9883d76c4b9f56c35c0a1e557c40951086d72bb0ed264f769313c1b45be30b2dd760024a
6
+ metadata.gz: 762d0c1725dcd0017cdd6722894e546dfdf246e245af688a8bc99f177e43765fe8bd7a79639e45d3146ca5bedadce9f34389f61bf3a6957b406cbc664cf93829
7
+ data.tar.gz: 8c6624c70668356b8434dde9bd237cced89f23d4d241b770989f1af68ee687b7186f305ada409885d619b9974e7d2d47b6fddc9faf6935653cab805ee8709167
data/CHANGELOG.md CHANGED
@@ -1,3 +1,43 @@
1
+ ## 0.15.0 (2021-07-29)
2
+
3
+ * Add `Rodauth::Rails::Model` mixin that defines password attribute and associations on the model (@janko)
4
+
5
+ * Add support for the new internal_request feature (@janko)
6
+
7
+ * Implement `Rodauth::Rails.rodauth` in terms of the internal_request feature (@janko)
8
+
9
+ ## 0.14.0 (2021-07-10)
10
+
11
+ * Speed up template rendering by only searching formats accepted by the request (@janko)
12
+
13
+ * Add `--name` option to `rodauth:views` generator for specifying different rodauth configuration (@janko)
14
+
15
+ * Infer correct template path from configured controller in `rodauth:views` generator (@janko)
16
+
17
+ * Raise `ArgumentError` if undefined rodauth configuration is passed to `Rodauth::Rails.app` (@janko)
18
+
19
+ * Make `#rails_controller` method on the rodauth instance public (@janko)
20
+
21
+ * Remove `--directory` option from `rodauth:views` generator (@janko)
22
+
23
+ * Remove `#features` and `#routes` writer and `#configuration` reader from `Rodauth::Rails::Auth` (@janko)
24
+
25
+ ## 0.13.0 (2021-06-10)
26
+
27
+ * Add `:query`, `:form`, `:session`, `:account`, and `:env` options to `Rodauth::Rails.rodauth` (@janko)
28
+
29
+ ## 0.12.0 (2021-05-15)
30
+
31
+ * Include total view render time in logs for Rodauth requests (@janko)
32
+
33
+ * Instrument redirects (@janko)
34
+
35
+ * Instrument Rodauth requests on `action_controller` namespace (@janko)
36
+
37
+ * Update templates for Boostrap 5 compatibility (@janko)
38
+
39
+ * Log request parameters for Rodauth requests (@janko)
40
+
1
41
  ## 0.11.0 (2021-05-06)
2
42
 
3
43
  * Add controller-like logging for requests to Rodauth endpoints (@janko)
data/README.md CHANGED
@@ -41,27 +41,15 @@ Active Record's database connection][sequel-activerecord_connection].
41
41
 
42
42
  ## Upgrading
43
43
 
44
- ### Upgrading to 0.7.0
45
-
46
- Starting from version 0.7.0, rodauth-rails now correctly detects Rails
47
- application's `secret_key_base` when setting default `hmac_secret`, including
48
- when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
49
- means that your authentication will now be more secure by default, and Rodauth
50
- features that require `hmac_secret` should now work automatically as well.
51
-
52
- However, if you've already been using rodauth-rails in production, where the
53
- `secret_key_base` is set via credentials or environment variable and `hmac_secret`
54
- was not explicitly set, the fact that your authentication will now start using
55
- HMACs has backwards compatibility considerations. See the [Rodauth
56
- documentation][hmac] for instructions on how to safely transition, or just set
57
- `hmac_secret nil` in your Rodauth configuration.
44
+ For instructions on upgrading from previous rodauth-rails versions, see
45
+ [UPGRADING.md](/UPGRADING.md).
58
46
 
59
47
  ## Installation
60
48
 
61
49
  Add the gem to your Gemfile:
62
50
 
63
51
  ```rb
64
- gem "rodauth-rails", "~> 0.10"
52
+ gem "rodauth-rails", "~> 0.15"
65
53
 
66
54
  # gem "jwt", require: false # for JWT feature
67
55
  # gem "rotp", require: false # for OTP feature
@@ -86,132 +74,22 @@ $ rails generate rodauth:install --jwt # token authentication via the "Authoriza
86
74
  $ bundle add jwt
87
75
  ```
88
76
 
89
- The generator will create the following files:
90
-
91
- * Rodauth migration at `db/migrate/*_create_rodauth.rb`
92
- * Rodauth initializer at `config/initializers/rodauth.rb`
93
- * Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
94
- * Rodauth app at `app/lib/rodauth_app.rb`
95
- * Rodauth controller at `app/controllers/rodauth_controller.rb`
96
- * Account model at `app/models/account.rb`
97
- * Rodauth mailer at `app/mailers/rodauth_mailer.rb` with views
98
-
99
- ### Migration
100
-
101
- The migration file creates tables required by Rodauth. You're encouraged to
102
- review the migration, and modify it to only create tables for features you
103
- intend to use.
104
-
105
- ```rb
106
- # db/migrate/*_create_rodauth.rb
107
- class CreateRodauth < ActiveRecord::Migration
108
- def change
109
- create_table :accounts do |t| ... end
110
- create_table :account_password_hashes do |t| ... end
111
- create_table :account_password_reset_keys do |t| ... end
112
- create_table :account_verification_keys do |t| ... end
113
- create_table :account_login_change_keys do |t| ... end
114
- create_table :account_remember_keys do |t| ... end
115
- end
116
- end
117
- ```
77
+ This generator will create a Rodauth app with common authentication features
78
+ enabled, a database migration with tables required by those features, a mailer
79
+ with default templates, and a few other files.
118
80
 
119
- Once you're done, you can run the migration:
81
+ Feel free to remove any features you don't need, along with their corresponding
82
+ tables. Afterwards, run the migration:
120
83
 
121
- ```
84
+ ```sh
122
85
  $ rails db:migrate
123
86
  ```
124
87
 
125
- ### Rodauth initializer
126
-
127
- The Rodauth initializer assigns the constant for your Rodauth app, which will
128
- be called by the Rack middleware that's added in front of your Rails router.
129
-
130
- ```rb
131
- # config/initializers/rodauth.rb
132
- Rodauth::Rails.configure do |config|
133
- config.app = "RodauthApp"
134
- end
135
- ```
136
-
137
- ### Sequel initializer
138
-
139
- Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
140
- an additional initializer will be created which configures Sequel to use the
141
- ActiveRecord connection.
142
-
143
- ```rb
144
- # config/initializers/sequel.rb
145
- require "sequel/core"
146
-
147
- # initialize Sequel and have it reuse Active Record's database connection
148
- DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
149
- ```
150
-
151
- ### Rodauth app
152
-
153
- Your Rodauth app is created in the `app/lib/` directory, and comes with a
154
- default set of authentication features enabled, as well as extensive examples
155
- on ways you can configure authentication behaviour.
156
-
157
- ```rb
158
- # app/lib/rodauth_app.rb
159
- class RodauthApp < Rodauth::Rails::App
160
- configure do
161
- # authentication configuration
162
- end
163
-
164
- route do |r|
165
- # request handling
166
- end
167
- end
168
- ```
169
-
170
- ### Controller
171
-
172
- Your Rodauth app will by default use `RodauthController` for view rendering,
173
- CSRF protection, and running controller callbacks and rescue handlers around
174
- Rodauth actions.
175
-
176
- ```rb
177
- # app/controllers/rodauth_controller.rb
178
- class RodauthController < ApplicationController
179
- end
180
- ```
181
-
182
- ### Account model
183
-
184
- Rodauth stores user accounts in the `accounts` table, so the generator will
185
- also create an `Account` model for custom use.
186
-
187
- ```rb
188
- # app/models/account.rb
189
- class Account < ApplicationRecord
190
- end
191
- ```
192
-
193
- ### Rodauth mailer
194
-
195
- The default Rodauth app is configured to use `RodauthMailer` mailer
196
- for sending authentication emails.
197
-
198
- ```rb
199
- # app/mailers/rodauth_mailer.rb
200
- class RodauthMailer < ApplicationMailer
201
- def verify_account(recipient, email_link) ... end
202
- def reset_password(recipient, email_link) ... end
203
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
204
- def password_changed(recipient) ... end
205
- # def email_auth(recipient, email_link) ... end
206
- # def unlock_account(recipient, email_link) ... end
207
- end
208
- ```
209
-
210
88
  ## Usage
211
89
 
212
90
  ### Routes
213
91
 
214
- We can see the list of routes our Rodauth middleware handles:
92
+ You can see the list of routes our Rodauth middleware handles:
215
93
 
216
94
  ```sh
217
95
  $ rails rodauth:routes
@@ -233,7 +111,7 @@ Routes handled by RodauthApp:
233
111
  /close-account rodauth.close_account_path
234
112
  ```
235
113
 
236
- Using this information, we could add some basic authentication links to our
114
+ Using this information, you can add some basic authentication links to your
237
115
  navigation header:
238
116
 
239
117
  ```erb
@@ -264,7 +142,7 @@ end
264
142
 
265
143
  ### Current account
266
144
 
267
- To be able to fetch currently authenticated account, let's define a
145
+ To be able to fetch currently authenticated account, you can define a
268
146
  `#current_account` method that fetches the account id from session and
269
147
  retrieves the corresponding account record:
270
148
 
@@ -281,11 +159,11 @@ class ApplicationController < ActionController::Base
281
159
  rodauth.logout
282
160
  rodauth.login_required
283
161
  end
284
- helper_method :current_account # skip if inheriting from ActionController:API
162
+ helper_method :current_account # skip if inheriting from ActionController::API
285
163
  end
286
164
  ```
287
165
 
288
- This allows us to access the current account in controllers and views:
166
+ This allows you to access the current account in controllers and views:
289
167
 
290
168
  ```erb
291
169
  <p>Authenticated as: <%= current_account.email %></p>
@@ -293,9 +171,9 @@ This allows us to access the current account in controllers and views:
293
171
 
294
172
  ### Requiring authentication
295
173
 
296
- We'll likely want to require authentication for certain parts of our app,
297
- redirecting the user to the login page if they're not logged in. We can do this
298
- in our Rodauth app's routing block, which helps keep the authentication logic
174
+ You'll likely want to require authentication for certain parts of your app,
175
+ redirecting the user to the login page if they're not logged in. You can do this
176
+ in your Rodauth app's routing block, which helps keep the authentication logic
299
177
  encapsulated:
300
178
 
301
179
  ```rb
@@ -314,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
314
192
  end
315
193
  ```
316
194
 
317
- We can also require authentication at the controller layer:
195
+ You can also require authentication at the controller layer:
318
196
 
319
197
  ```rb
320
198
  # app/controllers/application_controller.rb
@@ -341,8 +219,8 @@ end
341
219
 
342
220
  #### Routing constraints
343
221
 
344
- You can also require authentication at the Rails router level by
345
- using a built-in `authenticated` routing constraint:
222
+ In some cases it makes sense to require authentication at the Rails router
223
+ level. You can do this via the built-in `authenticated` routing constraint:
346
224
 
347
225
  ```rb
348
226
  # config/routes.rb
@@ -400,11 +278,11 @@ $ rails generate rodauth:views
400
278
  ```
401
279
 
402
280
  This will generate views for the default set of Rodauth features into the
403
- `app/views/rodauth` directory, which will be automatically picked up by the
404
- `RodauthController`.
281
+ `app/views/rodauth` directory, provided that `RodauthController` is set for the
282
+ main configuration.
405
283
 
406
284
  You can pass a list of Rodauth features to the generator to create views for
407
- these features (this will not remove any existing views):
285
+ these features (this will not remove or overwrite any existing views):
408
286
 
409
287
  ```sh
410
288
  $ rails generate rodauth:views login create_account lockout otp
@@ -416,12 +294,10 @@ Or you can generate views for all features:
416
294
  $ rails generate rodauth:views --all
417
295
  ```
418
296
 
419
- You can also tell the generator to create views into another directory (in this
420
- case make sure to rename the Rodauth controller accordingly):
297
+ Use `--name` to generate views for a different Rodauth configuration:
421
298
 
422
299
  ```sh
423
- # generates views into app/views/authentication
424
- $ rails generate rodauth:views --name authentication
300
+ $ rails generate rodauth:views --name admin
425
301
  ```
426
302
 
427
303
  #### Layout
@@ -514,14 +390,48 @@ end
514
390
  This configuration calls `#deliver_later`, which uses Active Job to deliver
515
391
  emails in a background job. It's generally recommended to send emails
516
392
  asynchronously for better request throughput and the ability to retry
517
- deliveries. However, if you want to send emails synchronously, modify the
518
- configuration to call `#deliver_now` instead.
393
+ deliveries. However, if you want to send emails synchronously, you can modify
394
+ the configuration to call `#deliver_now` instead.
519
395
 
520
396
  If you're using a background processing library without an Active Job adapter,
521
397
  or a 3rd-party service for sending transactional emails, this two-phase API
522
398
  might not be suitable. In this case, instead of overriding `#create_*_email`
523
399
  and `#send_email`, override the `#send_*_email` methods instead, which are
524
- required to send the email immediately.
400
+ required to send the email immediately. For example:
401
+
402
+ ```rb
403
+ # app/workers/rodauth_mailer_worker.rb
404
+ class RodauthMailerWorker
405
+ include Sidekiq::Worker
406
+
407
+ def perform(name, *args)
408
+ email = RodauthMailer.public_send(name, *args)
409
+ email.deliver_now
410
+ end
411
+ end
412
+ ```
413
+ ```rb
414
+ # app/lib/rodauth_app.rb
415
+ class RodauthApp < Rodauth::Rails::App
416
+ configure do
417
+ # ...
418
+ # use `#send_*_email` method to be able to immediately enqueue email delivery
419
+ send_reset_password_email do
420
+ enqueue_email(:reset_password, email_to, reset_password_email_link)
421
+ end
422
+ # ...
423
+ auth_class_eval do
424
+ # custom method for enqueuing email delivery using our worker
425
+ def enqueue_email(name, *args)
426
+ db.after_commit do
427
+ RodauthMailerWorker.perform_async(name, *args)
428
+ end
429
+ end
430
+ end
431
+ # ...
432
+ end
433
+ end
434
+ ```
525
435
 
526
436
  ### Migrations
527
437
 
@@ -543,10 +453,134 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
543
453
  end
544
454
  ```
545
455
 
456
+ ### Model
457
+
458
+ The `Rodauth::Rails::Model` mixin can be included into the account model, which
459
+ defines a password attribute and associations for tables used by enabled
460
+ authentication features.
461
+
462
+ ```rb
463
+ class Account < ApplicationRecord
464
+ include Rodauth::Rails.model # or `Rodauth::Rails.model(:admin)`
465
+ end
466
+ ```
467
+
468
+ #### Password attribute
469
+
470
+ Regardless of whether you're storing the password hash in a column in the
471
+ accounts table, or in a separate table, the `#password` attribute can be used
472
+ to set or clear the password hash.
473
+
474
+ ```rb
475
+ account = Account.create!(email: "user@example.com", password: "secret")
476
+
477
+ # when password hash is stored in a column on the accounts table
478
+ account.password_hash #=> "$2a$12$k/Ub1I2iomi84RacqY89Hu4.M0vK7klRnRtzorDyvOkVI.hKhkNw."
479
+
480
+ # when password hash is stored in a separate table
481
+ account.password_hash #=> #<Account::PasswordHash...> (record from `account_password_hashes` table)
482
+ account.password_hash.password_hash #=> "$2a$12$k/Ub1..." (inaccessible when using database authentication functions)
483
+
484
+ account.password = nil # clears password hash
485
+ account.password_hash #=> nil
486
+ ```
487
+
488
+ Note that the password attribute doesn't come with validations, making it
489
+ unsuitable for forms. It was primarily intended to allow easily creating
490
+ accounts in development console and in tests.
491
+
492
+ #### Associations
493
+
494
+ The `Rodauth::Rails::Model` mixin defines associations for Rodauth tables
495
+ associated to the accounts table:
496
+
497
+ ```rb
498
+ account.remember_key #=> #<Account::RememberKey> (record from `account_remember_keys` table)
499
+ account.active_session_keys #=> [#<Account::ActiveSessionKey>,...] (records from `account_active_session_keys` table)
500
+ ```
501
+
502
+ You can also reference the associated models directly:
503
+
504
+ ```rb
505
+ # model referencing the `account_authentication_audit_logs` table
506
+ Account::AuthenticationAuditLog.where(message: "login").group(:account_id)
507
+ ```
508
+
509
+ The associated models define the inverse `belongs_to :account` association:
510
+
511
+ ```rb
512
+ Account::ActiveSessionKey.includes(:account).map(&:account)
513
+ ```
514
+
515
+ Here is an example of using associations to create a method that returns
516
+ whether the account has multifactor authentication enabled:
517
+
518
+ ```rb
519
+ class Account < ApplicationRecord
520
+ include Rodauth::Rails.model
521
+
522
+ def mfa_enabled?
523
+ otp_key || (sms_code && sms_code.num_failures.nil?) || recovery_codes.any?
524
+ end
525
+ end
526
+ ```
527
+
528
+ Here is another example of creating a query scope that selects accounts with
529
+ multifactor authentication enabled:
530
+
531
+ ```rb
532
+ class Account < ApplicationRecord
533
+ include Rodauth::Rails.model
534
+
535
+ scope :otp_setup, -> { where(otp_key: OtpKey.all) }
536
+ scope :sms_codes_setup, -> { where(sms_code: SmsCode.where(num_failures: nil)) }
537
+ scope :recovery_codes_setup, -> { where(recovery_codes: RecoveryCode.all) }
538
+ scope :mfa_enabled, -> { merge(otp_setup.or(sms_codes_setup).or(recovery_codes_setup)) }
539
+ end
540
+ ```
541
+
542
+ Below is a list of all associations defined depending on the features loaded:
543
+
544
+ | Feature | Association | Type | Model | Table (default) |
545
+ | :------ | :---------- | :--- | :---- | :---- |
546
+ | account_expiration | `:activity_time` | `has_one` | `ActivityTime` | `account_activity_times` |
547
+ | active_sessions | `:active_session_keys` | `has_many` | `ActiveSessionKey` | `account_active_session_keys` |
548
+ | audit_logging | `:authentication_audit_logs` | `has_many` | `AuthenticationAuditLog` | `account_authentication_audit_logs` |
549
+ | disallow_password_reuse | `:previous_password_hashes` | `has_many` | `PreviousPasswordHash` | `account_previous_password_hashes` |
550
+ | email_auth | `:email_auth_key` | `has_one` | `EmailAuthKey` | `account_email_auth_keys` |
551
+ | jwt_refresh | `:jwt_refresh_keys` | `has_many` | `JwtRefreshKey` | `account_jwt_refresh_keys` |
552
+ | lockout | `:lockout` | `has_one` | `Lockout` | `account_lockouts` |
553
+ | lockout | `:login_failure` | `has_one` | `LoginFailure` | `account_login_failures` |
554
+ | otp | `:otp_key` | `has_one` | `OtpKey` | `account_otp_keys` |
555
+ | password_expiration | `:password_change_time` | `has_one` | `PasswordChangeTime` | `account_password_change_times` |
556
+ | recovery_codes | `:recovery_codes` | `has_many` | `RecoveryCode` | `account_recovery_codes` |
557
+ | remember | `:remember_key` | `has_one` | `RememberKey` | `account_remember_keys` |
558
+ | reset_password | `:password_reset_key` | `has_one` | `PasswordResetKey` | `account_password_reset_keys` |
559
+ | single_session | `:session_key` | `has_one` | `SessionKey` | `account_session_keys` |
560
+ | sms_codes | `:sms_code` | `has_one` | `SmsCode` | `account_sms_codes` |
561
+ | verify_account | `:verification_key` | `has_one` | `VerificationKey` | `account_verification_keys` |
562
+ | verify_login_change | `:login_change_key` | `has_one` | `LoginChangeKey` | `account_login_change_keys` |
563
+ | webauthn | `:webauthn_keys` | `has_many` | `WebauthnKey` | `account_webauthn_keys` |
564
+ | webauthn | `:webauthn_user_id` | `has_one` | `WebauthnUserId` | `account_webauthn_user_ids` |
565
+
566
+ By default, all associations except for audit logs have `dependent: :destroy`
567
+ set, to allow for easy deletion of account records in the console. You can use
568
+ `:association_options` to modify global or per-association options:
569
+
570
+ ```rb
571
+ # don't auto-delete associations when account model is deleted
572
+ Rodauth::Rails.model(association_options: { dependent: nil })
573
+
574
+ # require authentication audit logs to be eager loaded before retrieval
575
+ Rodauth::Rails.model(association_options: -> (name) {
576
+ { strict_loading: true } if name == :authentication_audit_logs
577
+ })
578
+ ```
579
+
546
580
  ### Multiple configurations
547
581
 
548
582
  If you need to handle multiple types of accounts that require different
549
- authentication logic, you can create different configurations for them:
583
+ authentication logic, you can create additional configurations for them:
550
584
 
551
585
  ```rb
552
586
  # app/lib/rodauth_app.rb
@@ -562,10 +596,6 @@ class RodauthApp < Rodauth::Rails::App
562
596
  prefix "/admin"
563
597
  session_key_prefix "admin_"
564
598
  remember_cookie_key "_admin_remember" # if using remember feature
565
-
566
- # if you want separate tables
567
- accounts_table :admin_accounts
568
- password_hash_table :admin_account_password_hashes
569
599
  # ...
570
600
  end
571
601
 
@@ -574,7 +604,7 @@ class RodauthApp < Rodauth::Rails::App
574
604
 
575
605
  r.on "admin" do
576
606
  r.rodauth(:admin)
577
- r.pass # allow the Rails app to handle other "/admin/*" requests
607
+ break # allow routing of other /admin/* requests to continue to Rails
578
608
  end
579
609
 
580
610
  # ...
@@ -588,6 +618,50 @@ Then in your application you can reference the secondary Rodauth instance:
588
618
  rodauth(:admin).login_path #=> "/admin/login"
589
619
  ```
590
620
 
621
+ You'll likely want to save the information of which account belongs to which
622
+ configuration to the database. One way would be to have a separate table that
623
+ stores account types:
624
+
625
+ ```sh
626
+ $ rails generate migration create_account_types
627
+ ```
628
+ ```rb
629
+ # db/migrate/*_create_account_types.rb
630
+ class CreateAccountTypes < ActiveRecord::Migration
631
+ def change
632
+ create_table :account_types do |t|
633
+ t.references :account, foreign_key: { on_delete: :cascade }, null: false
634
+ t.string :type, null: false
635
+ end
636
+ end
637
+ end
638
+ ```
639
+ ```sh
640
+ $ rails db:migrate
641
+ ```
642
+
643
+ Then an entry would be inserted after account creation, and optionally whenever
644
+ Rodauth retrieves accounts you could filter only those belonging to the current
645
+ configuration:
646
+
647
+ ```rb
648
+ # app/lib/rodauth_app.rb
649
+ class RodauthApp < Rodauth::Rails::App
650
+ configure(:admin) do
651
+ # ...
652
+ after_create_account do
653
+ db[:account_types].insert(account_id: account_id, type: "admin")
654
+ end
655
+ auth_class_eval do
656
+ def account_ds(*)
657
+ super.join(:account_types, account_id: :id).where(type: "admin")
658
+ end
659
+ end
660
+ # ...
661
+ end
662
+ end
663
+ ```
664
+
591
665
  #### Named auth classes
592
666
 
593
667
  A `configure` block inside `Rodauth::Rails::App` will internally create an
@@ -656,8 +730,8 @@ class RodauthAdmin < RodauthBase # inherit common settings
656
730
  end
657
731
  ```
658
732
 
659
- Another benefit is that you can define custom methods directly on the class
660
- instead of through `auth_class_eval`:
733
+ Another benefit of explicit classes is that you can define custom methods
734
+ directly at the class level instead of inside an `auth_class_eval`:
661
735
 
662
736
  ```rb
663
737
  # app/lib/rodauth_admin.rb
@@ -706,24 +780,54 @@ class RodauthApp < Rodauth::Rails::App
706
780
  end
707
781
  ```
708
782
 
709
- ### Rodauth instance
783
+ ### Outside of a request
784
+
785
+ In some cases you might need to use Rodauth more programmatically. If you would
786
+ like to perform Rodauth operations outside of request context, Rodauth ships
787
+ with the [internal_request] feature just for that. The rodauth-rails gem
788
+ additionally updates the internal rack env hash with your
789
+ `config.action_mailer.default_url_options`, which is used for generating URLs.
710
790
 
711
- In some cases you might need to use Rodauth more programmatically, and perform
712
- Rodauth operations outside of the request context. rodauth-rails gives you the
713
- ability to retrieve the Rodauth instance:
791
+ If you need to access Rodauth methods not exposed as internal requests, you can
792
+ use `Rodauth::Rails.rodauth` to retrieve the Rodauth instance used by the
793
+ internal_request feature:
714
794
 
715
795
  ```rb
716
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
796
+ # app/lib/rodauth_app.rb
797
+ class RodauthApp < Rodauth::Rails::App
798
+ configure do
799
+ enable :internal_request # this is required
800
+ end
801
+ end
802
+ ```
803
+ ```rb
804
+ account = Account.find_by!(email: "user@example.com")
805
+ rodauth = Rodauth::Rails.rodauth(account: account)
806
+
807
+ rodauth.compute_hmac("token") #=> "TpEJTKfKwqYvIDKWsuZhkhKlhaBXtR1aodskBAflD8U"
808
+ rodauth.open_account? #=> true
809
+ rodauth.two_factor_authentication_setup? #=> true
810
+ rodauth.password_meets_requirements?("foo") #=> false
811
+ rodauth.locked_out? #=> false
812
+ ```
813
+
814
+ In addition to the `:account` option, the `Rodauth::Rails.rodauth`
815
+ method accepts any options supported by the internal_request feature.
717
816
 
718
- rodauth.login_url #=> "https://example.com/login"
719
- rodauth.account_from_login("user@example.com") # loads user by email
720
- rodauth.password_match?("secret") #=> true
721
- rodauth.setup_account_verification
722
- rodauth.close_account
817
+ ```rb
818
+ Rodauth::Rails.rodauth(
819
+ env: { "HTTP_USER_AGENT" => "programmatic" },
820
+ session: { two_factor_auth_setup: true },
821
+ params: { "param" => "value" }
822
+ )
723
823
  ```
724
824
 
725
- This Rodauth instance will be initialized with basic Rack env that allows is it
726
- to generate URLs, using `config.action_mailer.default_url_options` options.
825
+ Secondary Rodauth configurations are specified by passing the configuration
826
+ name:
827
+
828
+ ```rb
829
+ Rodauth::Rails.rodauth(:admin)
830
+ ```
727
831
 
728
832
  ## How it works
729
833
 
@@ -834,7 +938,7 @@ class RodauthApp < Rodauth::Rails::App
834
938
  configure do
835
939
  # ...
836
940
  enable :json
837
- only_json? true # accept only JSON requests
941
+ only_json? true # accept only JSON requests (optional)
838
942
  # ...
839
943
  end
840
944
  end
@@ -855,27 +959,29 @@ class RodauthApp < Rodauth::Rails::App
855
959
  # ...
856
960
  enable :jwt
857
961
  jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
858
- only_json? true # accept only JSON requests
962
+ only_json? true # accept only JSON requests (optional)
859
963
  # ...
860
964
  end
861
965
  end
862
966
  ```
863
967
 
864
- If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
865
- corresponding Rodauth features and create the necessary tables:
968
+ The JWT token will be returned after each request to Rodauth routes. To also
969
+ return the JWT token on requests to your app's routes, you can add the
970
+ following code to your base controller:
866
971
 
867
- ```sh
868
- $ rails generate rodauth:migration jwt_refresh
869
- $ rails db:migrate
870
- ```
871
972
  ```rb
872
- # app/lib/rodauth_app.rb
873
- class RodauthApp < Rodauth::Rails::App
874
- configure do
875
- # ...
876
- enable :jwt, :jwt_cors, :jwt_refresh
877
- # ...
973
+ class ApplicationController < ActionController::Base
974
+ # ...
975
+ after_action :set_jwt_token
976
+
977
+ private
978
+
979
+ def set_jwt_token
980
+ if rodauth.use_jwt? && rodauth.valid_jwt?
981
+ response.headers["Authorization"] = rodauth.session_jwt
982
+ end
878
983
  end
984
+ # ...
879
985
  end
880
986
  ```
881
987
 
@@ -935,7 +1041,8 @@ end
935
1041
  <%= link_to "Login via Facebook", "/auth/facebook" %>
936
1042
  ```
937
1043
 
938
- Let's implement the OmniAuth callback endpoint on our Rodauth controller:
1044
+ Finally, let's implement the OmniAuth callback endpoint on our Rodauth
1045
+ controller:
939
1046
 
940
1047
  ```rb
941
1048
  # config/routes.rb
@@ -988,11 +1095,8 @@ end
988
1095
 
989
1096
  ## Configuring
990
1097
 
991
- For the list of configuration methods provided by Rodauth, see the [feature
992
- documentation].
993
-
994
- The `rails` feature rodauth-rails loads is customizable as well, here is the
995
- list of its configuration methods:
1098
+ The `rails` feature rodauth-rails loads provides the following configuration
1099
+ methods:
996
1100
 
997
1101
  | Name | Description |
998
1102
  | :---- | :---------- |
@@ -1019,12 +1123,16 @@ Rodauth::Rails.configure do |config|
1019
1123
  end
1020
1124
  ```
1021
1125
 
1126
+ For the list of configuration methods provided by Rodauth, see the [feature
1127
+ documentation].
1128
+
1022
1129
  ## Custom extensions
1023
1130
 
1024
1131
  When developing custom extensions for Rodauth inside your Rails project, it's
1025
- better to use plain modules (at least in the beginning), because Rodauth
1026
- feature design doesn't yet support Zeitwerk reloading well. Here is
1027
- an example of an LDAP authentication extension that uses the
1132
+ probably better to use plain modules, at least in the beginning, as Rodauth
1133
+ feature design doesn't yet work well with Zeitwerk reloading.
1134
+
1135
+ Here is an example of an LDAP authentication extension that uses the
1028
1136
  [simple_ldap_authenticator] gem.
1029
1137
 
1030
1138
  ```rb
@@ -1181,29 +1289,6 @@ Rails.application.configure do |config|
1181
1289
  end
1182
1290
  ```
1183
1291
 
1184
- If you need to create an account record with a password directly, you can do it
1185
- as follows:
1186
-
1187
- ```rb
1188
- # app/models/account.rb
1189
- class Account < ApplicationRecord
1190
- has_one :password_hash, foreign_key: :id
1191
- end
1192
- ```
1193
- ```rb
1194
- # app/models/account/password_hash.rb
1195
- class Account::PasswordHash < ApplicationRecord
1196
- belongs_to :account, foreign_key: :id
1197
- end
1198
- ```
1199
- ```rb
1200
- require "bcrypt"
1201
-
1202
- account = Account.create!(email: "user@example.com", status: "verified")
1203
- password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1204
- account.create_password_hash!(id: account.id, password_hash: password_hash)
1205
- ```
1206
-
1207
1292
  ## Rodauth defaults
1208
1293
 
1209
1294
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -1284,6 +1369,18 @@ configure do
1284
1369
  end
1285
1370
  ```
1286
1371
 
1372
+ ### Deadline values
1373
+
1374
+ To simplify changes to the database schema, rodauth-rails configures Rodauth
1375
+ to set deadline values for various features in Ruby, instead of relying on
1376
+ the database to set default column values.
1377
+
1378
+ You can easily change this back:
1379
+
1380
+ ```rb
1381
+ set_deadline_values? false
1382
+ ```
1383
+
1287
1384
  ## License
1288
1385
 
1289
1386
  The gem is available as open source under the terms of the [MIT
@@ -1325,3 +1422,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
1325
1422
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1326
1423
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1327
1424
  [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
1425
+ [internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.html