rodauth-rails 0.8.2 → 0.12.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 +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