rodauth-rails 0.8.2 → 0.12.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 +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +453 -223
  4. data/lib/generators/rodauth/install_generator.rb +26 -15
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +50 -49
  7. data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  20. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
  21. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
  22. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
  23. data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
  24. data/lib/rodauth/rails.rb +20 -0
  25. data/lib/rodauth/rails/app.rb +23 -31
  26. data/lib/rodauth/rails/app/flash.rb +7 -11
  27. data/lib/rodauth/rails/app/middleware.rb +20 -10
  28. data/lib/rodauth/rails/auth.rb +40 -0
  29. data/lib/rodauth/rails/controller_methods.rb +1 -5
  30. data/lib/rodauth/rails/feature.rb +17 -202
  31. data/lib/rodauth/rails/feature/base.rb +62 -0
  32. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  33. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  34. data/lib/rodauth/rails/feature/email.rb +30 -0
  35. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  36. data/lib/rodauth/rails/feature/render.rb +41 -0
  37. data/lib/rodauth/rails/version.rb +1 -1
  38. data/rodauth-rails.gemspec +1 -1
  39. metadata +15 -9
  40. data/lib/generators/rodauth/mailer_generator.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40b75a97a14021dafe773b2585d66d6ceef7498dddcef146498721bf39e57426
4
- data.tar.gz: 270bac846036bfd32945e71116e6816db4d51ec752ec6ff354be8f38dd206da9
3
+ metadata.gz: 27d48e6bf86cf81b33f6b0282048c2fb6f16ec6602136e18de6ede5120cfd808
4
+ data.tar.gz: 2f79498ff25a42131a5ead77f3d4adf05152bc85f271c8b985f0f9fa8c04b503
5
5
  SHA512:
6
- metadata.gz: 46e413275b4aca41959f36fb21d5fa3fd55eb6c73f87c6eda42a6eb8e36c5886913ad01e6e80394923f38236810aad4d608064f1f51661367798e8da70a9fd11
7
- data.tar.gz: 22779086e32ced89a113d33e6b123c64f7f6094e1e41d0b99a741d38c3bbfc3ed64a0894d5b656754717cd11b50c83cd541039c5a8d094ffa3e37e1ed7f592ff
6
+ metadata.gz: 8a0c44b54d304d4dfb2a205d41a5ac360e483209229fa49e767f9eaa595434b291661e283110f3ee39a8fbc17a4ad2d82f90a6e4545ca4112852ee50a35aa8da
7
+ data.tar.gz: 52bb16489dd97777f7ff2359be9014a2c55c7537b8d4449621eb95ef3b7f0030febcd06caa811d406db1fb24fcc884d22c7460a36a94255133ce261a2bbeb68d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,55 @@
1
+ ## 0.12.0 (2021-05-15)
2
+
3
+ * Include total view render time in logs for Rodauth requests (@janko)
4
+
5
+ * Instrument redirects (@janko)
6
+
7
+ * Instrument Rodauth requests on `action_controller` namespace (@janko)
8
+
9
+ * Update templates for Boostrap 5 compatibility (@janko)
10
+
11
+ * Log request parameters for Rodauth requests (@janko)
12
+
13
+ ## 0.11.0 (2021-05-06)
14
+
15
+ * Add controller-like logging for requests to Rodauth endpoints (@janko)
16
+
17
+ * Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
18
+
19
+ * Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
20
+
21
+ ## 0.10.0 (2021-03-23)
22
+
23
+ * Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
24
+
25
+ * Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
26
+
27
+ * Improve Roda middleware inspect output (@janko)
28
+
29
+ * Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
30
+
31
+ * Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
32
+
33
+ * Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
34
+
35
+ ## 0.9.1 (2021-02-10)
36
+
37
+ * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
38
+
39
+ * Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
40
+
41
+ ## 0.9.0 (2021-02-07)
42
+
43
+ * Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
44
+
45
+ * Bump Rodauth dependency to 2.9+ (@janko)
46
+
47
+ * Add `--json` option for `rodauth:install` generator for configuring `json` feature (@janko)
48
+
49
+ * Add `--jwt` option for `rodauth:install` generator for configuring `jwt` feature (@janko)
50
+
51
+ * Remove the `--api` option from `rodauth:install` generator (@janko)
52
+
1
53
  ## 0.8.2 (2021-01-10)
2
54
 
3
55
  * Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
data/README.md CHANGED
@@ -14,15 +14,16 @@ Articles:
14
14
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
15
  * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
