rodauth-rails 0.9.0 → 0.13.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 +44 -0
  3. data/README.md +417 -250
  4. data/lib/generators/rodauth/install_generator.rb +17 -0
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
  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 +32 -4
  25. data/lib/rodauth/rails/app.rb +20 -22
  26. data/lib/rodauth/rails/app/flash.rb +2 -8
  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 -210
  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 +12 -6
  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: 60fda35b195285a7c9cc14e07153d80faa9e939cf2944fbb04e1360baf30e306
4
- data.tar.gz: 1f89bcfff28e6d08287fa67a9fe9228a2d61f15a8a9cdecb9fcf138137d72c47
3
+ metadata.gz: cc8ee44d094627dcacd9d9b7f5da1eb165cff1af209f079b667e0f04e9540b30
4
+ data.tar.gz: f179e4eaea99d04ff6ff71c6357cdf75a19991645c9904ab6373c03b5dcd1a16
5
5
  SHA512:
6
- metadata.gz: 8e4ed3afbe7a114ba36f19541d1c8c8ee62de07526400230b1386f027b05876cd78ad88bdb40cae9767e74f83d1532d4939d97d03657662933f81d7086df34d9
7
- data.tar.gz: cae1fc15a86f1b2e2423a8e54f36b844f610ba23ff74fd9ced132200e9816260028682c0efea1dcf19cf8a722a7ae49882c8d31c670e268e229117b4f6fb84f2
6
+ metadata.gz: 78c28c13751abb439179813948bf665cd040444171998e42ecdb4cb42f698097731f4c073b7595d083ba5825a9989940deee052771fb5f76f93bd333e94af500
7
+ data.tar.gz: eb3a04ae6333dc471fd7fbdb264527a359893fb100d7833bab3545f7d91e213bfc8a2daa562ffc22f531073f39f4bee3de893bffd87201b3e57d1dce99c97320
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## 0.13.0 (2021-06-10)
2
+
3
+ * Add `:query`, `:form`, `:session`, `:account`, and `:env` options to `Rodauth::Rails.rodauth` (@janko)
4
+
5
+ ## 0.12.0 (2021-05-15)
6
+
7
+ * Include total view render time in logs for Rodauth requests (@janko)
8
+
9
+ * Instrument redirects (@janko)
10
+
11
+ * Instrument Rodauth requests on `action_controller` namespace (@janko)
12
+
13
+ * Update templates for Boostrap 5 compatibility (@janko)
14
+
15
+ * Log request parameters for Rodauth requests (@janko)
16
+
17
+ ## 0.11.0 (2021-05-06)
18
+
19
+ * Add controller-like logging for requests to Rodauth endpoints (@janko)
20
+
21
+ * Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
22
+
23
+ * Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
24
+
25
+ ## 0.10.0 (2021-03-23)
26
+
27
+ * Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
28
+
29
+ * Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
30
+
31
+ * Improve Roda middleware inspect output (@janko)
32
+
33
+ * Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
34
+
35
+ * Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
36
+
37
+ * Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
38
+
39
+ ## 0.9.1 (2021-02-10)
40
+
41
+ * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
42
+
43
+ * Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
44
+
1
45
  ## 0.9.0 (2021-02-07)
2
46
 
3
47
  * Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
data/README.md CHANGED
@@ -2,35 +2,6 @@
2
2
 
3
3
  Provides Rails integration for the [Rodauth] authentication framework.
4
4
 
5
- ## Table of contents
6
-
7
- * [Resources](#resources)
8
- * [Why Rodauth?](#why-rodauth)
9
- * [Upgrading](#upgrading)
10
- * [Installation](#installation)
11
- * [Usage](#usage)
12
- - [Routes](#routes)
13
- - [Current account](#current-account)
14
- - [Requiring authentication](#requiring-authentication)
15
- - [Views](#views)
16
- - [Mailer](#mailer)
17
- - [Migrations](#migrations)
18
- - [Multiple configurations](#multiple-configurations)
19
- - [Calling controller methods](#calling-controller-methods)
20
- - [Rodauth instance](#rodauth-instance)
21
- * [How it works](#how-it-works)
22
- - [Middleware](#middleware)
23
- - [App](#app)
24
- - [Sequel](#sequel)
25
- * [JSON API](#json-api)
26
- * [OmniAuth](#omniauth)
27
- * [Configuring](#configuring)
28
- * [Custom extensions](#custom-extensions)
29
- * [Testing](#testing)
30
- * [Rodauth defaults](#rodauth-defaults)
31
- - [Database functions](#database-functions)
32
- - [Account statuses](#account-statuses)
33
-
34
5
  ## Resources
35
6
 
36
7
  Useful links:
@@ -43,6 +14,7 @@ Articles:
43
14
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
44
15
  * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
45
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)
46
18
 
47
19
  ## Why Rodauth?
48
20
 
@@ -61,29 +33,23 @@ of the advantages that stand out for me:
61
33
  * consistent before/after hooks around everything
62
34
  * dedicated object encapsulating all authentication logic
63
35
 
64
- ## Upgrading
65
-
66
- ### Upgrading to 0.7.0
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].
67
41
 
68
- Starting from version 0.7.0, rodauth-rails now correctly detects Rails
69
- application's `secret_key_base` when setting default `hmac_secret`, including
70
- when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
71
- means that your authentication will now be more secure by default, and Rodauth
72
- features that require `hmac_secret` should now work automatically as well.
42
+ ## Upgrading
73
43
 
74
- However, if you've already been using rodauth-rails in production, where the
75
- `secret_key_base` is set via credentials or environment variable and `hmac_secret`
76
- was not explicitly set, the fact that your authentication will now start using
77
- HMACs has backwards compatibility considerations. See the [Rodauth
78
- documentation][hmac] for instructions on how to safely transition, or just set
79
- `hmac_secret nil` in your Rodauth configuration.
44
+ For instructions on upgrading from previous rodauth-rails versions, see
45
+ [UPGRADING.md](/UPGRADING.md).
80
46
 
81
47
  ## Installation
82
48
 
83
49
  Add the gem to your Gemfile:
84
50
 
85
51
  ```rb
86
- gem "rodauth-rails", "~> 0.9"
52
+ gem "rodauth-rails", "~> 0.13"
87
53
 
88
54
  # gem "jwt", require: false # for JWT feature
89
55
  # gem "rotp", require: false # for OTP feature
@@ -108,114 +74,22 @@ $ rails generate rodauth:install --jwt # token authentication via the "Authoriza
108
74
  $ bundle add jwt
109
75
  ```
110
76
 
111
- The generator will create the following files:
112
-
113
- * Rodauth migration at `db/migrate/*_create_rodauth.rb`
114
- * Rodauth initializer at `config/initializers/rodauth.rb`
115
- * Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
116
- * Rodauth app at `app/lib/rodauth_app.rb`
117
- * Rodauth controller at `app/controllers/rodauth_controller.rb`
118
- * Account model at `app/models/account.rb`
119
-
120
- ### Migration
121
-
122
- The migration file creates tables required by Rodauth. You're encouraged to
123
- review the migration, and modify it to only create tables for features you
124
- intend to use.
125
-
126
- ```rb
127
- # db/migrate/*_create_rodauth.rb
128
- class CreateRodauth < ActiveRecord::Migration
129
- def change
130
- create_table :accounts do |t| ... end
131
- create_table :account_password_hashes do |t| ... end
132
- create_table :account_password_reset_keys do |t| ... end
133
- create_table :account_verification_keys do |t| ... end
134
- create_table :account_login_change_keys do |t| ... end
135
- create_table :account_remember_keys do |t| ... end
136
- end
137
- end
138
- ```
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.
139
80
 
140
- 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:
141
83
 
142
- ```
84
+ ```sh
143
85
  $ rails db:migrate
144
86
  ```
145
87
 
146
- ### Rodauth initializer
147
-
148
- The Rodauth initializer assigns the constant for your Rodauth app, which will
149
- be called by the Rack middleware that's added in front of your Rails router.
150
-
151
- ```rb
152
- # config/initializers/rodauth.rb
153
- Rodauth::Rails.configure do |config|
154
- config.app = "RodauthApp"
155
- end
156
- ```
157
-
158
- ### Sequel initializer
159
-
160
- Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
161
- an additional initializer will be created which configures Sequel to use the
162
- ActiveRecord connection.
163
-
164
- ```rb
165
- # config/initializers/sequel.rb
166
- require "sequel/core"
167
-
168
- # initialize Sequel and have it reuse Active Record's database connection
169
- DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
170
- ```
171
-
172
- ### Rodauth app
173
-
174
- Your Rodauth app is created in the `app/lib/` directory, and comes with a
175
- default set of authentication features enabled, as well as extensive examples
176
- on ways you can configure authentication behaviour.
177
-
178
- ```rb
179
- # app/lib/rodauth_app.rb
180
- class RodauthApp < Rodauth::Rails::App
181
- configure do
182
- # authentication configuration
183
- end
184
-
185
- route do |r|
186
- # request handling
187
- end
188
- end
189
- ```
190
-
191
- ### Controller
192
-
193
- Your Rodauth app will by default use `RodauthController` for view rendering,
194
- CSRF protection, and running controller callbacks and rescue handlers around
195
- Rodauth actions.
196
-
197
- ```rb
198
- # app/controllers/rodauth_controller.rb
199
- class RodauthController < ApplicationController
200
- end
201
- ```
202
-
203
- ### Account model
204
-
205
- Rodauth stores user accounts in the `accounts` table, so the generator will
206
- also create an `Account` model for custom use.
207
-
208
- ```rb
209
- # app/models/account.rb
210
- class Account < ApplicationRecord
211
- end
212
- ```
213
-
214
88
  ## Usage
215
89
 
216
90
  ### Routes
217
91
 
218
- We can see the list of routes our Rodauth middleware handles:
92
+ You can see the list of routes our Rodauth middleware handles:
219
93
 
220
94
  ```sh
221
95
  $ rails rodauth:routes
@@ -237,7 +111,7 @@ Routes handled by RodauthApp:
237
111
  /close-account rodauth.close_account_path
238
112
  ```
239
113
 
240
- 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
241
115
  navigation header:
242
116
 
243
117
  ```erb
@@ -253,9 +127,22 @@ These routes are fully functional, feel free to visit them and interact with the
253
127
  pages. The templates that ship with Rodauth aim to provide a complete
254
128
  authentication experience, and the forms use [Bootstrap] markup.
255
129
 
130
+ Inside Rodauth configuration and the `route` block you can access Rails route
131
+ helpers through `#rails_routes`:
132
+
133
+ ```rb
134
+ class RodauthApp < Rodauth::Rails::App
135
+ configure do
136
+ # ...
137
+ login_redirect { rails_routes.activity_path }
138
+ # ...
139
+ end
140
+ end
141
+ ```
142
+
256
143
  ### Current account
257
144
 
258
- 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
259
146
  `#current_account` method that fetches the account id from session and
260
147
  retrieves the corresponding account record:
261
148
 
@@ -272,11 +159,11 @@ class ApplicationController < ActionController::Base
272
159
  rodauth.logout
273
160
  rodauth.login_required
274
161
  end
275
- helper_method :current_account
162
+ helper_method :current_account # skip if inheriting from ActionController::API
276
163
  end
277
164
  ```
278
165
 
279
- 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:
280
167
 
281
168
  ```erb
282
169
  <p>Authenticated as: <%= current_account.email %></p>
@@ -284,9 +171,9 @@ This allows us to access the current account in controllers and views:
284
171
 
285
172
  ### Requiring authentication
286
173
 
287
- We'll likely want to require authentication for certain parts of our app,
288
- redirecting the user to the login page if they're not logged in. We can do this
289
- 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
290
177
  encapsulated:
291
178
 
292
179
  ```rb
@@ -305,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
305
192
  end
306
193
  ```
307
194
 
308
- We can also require authentication at the controller layer:
195
+ You can also require authentication at the controller layer:
309
196
 
310
197
  ```rb
311
198
  # app/controllers/application_controller.rb
@@ -330,15 +217,52 @@ class PostsController < ApplicationController
330
217
  end
331
218
  ```
332
219
 
333
- Or at the Rails router level:
220
+ #### Routing constraints
221
+
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:
334
224
 
335
225
  ```rb
336
226
  # config/routes.rb
337
227
  Rails.application.routes.draw do
338
- constraints -> (r) { r.env["rodauth"].require_authentication } do
339
- namespace :admin do
340
- # ...
341
- end
228
+ constraints Rodauth::Rails.authenticated do
229
+ # ... authenticated routes ...
230
+ end
231
+ end
232
+ ```
233
+
234
+ If you want additional conditions, you can pass in a block, which is
235
+ called with the Rodauth instance:
236
+
237
+ ```rb
238
+ # config/routes.rb
239
+ Rails.application.routes.draw do
240
+ # require multifactor authentication to be setup
241
+ constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
242
+ # ...
243
+ end
244
+ end
245
+ ```
246
+
247
+ You can specify the Rodauth configuration by passing the configuration name:
248
+
249
+ ```rb
250
+ # config/routes.rb
251
+ Rails.application.routes.draw do
252
+ constraints Rodauth::Rails.authenticated(:admin) do
253
+ # ...
254
+ end
255
+ end
256
+ ```
257
+
258
+ If you need something more custom, you can always create the routing constraint
259
+ manually:
260
+
261
+ ```rb
262
+ # config/routes.rb
263
+ Rails.application.routes.draw do
264
+ constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
265
+ # routes when the user is not logged in
342
266
  end
343
267
  end
344
268
  ```
@@ -358,7 +282,7 @@ This will generate views for the default set of Rodauth features into the
358
282
  `RodauthController`.
359
283
 
360
284
  You can pass a list of Rodauth features to the generator to create views for
361
- these features (this will not remove any existing views):
285
+ these features (this will not remove or overwrite any existing views):
362
286
 
363
287
  ```sh
364
288
  $ rails generate rodauth:views login create_account lockout otp
@@ -406,58 +330,36 @@ end
406
330
 
407
331
  ### Mailer
408
332
 
409
- Depending on the features you've enabled, Rodauth may send emails as part of
410
- the authentication flow. Most email settings can be customized:
333
+ The install generator will create `RodauthMailer` with default email templates,
334
+ and configure Rodauth features that send emails as part of the authentication
335
+ flow to use it.
411
336
 
412
337
  ```rb
413
- # app/lib/rodauth_app.rb
414
- class RodauthApp < Rodauth::Rails::App
415
- # ...
416
- configure do
338
+ # app/mailers/rodauth_mailer.rb
339
+ class RodauthMailer < ApplicationMailer
340
+ def verify_account(recipient, email_link)
341
+ # ...
342
+ end
343
+ def reset_password(recipient, email_link)
417
344
  # ...
418
- # general settings
419
- email_from "no-reply@myapp.com"
420
- email_subject_prefix "[MyApp] "
421
- send_email(&:deliver_later)
345
+ end
346
+ def verify_login_change(recipient, old_login, new_login, email_link)
422
347
  # ...
423
- # feature settings
424
- verify_account_email_subject "Verify your account"
425
- verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
348
+ end
349
+ def password_changed(recipient)
426
350
  # ...
427
351
  end
352
+ # def email_auth(recipient, email_link)
353
+ # ...
354
+ # end
355
+ # def unlock_account(recipient, email_link)
356
+ # ...
357
+ # end
428
358
  end
429
359
  ```
430
-
431
- This is convenient when starting out, but eventually you might want to use your
432
- own mailer. You can start by running the following command:
433
-
434
- ```sh
435
- $ rails generate rodauth:mailer
436
- ```
437
-
438
- This will create a `RodauthMailer` with the associated mailer views in
439
- `app/views/rodauth_mailer` directory:
440
-
441
- ```rb
442
- # app/mailers/rodauth_mailer.rb
443
- class RodauthMailer < ApplicationMailer
444
- def verify_account(recipient, email_link) ... end
445
- def reset_password(recipient, email_link) ... end
446
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
447
- def password_changed(recipient) ... end
448
- # def email_auth(recipient, email_link) ... end
449
- # def unlock_account(recipient, email_link) ... end
450
- end
451
- ```
452
-
453
- You can then uncomment the lines in your Rodauth configuration to have it call
454
- your mailer. If you've enabled additional authentication features that send
455
- emails, make sure to override their `create_*_email` methods as well.
456
-
457
360
  ```rb
458
361
  # app/lib/rodauth_app.rb
459
362
  class RodauthApp < Rodauth::Rails::App
460
- # ...
461
363
  configure do
462
364
  # ...
463
365
  create_reset_password_email do
@@ -487,10 +389,17 @@ class RodauthApp < Rodauth::Rails::App
487
389
  end
488
390
  ```
489
391
 
490
- This approach can be used even if you're using a 3rd-party service for
491
- transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
492
- the `create_*_email` block returns will be passed to `send_email`, so you can
493
- be creative.
392
+ This configuration calls `#deliver_later`, which uses Active Job to deliver
393
+ emails in a background job. It's generally recommended to send emails
394
+ asynchronously for better request throughput and the ability to retry
395
+ deliveries. However, if you want to send emails synchronously, modify the
396
+ configuration to call `#deliver_now` instead.
397
+
398
+ If you're using a background processing library without an Active Job adapter,
399
+ or a 3rd-party service for sending transactional emails, this two-phase API
400
+ might not be suitable. In this case, instead of overriding `#create_*_email`
401
+ and `#send_email`, override the `#send_*_email` methods instead, which are
402
+ required to send the email immediately.
494
403
 
495
404
  ### Migrations
496
405
 
@@ -515,7 +424,7 @@ end
515
424
  ### Multiple configurations
516
425
 
517
426
  If you need to handle multiple types of accounts that require different
518
- authentication logic, you can create different configurations for them:
427
+ authentication logic, you can create additional configurations for them:
519
428
 
520
429
  ```rb
521
430
  # app/lib/rodauth_app.rb
@@ -531,12 +440,21 @@ class RodauthApp < Rodauth::Rails::App
531
440
  prefix "/admin"
532
441
  session_key_prefix "admin_"
533
442
  remember_cookie_key "_admin_remember" # if using remember feature
443
+
444
+ # if you want separate tables
445
+ accounts_table :admin_accounts
446
+ password_hash_table :admin_account_password_hashes
534
447
  # ...
535
448
  end
536
449
 
537
450
  route do |r|
538
451
  r.rodauth
539
- r.on("admin") { r.rodauth(:admin) }
452
+
453
+ r.on "admin" do
454
+ r.rodauth(:admin)
455
+ break # allow routing of other /admin/* requests to continue to Rails
456
+ end
457
+
540
458
  # ...
541
459
  end
542
460
  end
@@ -548,6 +466,98 @@ Then in your application you can reference the secondary Rodauth instance:
548
466
  rodauth(:admin).login_path #=> "/admin/login"
549
467
  ```
550
468
 
469
+ #### Named auth classes
470
+
471
+ A `configure` block inside `Rodauth::Rails::App` will internally create an
472
+ anonymous `Rodauth::Auth` subclass, and register it under the given name.
473
+ However, you can also define the auth classes explicitly, by creating
474
+ subclasses of `Rodauth::Rails::Auth`:
475
+
476
+ ```rb
477
+ # app/lib/rodauth_main.rb
478
+ class RodauthMain < Rodauth::Rails::Auth
479
+ configure do
480
+ # ... main configuration ...
481
+ end
482
+ end
483
+ ```
484
+ ```rb
485
+ # app/lib/rodauth_admin.rb
486
+ class RodauthAdmin < Rodauth::Rails::Auth
487
+ configure do
488
+ # ...
489
+ prefix "/admin"
490
+ session_key_prefix "admin_"
491
+ # ...
492
+ end
493
+ end
494
+ ```
495
+ ```rb
496
+ # app/lib/rodauth_app.rb
497
+ class RodauthApp < Rodauth::Rails::App
498
+ configure RodauthMain
499
+ configure RodauthAdmin, :admin
500
+ # ...
501
+ end
502
+ ```
503
+
504
+ This allows having each configuration in a dedicated file, and named constants
505
+ improve introspection and error messages. You can also use inheritance to share
506
+ common settings:
507
+
508
+ ```rb
509
+ # app/lib/rodauth_base.rb
510
+ class RodauthBase < Rodauth::Rails::Auth
511
+ # common settings that can be shared between multiple configurations
512
+ configure do
513
+ enable :login, :logout
514
+ login_return_to_requested_location? true
515
+ logout_redirect "/"
516
+ # ...
517
+ end
518
+ end
519
+ ```
520
+ ```rb
521
+ # app/lib/rodauth_main.rb
522
+ class RodauthMain < RodauthBase # inherit common settings
523
+ configure do
524
+ # ... customize main ...
525
+ end
526
+ end
527
+ ```
528
+ ```rb
529
+ # app/lib/rodauth_admin.rb
530
+ class RodauthAdmin < RodauthBase # inherit common settings
531
+ configure do
532
+ # ... customize admin ...
533
+ end
534
+ end
535
+ ```
536
+
537
+ Another benefit of explicit classes is that you can define custom methods
538
+ directly at the class level instead of inside an `auth_class_eval`:
539
+
540
+ ```rb
541
+ # app/lib/rodauth_admin.rb
542
+ class RodauthAdmin < Rodauth::Rails::Auth
543
+ configure do
544
+ # ...
545
+ end
546
+
547
+ def superadmin?
548
+ Role.where(account_id: session_id, type: "superadmin").any?
549
+ end
550
+ end
551
+ ```
552
+ ```rb
553
+ # config/routes.rb
554
+ Rails.application.routes.draw do
555
+ constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
556
+ mount Sidekiq::Web => "sidekiq"
557
+ end
558
+ end
559
+ ```
560
+
551
561
  ### Calling controller methods
552
562
 
553
563
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -577,8 +587,8 @@ end
577
587
  ### Rodauth instance
578
588
 
579
589
  In some cases you might need to use Rodauth more programmatically, and perform
580
- Rodauth operations outside of the request context. rodauth-rails gives you the
581
- ability to retrieve the Rodauth instance:
590
+ Rodauth operations outside of the request context. rodauth-rails gives you a
591
+ helper method for building a Rodauth instance:
582
592
 
583
593
  ```rb
584
594
  rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
@@ -590,8 +600,22 @@ rodauth.setup_account_verification
590
600
  rodauth.close_account
591
601
  ```
592
602
 
593
- This Rodauth instance will be initialized with basic Rack env that allows is it
594
- to generate URLs, using `config.action_mailer.default_url_options` options.
603
+ The base URL is taken from Action Mailer's `default_url_options` setting if
604
+ configured. The `Rodauth::Rails.rodauth` method accepts additional keyword
605
+ arguments:
606
+
607
+ * `:account` – Active Record model instance from which to set `account` and `session[:account_id]`
608
+ * `:query` & `:form` – set specific query/form parameters
609
+ * `:session` – set any session values
610
+ * `:env` – set any additional Rack env values
611
+
612
+ ```rb
613
+ Rodauth::Rails.rodauth(account: Account.find(account_id))
614
+ Rodauth::Rails.rodauth(query: { "param" => "value" })
615
+ Rodauth::Rails.rodauth(form: { "param" => "value" })
616
+ Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
617
+ Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
618
+ ```
595
619
 
596
620
  ## How it works
597
621
 
@@ -702,7 +726,7 @@ class RodauthApp < Rodauth::Rails::App
702
726
  configure do
703
727
  # ...
704
728
  enable :json
705
- only_json? true # accept only JSON requests
729
+ only_json? true # accept only JSON requests (optional)
706
730
  # ...
707
731
  end
708
732
  end
@@ -723,7 +747,25 @@ class RodauthApp < Rodauth::Rails::App
723
747
  # ...
724
748
  enable :jwt
725
749
  jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
726
- only_json? true # accept only JSON requests
750
+ only_json? true # accept only JSON requests (optional)
751
+ # ...
752
+ end
753
+ end
754
+ ```
755
+
756
+ If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
757
+ corresponding Rodauth features and create the necessary tables:
758
+
759
+ ```sh
760
+ $ rails generate rodauth:migration jwt_refresh
761
+ $ rails db:migrate
762
+ ```
763
+ ```rb
764
+ # app/lib/rodauth_app.rb
765
+ class RodauthApp < Rodauth::Rails::App
766
+ configure do
767
+ # ...
768
+ enable :jwt, :jwt_cors, :jwt_refresh
727
769
  # ...
728
770
  end
729
771
  end
@@ -785,7 +827,8 @@ end
785
827
  <%= link_to "Login via Facebook", "/auth/facebook" %>
786
828
  ```
787
829
 
788
- Let's implement the OmniAuth callback endpoint on our Rodauth controller:
830
+ Finally, let's implement the OmniAuth callback endpoint on our Rodauth
831
+ controller:
789
832
 
790
833
  ```rb
791
834
  # config/routes.rb
@@ -821,7 +864,7 @@ class RodauthController < ApplicationController
821
864
 
822
865
  # create new account if it doesn't exist
823
866
  unless account
824
- account = Account.create!(email: auth["info"]["email"])
867
+ account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
825
868
  end
826
869
 
827
870
  # create new identity if it doesn't exist
@@ -838,11 +881,8 @@ end
838
881
 
839
882
  ## Configuring
840
883
 
841
- For the list of configuration methods provided by Rodauth, see the [feature
842
- documentation].
843
-
844
- The `rails` feature rodauth-rails loads is customizable as well, here is the
845
- list of its configuration methods:
884
+ The `rails` feature rodauth-rails loads provides the following configuration
885
+ methods:
846
886
 
847
887
  | Name | Description |
848
888
  | :---- | :---------- |
@@ -869,21 +909,27 @@ Rodauth::Rails.configure do |config|
869
909
  end
870
910
  ```
871
911
 
912
+ For the list of configuration methods provided by Rodauth, see the [feature
913
+ documentation].
914
+
872
915
  ## Custom extensions
873
916
 
874
917
  When developing custom extensions for Rodauth inside your Rails project, it's
875
- better to use plain modules (at least in the beginning), because Rodauth
876
- feature API doesn't yet support Zeitwerk reloading well.
918
+ probably better to use plain modules, at least in the beginning, as Rodauth
919
+ feature design doesn't yet work well with Zeitwerk reloading.
920
+
921
+ Here is an example of an LDAP authentication extension that uses the
922
+ [simple_ldap_authenticator] gem.
877
923
 
878
924
  ```rb
879
- # app/lib/rodauth_argon2.rb
880
- module RodauthArgon2
881
- def password_hash(password)
882
- Argon2::Password.create(password, t_cost: password_hash_cost, m_cost: password_hash_cost)
925
+ # app/lib/rodauth_ldap.rb
926
+ module RodauthLdap
927
+ def require_bcrypt?
928
+ false
883
929
  end
884
930
 
885
- def password_hash_match?(hash, password)
886
- Argon2::Password.verify_password(password, hash)
931
+ def password_match?(password)
932
+ SimpleLdapAuthenticator.valid?(account[:email], password)
887
933
  end
888
934
  end
889
935
  ```
@@ -893,7 +939,7 @@ class RodauthApp < Rodauth::Rails::App
893
939
  configure do
894
940
  # ...
895
941
  auth_class_eval do
896
- include RodauthArgon2
942
+ include RodauthLdap
897
943
  end
898
944
  # ...
899
945
  end
@@ -902,48 +948,156 @@ end
902
948
 
903
949
  ## Testing
904
950
 
905
- If you're writing system tests, it's generally better to go through the actual
906
- authentication flow with tools like Capybara, and to not use any stubbing.
907
-
908
- In functional and integration tests you can just make requests to Rodauth
909
- routes:
951
+ System (browser) tests for Rodauth actions could look something like this:
910
952
 
911
953
  ```rb
912
- # test/controllers/posts_controller_test.rb
913
- class PostsControllerTest < ActionDispatch::IntegrationTest
914
- test "should require authentication" do
915
- get posts_url
916
- assert_redirected_to "/login"
954
+ # test/system/authentication_test.rb
955
+ require "test_helper"
956
+
957
+ class AuthenticationTest < ActionDispatch::SystemTestCase
958
+ include ActiveJob::TestHelper
959
+ driven_by :rack_test
960
+
961
+ test "creating and verifying an account" do
962
+ create_account
963
+ assert_match "An email has been sent to you with a link to verify your account", page.text
964
+
965
+ verify_account
966
+ assert_match "Your account has been verified", page.text
967
+ end
968
+
969
+ test "logging in and logging out" do
970
+ create_account(verify: true)
971
+
972
+ logout
973
+ assert_match "You have been logged out", page.text
917
974
 
918
975
  login
919
- get posts_url
976
+ assert_match "You have been logged in", page.text
977
+ end
978
+
979
+ private
980
+
981
+ def create_account(email: "user@example.com", password: "secret", verify: false)
982
+ visit "/create-account"
983
+ fill_in "Login", with: email
984
+ fill_in "Password", with: password
985
+ fill_in "Confirm Password", with: password
986
+ click_on "Create Account"
987
+ verify_account if verify
988
+ end
989
+
990
+ def verify_account
991
+ perform_enqueued_jobs # run enqueued email deliveries
992
+ email = ActionMailer::Base.deliveries.last
993
+ verify_account_link = email.body.to_s[/\S+verify-account\S+/]
994
+ visit verify_account_link
995
+ click_on "Verify Account"
996
+ end
997
+
998
+ def login(email: "user@example.com", password: "secret")
999
+ visit "/login"
1000
+ fill_in "Login", with: email
1001
+ fill_in "Password", with: password
1002
+ click_on "Login"
1003
+ end
1004
+
1005
+ def logout
1006
+ visit "/logout"
1007
+ click_on "Logout"
1008
+ end
1009
+ end
1010
+ ```
1011
+
1012
+ While request tests in JSON API mode with JWT tokens could look something like
1013
+ this:
1014
+
1015
+ ```rb
1016
+ # test/integration/authentication_test.rb
1017
+ require "test_helper"
1018
+
1019
+ class AuthenticationTest < ActionDispatch::IntegrationTest
1020
+ test "creating and verifying an account" do
1021
+ create_account
920
1022
  assert_response :success
1023
+ assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
1024
+
1025
+ verify_account
1026
+ assert_response :success
1027
+ assert_match "Your account has been verified", JSON.parse(body)["success"]
1028
+ end
1029
+
1030
+ test "logging in and logging out" do
1031
+ create_account(verify: true)
921
1032
 
922
1033
  logout
923
- assert_redirected_to "/login"
1034
+ assert_response :success
1035
+ assert_match "You have been logged out", JSON.parse(body)["success"]
1036
+
1037
+ login
1038
+ assert_response :success
1039
+ assert_match "You have been logged in", JSON.parse(body)["success"]
924
1040
  end
925
1041
 
926
1042
  private
927
1043
 
928
- def login(login: "user@example.com", password: "secret")
929
- post "/create-account", params: {
930
- "login" => login,
931
- "password" => password,
932
- "password-confirm" => password,
933
- }
934
-
935
- post "/login", params: {
936
- "login" => login,
937
- "password" => password,
938
- }
1044
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1045
+ post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
1046
+ verify_account if verify
1047
+ end
1048
+
1049
+ def verify_account
1050
+ perform_enqueued_jobs # run enqueued email deliveries
1051
+ email = ActionMailer::Base.deliveries.last
1052
+ verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
1053
+ post "/verify-account", as: :json, params: { key: verify_account_key }
1054
+ end
1055
+
1056
+ def login(email: "user@example.com", password: "secret")
1057
+ post "/login", as: :json, params: { login: email, password: password }
939
1058
  end
940
1059
 
941
1060
  def logout
942
- post "/logout"
1061
+ post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
943
1062
  end
944
1063
  end
945
1064
  ```
946
1065
 
1066
+ If you're delivering emails in the background, make sure to set Active Job
1067
+ queue adapter to `:test` or `:inline`:
1068
+
1069
+ ```rb
1070
+ # config/environments/test.rb
1071
+ Rails.application.configure do |config|
1072
+ # ...
1073
+ config.active_job.queue_adapter = :test # or :inline
1074
+ # ...
1075
+ end
1076
+ ```
1077
+
1078
+ If you need to create an account record with a password directly, you can do it
1079
+ as follows:
1080
+
1081
+ ```rb
1082
+ # app/models/account.rb
1083
+ class Account < ApplicationRecord
1084
+ has_one :password_hash, foreign_key: :id
1085
+ end
1086
+ ```
1087
+ ```rb
1088
+ # app/models/account/password_hash.rb
1089
+ class Account::PasswordHash < ApplicationRecord
1090
+ belongs_to :account, foreign_key: :id
1091
+ end
1092
+ ```
1093
+ ```rb
1094
+ require "bcrypt"
1095
+
1096
+ account = Account.create!(email: "user@example.com", status: "verified")
1097
+ password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1098
+ account.create_password_hash!(id: account.id, password_hash: password_hash)
1099
+ ```
1100
+
947
1101
  ## Rodauth defaults
948
1102
 
949
1103
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -1024,6 +1178,18 @@ configure do
1024
1178
  end
1025
1179
  ```
1026
1180
 
1181
+ ### Deadline values
1182
+
1183
+ To simplify changes to the database schema, rodauth-rails configures Rodauth
1184
+ to set deadline values for various features in Ruby, instead of relying on
1185
+ the database to set default column values.
1186
+
1187
+ You can easily change this back:
1188
+
1189
+ ```rb
1190
+ set_deadline_values? false
1191
+ ```
1192
+
1027
1193
  ## License
1028
1194
 
1029
1195
  The gem is available as open source under the terms of the [MIT
@@ -1064,3 +1230,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
1064
1230
  [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
1065
1231
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1066
1232
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1233
+ [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator