rodauth-rails 0.9.0 → 0.13.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 +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