16
  * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+ * [How to build an OIDC provider using rodauth-oauth on Rails](https://honeyryderchuck.gitlab.io/httpx/2021/03/15/oidc-provider-on-rails-using-rodauth-oauth.html)
17
18
 
18
19
  ## Why Rodauth?
19
20
 
20
21
  There are already several popular authentication solutions for Rails (Devise,
21
- Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Well, because
22
- it has many advantages over the mentioned alternatives:
22
+ Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Here are some
23
+ of the advantages that stand out for me:
23
24
 
24
25
  * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
25
- * standardized [JSON API support][jwt] (for every feature)
26
+ * standardized [JSON API support][json] for every feature (including [JWT][jwt])
26
27
  * enterprise security features ([password complexity][password_complexity], [disallow password reuse][disallow_password_reuse], [password expiration][password_expiration], [session expiration][session_expiration], [single session][single_session], [account expiration][account_expiration])
27
28
  * [email authentication][email_auth] (aka "passwordless")
28
29
  * [audit logging][audit_logging] (for any action)
@@ -32,6 +33,12 @@ it has many advantages over the mentioned alternatives:
32
33
  * consistent before/after hooks around everything
33
34
  * dedicated object encapsulating all authentication logic
34
35
 
36
+ One commmon concern is the fact that, unlike most other authentication
37
+ frameworks for Rails, Rodauth uses [Sequel] for database interaction instead of
38
+ Active Record. There are good reasons for this, and to make Rodauth work
39
+ smoothly alongside Active Record, rodauth-rails configures Sequel to [reuse
40
+ Active Record's database connection][sequel-activerecord_connection].
41
+
35
42
  ## Upgrading
36
43
 
37
44
  ### Upgrading to 0.7.0
@@ -54,7 +61,7 @@ documentation][hmac] for instructions on how to safely transition, or just set
54
61
  Add the gem to your Gemfile:
55
62
 
56
63
  ```rb
57
- gem "rodauth-rails", "~> 0.6"
64
+ gem "rodauth-rails", "~> 0.12"
58
65
 
59
66
  # gem "jwt", require: false # for JWT feature
60
67
  # gem "rotp", require: false # for OTP feature
@@ -73,118 +80,28 @@ $ rails generate rodauth:install
73
80
  Or if you want Rodauth endpoints to be exposed via JSON API:
74
81
 
75
82
  ```sh
76
- $ rails generate rodauth:install --api
83
+ $ rails generate rodauth:install --json # regular authentication using the Rails session
84
+ # or
85
+ $ rails generate rodauth:install --jwt # token authentication via the "Authorization" header
77
86
  $ bundle add jwt
78
87
  ```
79
88
 
80
- The generator will create the following files:
81
-
82
- * Rodauth migration at `db/migrate/*_create_rodauth.rb`
83
- * Rodauth initializer at `config/initializers/rodauth.rb`
84
- * Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
85
- * Rodauth app at `app/lib/rodauth_app.rb`
86
- * Rodauth controller at `app/controllers/rodauth_controller.rb`
87
- * Account model at `app/models/account.rb`
88
-
89
- ### Migration
90
-
91
- The migration file creates tables required by Rodauth. You're encouraged to
92
- review the migration, and modify it to only create tables for features you
93
- intend to use.
94
-
95
- ```rb
96
- # db/migrate/*_create_rodauth.rb
97
- class CreateRodauth < ActiveRecord::Migration
98
- def change
99
- create_table :accounts do |t| ... end
100
- create_table :account_password_hashes do |t| ... end
101
- create_table :account_password_reset_keys do |t| ... end
102
- create_table :account_verification_keys do |t| ... end
103
- create_table :account_login_change_keys do |t| ... end
104
- create_table :account_remember_keys do |t| ... end
105
- end
106
- end
107
- ```
89
+ This generator will create a Rodauth app with common authentication features
90
+ enabled, a database migration with tables required by those features, a mailer
91
+ with default templates, and a few other files.
108
92
 
109
- Once you're done, you can run the migration:
93
+ Feel free to remove any features you don't need, along with their corresponding
94
+ tables. Afterwards, run the migration:
110
95
 
111
- ```
96
+ ```sh
112
97
  $ rails db:migrate
113
98
  ```
114
99
 
115
- ### Rodauth initializer
116
-
117
- The Rodauth initializer assigns the constant for your Rodauth app, which will
118
- be called by the Rack middleware that's added in front of your Rails router.
119
-
120
- ```rb
121
- # config/initializers/rodauth.rb
122
- Rodauth::Rails.configure do |config|
123
- config.app = "RodauthApp"
124
- end
125
- ```
126
-
127
- ### Sequel initializer
128
-
129
- Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
130
- an additional initializer will be created which configures Sequel to use the
131
- ActiveRecord connection.
132
-
133
- ```rb
134
- # config/initializers/sequel.rb
135
- require "sequel/core"
136
-
137
- # initialize Sequel and have it reuse Active Record's database connection
138
- DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
139
- ```
140
-
141
- ### Rodauth app
142
-
143
- Your Rodauth app is created in the `app/lib/` directory, and comes with a
144
- default set of authentication features enabled, as well as extensive examples
145
- on ways you can configure authentication behaviour.
146
-
147
- ```rb
148
- # app/lib/rodauth_app.rb
149
- class RodauthApp < Rodauth::Rails::App
150
- configure do
151
- # authentication configuration
152
- end
153
-
154
- route do |r|
155
- # request handling
156
- end
157
- end
158
- ```
159
-
160
- ### Controller
161
-
162
- Your Rodauth app will by default use `RodauthController` for view rendering,
163
- CSRF protection, and running controller callbacks and rescue handlers around
164
- Rodauth actions.
165
-
166
- ```rb
167
- # app/controllers/rodauth_controller.rb
168
- class RodauthController < ApplicationController
169
- end
170
- ```
171
-
172
- ### Account model
173
-
174
- Rodauth stores user accounts in the `accounts` table, so the generator will
175
- also create an `Account` model for custom use.
176
-
177
- ```rb
178
- # app/models/account.rb
179
- class Account < ApplicationRecord
180
- end
181
- ```
182
-
183
100
  ## Usage
184
101
 
185
102
  ### Routes
186
103
 
187
- We can see the list of routes our Rodauth middleware handles:
104
+ You can see the list of routes our Rodauth middleware handles:
188
105
 
189
106
  ```sh
190
107
  $ rails rodauth:routes
@@ -206,7 +123,7 @@ Routes handled by RodauthApp:
206
123
  /close-account rodauth.close_account_path
207
124
  ```
208
125
 
209
- Using this information, we could add some basic authentication links to our
126
+ Using this information, you can add some basic authentication links to your
210
127
  navigation header:
211
128
 
212
129
  ```erb
@@ -222,9 +139,22 @@ These routes are fully functional, feel free to visit them and interact with the
222
139
  pages. The templates that ship with Rodauth aim to provide a complete
223
140
  authentication experience, and the forms use [Bootstrap] markup.
224
141
 
142
+ Inside Rodauth configuration and the `route` block you can access Rails route
143
+ helpers through `#rails_routes`:
144
+
145
+ ```rb
146
+ class RodauthApp < Rodauth::Rails::App
147
+ configure do
148
+ # ...
149
+ login_redirect { rails_routes.activity_path }
150
+ # ...
151
+ end
152
+ end
153
+ ```
154
+
225
155
  ### Current account
226
156
 
227
- To be able to fetch currently authenticated account, let's define a
157
+ To be able to fetch currently authenticated account, you can define a
228
158
  `#current_account` method that fetches the account id from session and
229
159
  retrieves the corresponding account record:
230
160
 
@@ -241,11 +171,11 @@ class ApplicationController < ActionController::Base
241
171
  rodauth.logout
242
172
  rodauth.login_required
243
173
  end
244
- helper_method :current_account
174
+ helper_method :current_account # skip if inheriting from ActionController::API
245
175
  end
246
176
  ```
247
177
 
248
- This allows us to access the current account in controllers and views:
178
+ This allows you to access the current account in controllers and views:
249
179
 
250
180
  ```erb
251
181
  <p>Authenticated as: <%= current_account.email %></p>
@@ -253,9 +183,9 @@ This allows us to access the current account in controllers and views:
253
183
 
254
184
  ### Requiring authentication
255
185
 
256
- We'll likely want to require authentication for certain parts of our app,
257
- redirecting the user to the login page if they're not logged in. We can do this
258
- in our Rodauth app's routing block, which helps keep the authentication logic
186
+ You'll likely want to require authentication for certain parts of your app,
187
+ redirecting the user to the login page if they're not logged in. You can do this
188
+ in your Rodauth app's routing block, which helps keep the authentication logic
259
189
  encapsulated:
260
190
 
261
191
  ```rb
@@ -274,7 +204,7 @@ class RodauthApp < Rodauth::Rails::App
274
204
  end
275
205
  ```
276
206
 
277
- We can also require authentication at the controller layer:
207
+ You can also require authentication at the controller layer:
278
208
 
279
209
  ```rb
280
210
  # app/controllers/application_controller.rb
@@ -299,15 +229,52 @@ class PostsController < ApplicationController
299
229
  end
300
230
  ```
301
231
 
302
- Or at the Rails router level:
232
+ #### Routing constraints
233
+
234
+ In some cases it makes sense to require authentication at the Rails router
235
+ level. You can do this via the built-in `authenticated` routing constraint:
303
236
 
304
237
  ```rb
305
238
  # config/routes.rb
306
239
  Rails.application.routes.draw do
307
- constraints -> (r) { r.env["rodauth"].require_authentication } do
308
- namespace :admin do
309
- # ...
310
- end
240
+ constraints Rodauth::Rails.authenticated do
241
+ # ... authenticated routes ...
242
+ end
243
+ end
244
+ ```
245
+
246
+ If you want additional conditions, you can pass in a block, which is
247
+ called with the Rodauth instance:
248
+
249
+ ```rb
250
+ # config/routes.rb
251
+ Rails.application.routes.draw do
252
+ # require multifactor authentication to be setup
253
+ constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
254
+ # ...
255
+ end
256
+ end
257
+ ```
258
+
259
+ You can specify the Rodauth configuration by passing the configuration name:
260
+
261
+ ```rb
262
+ # config/routes.rb
263
+ Rails.application.routes.draw do
264
+ constraints Rodauth::Rails.authenticated(:admin) do
265
+ # ...
266
+ end
267
+ end
268
+ ```
269
+
270
+ If you need something more custom, you can always create the routing constraint
271
+ manually:
272
+
273
+ ```rb
274
+ # config/routes.rb
275
+ Rails.application.routes.draw do
276
+ constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
277
+ # routes when the user is not logged in
311
278
  end
312
279
  end
313
280
  ```
@@ -327,7 +294,7 @@ This will generate views for the default set of Rodauth features into the
327
294
  `RodauthController`.
328
295
 
329
296
  You can pass a list of Rodauth features to the generator to create views for
330
- these features (this will not remove any existing views):
297
+ these features (this will not remove or overwrite any existing views):
331
298
 
332
299
  ```sh
333
300
  $ rails generate rodauth:views login create_account lockout otp
@@ -375,58 +342,36 @@ end
375
342
 
376
343
  ### Mailer
377
344
 
378
- Depending on the features you've enabled, Rodauth may send emails as part of
379
- the authentication flow. Most email settings can be customized:
345
+ The install generator will create `RodauthMailer` with default email templates,
346
+ and configure Rodauth features that send emails as part of the authentication
347
+ flow to use it.
380
348
 
381
349
  ```rb
382
- # app/lib/rodauth_app.rb
383
- class RodauthApp < Rodauth::Rails::App
384
- # ...
385
- configure do
350
+ # app/mailers/rodauth_mailer.rb
351
+ class RodauthMailer < ApplicationMailer
352
+ def verify_account(recipient, email_link)
386
353
  # ...
387
- # general settings
388
- email_from "no-reply@myapp.com"
389
- email_subject_prefix "[MyApp] "
390
- send_email(&:deliver_later)
354
+ end
355
+ def reset_password(recipient, email_link)
391
356
  # ...
392
- # feature settings
393
- verify_account_email_subject "Verify your account"
394
- verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
357
+ end
358
+ def verify_login_change(recipient, old_login, new_login, email_link)
395
359
  # ...
396
360
  end
361
+ def password_changed(recipient)
362
+ # ...
363
+ end
364
+ # def email_auth(recipient, email_link)
365
+ # ...
366
+ # end
367
+ # def unlock_account(recipient, email_link)
368
+ # ...
369
+ # end
397
370
  end
398
371
  ```
399
-
400
- This is convenient when starting out, but eventually you might want to use your
401
- own mailer. You can start by running the following command:
402
-
403
- ```sh
404
- $ rails generate rodauth:mailer
405
- ```
406
-
407
- This will create a `RodauthMailer` with the associated mailer views in
408
- `app/views/rodauth_mailer` directory:
409
-
410
- ```rb
411
- # app/mailers/rodauth_mailer.rb
412
- class RodauthMailer < ApplicationMailer
413
- def verify_account(recipient, email_link) ... end
414
- def reset_password(recipient, email_link) ... end
415
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
416
- def password_changed(recipient) ... end
417
- # def email_auth(recipient, email_link) ... end
418
- # def unlock_account(recipient, email_link) ... end
419
- end
420
- ```
421
-
422
- You can then uncomment the lines in your Rodauth configuration to have it call
423
- your mailer. If you've enabled additional authentication features that send
424
- emails, make sure to override their `create_*_email` methods as well.
425
-
426
372
  ```rb
427
373
  # app/lib/rodauth_app.rb
428
374
  class RodauthApp < Rodauth::Rails::App
429
- # ...
430
375
  configure do
431
376
  # ...
432
377
  create_reset_password_email do
@@ -456,10 +401,17 @@ class RodauthApp < Rodauth::Rails::App
456
401
  end
457
402
  ```
458
403
 
459
- This approach can be used even if you're using a 3rd-party service for
460
- transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
461
- the `create_*_email` block returns will be passed to `send_email`, so you can
462
- be creative.
404
+ This configuration calls `#deliver_later`, which uses Active Job to deliver
405
+ emails in a background job. It's generally recommended to send emails
406
+ asynchronously for better request throughput and the ability to retry
407
+ deliveries. However, if you want to send emails synchronously, modify the
408
+ configuration to call `#deliver_now` instead.
409
+
410
+ If you're using a background processing library without an Active Job adapter,
411
+ or a 3rd-party service for sending transactional emails, this two-phase API
412
+ might not be suitable. In this case, instead of overriding `#create_*_email`
413
+ and `#send_email`, override the `#send_*_email` methods instead, which are
414
+ required to send the email immediately.
463
415
 
464
416
  ### Migrations
465
417
 
@@ -481,6 +433,143 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
481
433
  end
482
434
  ```
483
435
 
436
+ ### Multiple configurations
437
+
438
+ If you need to handle multiple types of accounts that require different
439
+ authentication logic, you can create additional configurations for them:
440
+
441
+ ```rb
442
+ # app/lib/rodauth_app.rb
443
+ class RodauthApp < Rodauth::Rails::App
444
+ # primary configuration
445
+ configure do
446
+ # ...
447
+ end
448
+
449
+ # alternative configuration
450
+ configure(:admin) do
451
+ # ... enable features ...
452
+ prefix "/admin"
453
+ session_key_prefix "admin_"
454
+ remember_cookie_key "_admin_remember" # if using remember feature
455
+
456
+ # if you want separate tables
457
+ accounts_table :admin_accounts
458
+ password_hash_table :admin_account_password_hashes
459
+ # ...
460
+ end
461
+
462
+ route do |r|
463
+ r.rodauth
464
+
465
+ r.on "admin" do
466
+ r.rodauth(:admin)
467
+ r.pass # allow the Rails app to handle other "/admin/*" requests
468
+ end
469
+
470
+ # ...
471
+ end
472
+ end
473
+ ```
474
+
475
+ Then in your application you can reference the secondary Rodauth instance:
476
+
477
+ ```rb
478
+ rodauth(:admin).login_path #=> "/admin/login"
479
+ ```
480
+
481
+ #### Named auth classes
482
+
483
+ A `configure` block inside `Rodauth::Rails::App` will internally create an
484
+ anonymous `Rodauth::Auth` subclass, and register it under the given name.
485
+ However, you can also define the auth classes explicitly, by creating
486
+ subclasses of `Rodauth::Rails::Auth`:
487
+
488
+ ```rb
489
+ # app/lib/rodauth_main.rb
490
+ class RodauthMain < Rodauth::Rails::Auth
491
+ configure do
492
+ # ... main configuration ...
493
+ end
494
+ end
495
+ ```
496
+ ```rb
497
+ # app/lib/rodauth_admin.rb
498
+ class RodauthAdmin < Rodauth::Rails::Auth
499
+ configure do
500
+ # ...
501
+ prefix "/admin"
502
+ session_key_prefix "admin_"
503
+ # ...
504
+ end
505
+ end
506
+ ```
507
+ ```rb
508
+ # app/lib/rodauth_app.rb
509
+ class RodauthApp < Rodauth::Rails::App
510
+ configure RodauthMain
511
+ configure RodauthAdmin, :admin
512
+ # ...
513
+ end
514
+ ```
515
+
516
+ This allows having each configuration in a dedicated file, and named constants
517
+ improve introspection and error messages. You can also use inheritance to share
518
+ common settings:
519
+
520
+ ```rb
521
+ # app/lib/rodauth_base.rb
522
+ class RodauthBase < Rodauth::Rails::Auth
523
+ # common settings that can be shared between multiple configurations
524
+ configure do
525
+ enable :login, :logout
526
+ login_return_to_requested_location? true
527
+ logout_redirect "/"
528
+ # ...
529
+ end
530
+ end
531
+ ```
532
+ ```rb
533
+ # app/lib/rodauth_main.rb
534
+ class RodauthMain < RodauthBase # inherit common settings
535
+ configure do
536
+ # ... customize main ...
537
+ end
538
+ end
539
+ ```
540
+ ```rb
541
+ # app/lib/rodauth_admin.rb
542
+ class RodauthAdmin < RodauthBase # inherit common settings
543
+ configure do
544
+ # ... customize admin ...
545
+ end
546
+ end
547
+ ```
548
+
549
+ Another benefit of explicit classes is that you can define custom methods
550
+ directly at the class level instead of inside an `auth_class_eval`:
551
+
552
+ ```rb
553
+ # app/lib/rodauth_admin.rb
554
+ class RodauthAdmin < Rodauth::Rails::Auth
555
+ configure do
556
+ # ...
557
+ end
558
+
559
+ def superadmin?
560
+ Role.where(account_id: session_id, type: "superadmin").any?
561
+ end
562
+ end
563
+ ```
564
+ ```rb
565
+ # config/routes.rb
566
+ Rails.application.routes.draw do
567
+ constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
568
+ mount Sidekiq::Web => "sidekiq"
569
+ end
570
+ end
571
+ ```
572
+
484
573
  ### Calling controller methods
485
574
 
486
575
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -514,7 +603,7 @@ Rodauth operations outside of the request context. rodauth-rails gives you the
514
603
  ability to retrieve the Rodauth instance:
515
604
 
516
605
  ```rb
517
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
606
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
518
607
 
519
608
  rodauth.login_url #=> "https://example.com/login"
520
609
  rodauth.account_from_login("user@example.com") # loads user by email
@@ -523,7 +612,7 @@ rodauth.setup_account_verification
523
612
  rodauth.close_account
524
613
  ```
525
614
 
526
- This Rodauth instance will be initialized with basic Rack env that allows is it
615
+ This Rodauth instance will be initialized with basic Rack env that allows it
527
616
  to generate URLs, using `config.action_mailer.default_url_options` options.
528
617
 
529
618
  ## How it works
@@ -545,8 +634,8 @@ The Rodauth app stores the `Rodauth::Auth` instance in the Rack env hash, which
545
634
  is then available in your Rails app:
546
635
 
547
636
  ```rb
548
- request.env["rodauth"] #=> #<Rodauth::Auth>
549
- request.env["rodauth.secondary"] #=> #<Rodauth::Auth> (if using multiple configurations)
637
+ request.env["rodauth"] #=> #<Rodauth::Auth>
638
+ request.env["rodauth.admin"] #=> #<Rodauth::Auth> (if using multiple configurations)
550
639
  ```
551
640
 
552
641
  For convenience, this object can be accessed via the `#rodauth` method in views
@@ -555,14 +644,14 @@ and controllers:
555
644
  ```rb
556
645
  class MyController < ApplicationController
557
646
  def my_action
558
- rodauth #=> #<Rodauth::Auth>
559
- rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations)
647
+ rodauth #=> #<Rodauth::Auth>
648
+ rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations)
560
649
  end
561
650
  end
562
651
  ```
563
652
  ```erb
564
- <% rodauth #=> #<Rodauth::Auth> %>
565
- <% rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations) %>
653
+ <% rodauth #=> #<Rodauth::Auth> %>
654
+ <% rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations) %>
566
655
  ```
567
656
 
568
657
  ### App
@@ -584,7 +673,7 @@ any additional [plugin options].
584
673
  class RodauthApp < Rodauth::Rails::App
585
674
  configure { ... } # defining default Rodauth configuration
586
675
  configure(json: true) { ... } # passing options to the Rodauth plugin
587
- configure(:secondary) { ... } # defining multiple Rodauth configurations
676
+ configure(:admin) { ... } # defining multiple Rodauth configurations
588
677
  end
589
678
  ```
590
679
 
@@ -619,15 +708,32 @@ function calls).
619
708
 
620
709
  If ActiveRecord is used in the application, the `rodauth:install` generator
621
710
  will have automatically configured Sequel to reuse ActiveRecord's database
622
- connection (using the [sequel-activerecord_connection] gem).
711
+ connection, using the [sequel-activerecord_connection] gem.
623
712
 
624
713
  This means that, from the usage perspective, Sequel can be considered just
625
714
  as an implementation detail of Rodauth.
626
715
 
627
716
  ## JSON API
628
717
 
629
- JSON API support in Rodauth is provided by the [JWT feature][jwt]. You'll need
630
- to install the [JWT gem], enable JSON support and enable the JWT feature:
718
+ To make Rodauth endpoints accessible via JSON API, enable the [`json`][json]
719
+ feature:
720
+
721
+ ```rb
722
+ # app/lib/rodauth_app.rb
723
+ class RodauthApp < Rodauth::Rails::App
724
+ configure do
725
+ # ...
726
+ enable :json
727
+ only_json? true # accept only JSON requests (optional)
728
+ # ...
729
+ end
730
+ end
731
+ ```
732
+
733
+ This will store account session data into the Rails session. If you rather want
734
+ stateless token-based authentication via the `Authorization` header, enable the
735
+ [`jwt`][jwt] feature (which builds on top of the `json` feature) and add the
736
+ [JWT gem] to the Gemfile:
631
737
 
632
738
  ```sh
633
739
  $ bundle add jwt
@@ -635,23 +741,33 @@ $ bundle add jwt
635
741
  ```rb
636
742
  # app/lib/rodauth_app.rb
637
743
  class RodauthApp < Rodauth::Rails::App
638
- configure(json: :only) do
744
+ configure do
639
745
  # ...
640
746
  enable :jwt
641
- # make sure to store the JWT secret below in a safe place
642
- jwt_secret "...your secret key..."
747
+ jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
748
+ only_json? true # accept only JSON requests (optional)
643
749
  # ...
644
750
  end
645
751
  end
646
752
  ```
647
753
 
648
- With the above configuration, Rodauth routes will only be accessible via JSON
649
- requests. If you still want to allow HTML access alongside JSON, change `json:
650
- :only` to `json: true`.
754
+ If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
755
+ corresponding Rodauth features and create the necessary tables:
651
756
 
652
- Emails will automatically work in JSON-only mode, because `Rodauth::Rails::App`
653
- comes with Roda's `render` plugin loaded. They are customized the same as in
654
- the non-JSON case.
757
+ ```sh
758
+ $ rails generate rodauth:migration jwt_refresh
759
+ $ rails db:migrate
760
+ ```
761
+ ```rb
762
+ # app/lib/rodauth_app.rb
763
+ class RodauthApp < Rodauth::Rails::App
764
+ configure do
765
+ # ...
766
+ enable :jwt, :jwt_cors, :jwt_refresh
767
+ # ...
768
+ end
769
+ end
770
+ ```
655
771
 
656
772
  ## OmniAuth
657
773
 
@@ -709,7 +825,8 @@ end
709
825
  <%= link_to "Login via Facebook", "/auth/facebook" %>
710
826
  ```
711
827
 
712
- Let's implement the OmniAuth callback endpoint on our Rodauth controller:
828
+ Finally, let's implement the OmniAuth callback endpoint on our Rodauth
829
+ controller:
713
830
 
714
831
  ```rb
715
832
  # config/routes.rb
@@ -745,7 +862,7 @@ class RodauthController < ApplicationController
745
862
 
746
863
  # create new account if it doesn't exist
747
864
  unless account
748
- account = Account.create!(email: auth["info"]["email"])
865
+ account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
749
866
  end
750
867
 
751
868
  # create new identity if it doesn't exist
@@ -762,11 +879,8 @@ end
762
879
 
763
880
  ## Configuring
764
881
 
765
- For the list of configuration methods provided by Rodauth, see the [feature
766
- documentation].
767
-
768
- The `rails` feature rodauth-rails loads is customizable as well, here is the
769
- list of its configuration methods:
882
+ The `rails` feature rodauth-rails loads provides the following configuration
883
+ methods:
770
884
 
771
885
  | Name | Description |
772
886
  | :---- | :---------- |
@@ -793,21 +907,27 @@ Rodauth::Rails.configure do |config|
793
907
  end
794
908
  ```
795
909
 
910
+ For the list of configuration methods provided by Rodauth, see the [feature
911
+ documentation].
912
+
796
913
  ## Custom extensions
797
914
 
798
915
  When developing custom extensions for Rodauth inside your Rails project, it's
799
- better to use plain modules (at least in the beginning), because Rodauth
800
- feature API doesn't yet support Zeitwerk reloading well.
916
+ probably better to use plain modules, at least in the beginning, as Rodauth
917
+ feature design doesn't yet work well with Zeitwerk reloading.
918
+
919
+ Here is an example of an LDAP authentication extension that uses the
920
+ [simple_ldap_authenticator] gem.
801
921
 
802
922
  ```rb
803
- # app/lib/rodauth_argon2.rb
804
- module RodauthArgon2
805
- def password_hash(password)
806
- Argon2::Password.create(password, t_cost: password_hash_cost, m_cost: password_hash_cost)
923
+ # app/lib/rodauth_ldap.rb
924
+ module RodauthLdap
925
+ def require_bcrypt?
926
+ false
807
927
  end
808
928
 
809
- def password_hash_match?(hash, password)
810
- Argon2::Password.verify_password(password, hash)
929
+ def password_match?(password)
930
+ SimpleLdapAuthenticator.valid?(account[:email], password)
811
931
  end
812
932
  end
813
933
  ```
@@ -817,7 +937,7 @@ class RodauthApp < Rodauth::Rails::App
817
937
  configure do
818
938
  # ...
819
939
  auth_class_eval do
820
- include RodauthArgon2
940
+ include RodauthLdap
821
941
  end
822
942
  # ...
823
943
  end
@@ -826,48 +946,156 @@ end
826
946
 
827
947
  ## Testing
828
948
 
829
- If you're writing system tests, it's generally better to go through the actual
830
- authentication flow with tools like Capybara, and to not use any stubbing.
831
-
832
- In functional and integration tests you can just make requests to Rodauth
833
- routes:
949
+ System (browser) tests for Rodauth actions could look something like this:
834
950
 
835
951
  ```rb
836
- # test/controllers/posts_controller_test.rb
837
- class PostsControllerTest < ActionDispatch::IntegrationTest
838
- test "should require authentication" do
839
- get posts_url
840
- assert_redirected_to "/login"
952
+ # test/system/authentication_test.rb
953
+ require "test_helper"
954
+
955
+ class AuthenticationTest < ActionDispatch::SystemTestCase
956
+ include ActiveJob::TestHelper
957
+ driven_by :rack_test
958
+
959
+ test "creating and verifying an account" do
960
+ create_account
961
+ assert_match "An email has been sent to you with a link to verify your account", page.text
962
+
963
+ verify_account
964
+ assert_match "Your account has been verified", page.text
965
+ end
966
+
967
+ test "logging in and logging out" do
968
+ create_account(verify: true)
969
+
970
+ logout
971
+ assert_match "You have been logged out", page.text
841
972
 
842
973
  login
843
- get posts_url
974
+ assert_match "You have been logged in", page.text
975
+ end
976
+
977
+ private
978
+
979
+ def create_account(email: "user@example.com", password: "secret", verify: false)
980
+ visit "/create-account"
981
+ fill_in "Login", with: email
982
+ fill_in "Password", with: password
983
+ fill_in "Confirm Password", with: password
984
+ click_on "Create Account"
985
+ verify_account if verify
986
+ end
987
+
988
+ def verify_account
989
+ perform_enqueued_jobs # run enqueued email deliveries
990
+ email = ActionMailer::Base.deliveries.last
991
+ verify_account_link = email.body.to_s[/\S+verify-account\S+/]
992
+ visit verify_account_link
993
+ click_on "Verify Account"
994
+ end
995
+
996
+ def login(email: "user@example.com", password: "secret")
997
+ visit "/login"
998
+ fill_in "Login", with: email
999
+ fill_in "Password", with: password
1000
+ click_on "Login"
1001
+ end
1002
+
1003
+ def logout
1004
+ visit "/logout"
1005
+ click_on "Logout"
1006
+ end
1007
+ end
1008
+ ```
1009
+
1010
+ While request tests in JSON API mode with JWT tokens could look something like
1011
+ this:
1012
+
1013
+ ```rb
1014
+ # test/integration/authentication_test.rb
1015
+ require "test_helper"
1016
+
1017
+ class AuthenticationTest < ActionDispatch::IntegrationTest
1018
+ test "creating and verifying an account" do
1019
+ create_account
844
1020
  assert_response :success
1021
+ assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
1022
+
1023
+ verify_account
1024
+ assert_response :success
1025
+ assert_match "Your account has been verified", JSON.parse(body)["success"]
1026
+ end
1027
+
1028
+ test "logging in and logging out" do
1029
+ create_account(verify: true)
845
1030
 
846
1031
  logout
847
- assert_redirected_to "/login"
1032
+ assert_response :success
1033
+ assert_match "You have been logged out", JSON.parse(body)["success"]
1034
+
1035
+ login
1036
+ assert_response :success
1037
+ assert_match "You have been logged in", JSON.parse(body)["success"]
848
1038
  end
849
1039
 
850
1040
  private
851
1041
 
852
- def login(login: "user@example.com", password: "secret")
853
- post "/create-account", params: {
854
- "login" => login,
855
- "password" => password,
856
- "password-confirm" => password,
857
- }
1042
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1043
+ post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
1044
+ verify_account if verify
1045
+ end
858
1046
 
859
- post "/login", params: {
860
- "login" => login,
861
- "password" => password,
862
- }
1047
+ def verify_account
1048
+ perform_enqueued_jobs # run enqueued email deliveries
1049
+ email = ActionMailer::Base.deliveries.last
1050
+ verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
1051
+ post "/verify-account", as: :json, params: { key: verify_account_key }
1052
+ end
1053
+
1054
+ def login(email: "user@example.com", password: "secret")
1055
+ post "/login", as: :json, params: { login: email, password: password }
863
1056
  end
864
1057
 
865
1058
  def logout
866
- post "/logout"
1059
+ post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
867
1060
  end
868
1061
  end
869
1062
  ```
870
1063
 
1064
+ If you're delivering emails in the background, make sure to set Active Job
1065
+ queue adapter to `:test` or `:inline`:
1066
+
1067
+ ```rb
1068
+ # config/environments/test.rb
1069
+ Rails.application.configure do |config|
1070
+ # ...
1071
+ config.active_job.queue_adapter = :test # or :inline
1072
+ # ...
1073
+ end
1074
+ ```
1075
+
1076
+ If you need to create an account record with a password directly, you can do it
1077
+ as follows:
1078
+
1079
+ ```rb
1080
+ # app/models/account.rb
1081
+ class Account < ApplicationRecord
1082
+ has_one :password_hash, foreign_key: :id
1083
+ end
1084
+ ```
1085
+ ```rb
1086
+ # app/models/account/password_hash.rb
1087
+ class Account::PasswordHash < ApplicationRecord
1088
+ belongs_to :account, foreign_key: :id
1089
+ end
1090
+ ```
1091
+ ```rb
1092
+ require "bcrypt"
1093
+
1094
+ account = Account.create!(email: "user@example.com", status: "verified")
1095
+ password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1096
+ account.create_password_hash!(id: account.id, password_hash: password_hash)
1097
+ ```
1098
+
871
1099
  ## Rodauth defaults
872
1100
 
873
1101
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -976,6 +1204,7 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
976
1204
  [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
977
1205
  [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
978
1206
  [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
1207
+ [json]: http://rodauth.jeremyevans.net/rdoc/files/doc/json_rdoc.html
979
1208
  [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
980
1209
  [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
981
1210
  [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
@@ -987,3 +1216,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
987
1216
  [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
988
1217
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
989
1218
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1219
+ [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator