rodauth-rails 0.4.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -1
  3. data/README.md +419 -83
  4. data/lib/generators/rodauth/install_generator.rb +38 -23
  5. data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
  6. data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
  7. data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
  8. data/lib/generators/rodauth/migration/base.erb +19 -0
  9. data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
  10. data/lib/generators/rodauth/migration/email_auth.erb +7 -0
  11. data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
  12. data/lib/generators/rodauth/migration/lockout.erb +11 -0
  13. data/lib/generators/rodauth/migration/otp.erb +7 -0
  14. data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
  15. data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
  16. data/lib/generators/rodauth/migration/remember.erb +6 -0
  17. data/lib/generators/rodauth/migration/reset_password.erb +7 -0
  18. data/lib/generators/rodauth/migration/single_session.erb +5 -0
  19. data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
  20. data/lib/generators/rodauth/migration/verify_account.erb +7 -0
  21. data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
  22. data/lib/generators/rodauth/migration/webauthn.erb +12 -0
  23. data/lib/generators/rodauth/migration_generator.rb +32 -0
  24. data/lib/generators/rodauth/migration_helpers.rb +69 -0
  25. data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
  26. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -20
  27. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  28. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
  29. data/lib/rodauth/rails.rb +33 -4
  30. data/lib/rodauth/rails/app.rb +6 -2
  31. data/lib/rodauth/rails/app/flash.rb +1 -1
  32. data/lib/rodauth/rails/app/middleware.rb +26 -0
  33. data/lib/rodauth/rails/feature.rb +100 -30
  34. data/lib/rodauth/rails/railtie.rb +6 -0
  35. data/lib/rodauth/rails/tasks.rake +28 -0
  36. data/lib/rodauth/rails/version.rb +1 -1
  37. data/rodauth-rails.gemspec +1 -1
  38. metadata +26 -5
  39. data/lib/rodauth/features/rails.rb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 230b201ad8b9f7c27a9ca00db1a27b4166b7ca0d202f32651fc15ff663bb860e
4
- data.tar.gz: b6b7205916994f829f8eb74c76d8b6172c52339b157f464afd2cd9bfa96f3f25
3
+ metadata.gz: 0f1312dbd1bb4dc0d954c77a5ff350b5c9e1ff3fc4dd45b8834cd3e7d0280a22
4
+ data.tar.gz: 5dda5720126361589a428add9b8256b35aa53644166ca7a8a6d14c5baef53f02
5
5
  SHA512:
6
- metadata.gz: fc5645e124796a66c59c51b3b1ab2f1a3b9d7168b53f0703543e42187bb29470d09d3b1103e543dad66ba9a7f940bd469383d767c7acf8fc1d1c8ac7675848e9
7
- data.tar.gz: 89cfd6289466b276240ffbb1111fef0b4274ae2479f5a1dbdaaa193333093508ddc692f47e7a8d57ffeafe70a7d25039371a7ebd88646d962f63f577039afc9c
6
+ metadata.gz: f70e5a44db25c016fe92169be342d1f489cd0e3307fe6c06dbe822c28c05f55dc696b26721836d315daabbbfb0889d18357cec3bb7aa52932649f5ecb08ceedb
7
+ data.tar.gz: 6af3cd43f9266729049d984c9da58beff019dd2f0148465d65c8f814602d9a9678308d752ef469f08ab381a1373b919d1e61b62b204751d95ca57d10ed05de2a
@@ -1,4 +1,42 @@
1
- # 0.4.2 (2020-11-08)
1
+ ## 0.8.0 (2021-01-03)
2
+
3
+ * Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
4
+
5
+ * Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
6
+
7
+ ## 0.7.0 (2020-11-27)
8
+
9
+ * Add `#rails_controller_eval` method for running code in context of a controller instance (@janko)
10
+
11
+ * Detect `secret_key_base` from credentials and `$SECRET_KEY_BASE` environment variable (@janko)
12
+
13
+ ## 0.6.1 (2020-11-25)
14
+
15
+ * Generate the Rodauth controller for API-only Rails apps as well (@janko)
16
+
17
+ * Fix remember cookie deadline not extending in remember feature (@janko)
18
+
19
+ ## 0.6.0 (2020-11-22)
20
+
21
+ * Add `Rodauth::Rails.rodauth` method for retrieving Rodauth instance outside of request context (@janko)
22
+
23
+ * Add default Action Dispatch response headers in Rodauth responses (@janko)
24
+
25
+ * Run controller rescue handlers around Rodauth actions (@janko)
26
+
27
+ * Run controller action callbacks around Rodauth actions (@janko)
28
+
29
+ ## 0.5.0 (2020-11-16)
30
+
31
+ * Support more Active Record adapters in `rodauth:install` generator (@janko)
32
+
33
+ * Add `rodauth:migration` generator for creating tables of specified features (@janko)
34
+
35
+ * Use UUIDs for primary keys if so configured in Rails generators (@janko)
36
+
37
+ * Add `rodauth:routes` rake task for printing routes handled by Rodauth middleware (@janko)
38
+
39
+ ## 0.4.2 (2020-11-08)
2
40
 
3
41
  * Drop support for Ruby 2.2 (@janko)
4
42
 
data/README.md CHANGED
@@ -4,16 +4,57 @@ Provides Rails integration for the [Rodauth] authentication framework.
4
4
 
5
5
  ## Resources
6
6
 
7
+ Useful links:
8
+
7
9
  * [Rodauth documentation](http://rodauth.jeremyevans.net/documentation.html)
8
- * [rodauth-rails wiki](https://github.com/janko/rodauth-rails/wiki)
9
10
  * [Rails demo](https://github.com/janko/rodauth-demo-rails)
10
11
 
12
+ Articles:
13
+
14
+ * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
+ * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
+ * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+
18
+ ## Why Rodauth?
19
+
20
+ 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:
23
+
24
+ * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
25
+ * standardized [JSON API support][jwt] (for every feature)
26
+ * 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
+ * [email authentication][email_auth] (aka "passwordless")
28
+ * [audit logging][audit_logging] (for any action)
29
+ * ability to protect password hashes even in case of SQL injection ([more details][password protection])
30
+ * additional bruteforce protection for tokens ([more details][bruteforce tokens])
31
+ * uniform configuration DSL (any setting can be static or dynamic)
32
+ * consistent before/after hooks around everything
33
+ * dedicated object encapsulating all authentication logic
34
+
35
+ ## Upgrading
36
+
37
+ ### Upgrading to 0.7.0
38
+
39
+ Starting from version 0.7.0, rodauth-rails now correctly detects Rails
40
+ application's `secret_key_base` when setting default `hmac_secret`, including
41
+ when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
42
+ means that your authentication will now be more secure by default, and Rodauth
43
+ features that require `hmac_secret` should now work automatically as well.
44
+
45
+ However, if you've already been using rodauth-rails in production, where the
46
+ `secret_key_base` is set via credentials or environment variable and `hmac_secret`
47
+ was not explicitly set, the fact that your authentication will now start using
48
+ HMACs has backwards compatibility considerations. See the [Rodauth
49
+ documentation][hmac] for instructions on how to safely transition, or just set
50
+ `hmac_secret nil` in your Rodauth configuration.
51
+
11
52
  ## Installation
12
53
 
13
54
  Add the gem to your Gemfile:
14
55
 
15
56
  ```rb
16
- gem "rodauth-rails", "~> 0.4"
57
+ gem "rodauth-rails", "~> 0.6"
17
58
 
18
59
  # gem "jwt", require: false # for JWT feature
19
60
  # gem "rotp", require: false # for OTP feature
@@ -25,10 +66,17 @@ Then run `bundle install`.
25
66
 
26
67
  Next, run the install generator:
27
68
 
28
- ```
69
+ ```sh
29
70
  $ rails generate rodauth:install
30
71
  ```
31
72
 
73
+ Or if you want Rodauth endpoints to be exposed via JSON API:
74
+
75
+ ```sh
76
+ $ rails generate rodauth:install --api
77
+ $ bundle add jwt
78
+ ```
79
+
32
80
  The generator will create the following files:
33
81
 
34
82
  * Rodauth migration at `db/migrate/*_create_rodauth.rb`
@@ -54,7 +102,6 @@ class CreateRodauth < ActiveRecord::Migration
54
102
  create_table :account_verification_keys do |t| ... end
55
103
  create_table :account_login_change_keys do |t| ... end
56
104
  create_table :account_remember_keys do |t| ... end
57
- # ...
58
105
  end
59
106
  end
60
107
  ```
@@ -88,7 +135,7 @@ ActiveRecord connection.
88
135
  require "sequel/core"
89
136
 
90
137
  # initialize Sequel and have it reuse Active Record's database connection
91
- DB = Sequel.postgres(extensions: :activerecord_connection)
138
+ DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
92
139
  ```
93
140
 
94
141
  ### Rodauth app
@@ -112,8 +159,9 @@ end
112
159
 
113
160
  ### Controller
114
161
 
115
- Your Rodauth app will by default use `RodauthController` for view rendering
116
- and CSRF protection.
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.
117
165
 
118
166
  ```rb
119
167
  # app/controllers/rodauth_controller.rb
@@ -121,7 +169,7 @@ class RodauthController < ApplicationController
121
169
  end
122
170
  ```
123
171
 
124
- ### Account Model
172
+ ### Account model
125
173
 
126
174
  Rodauth stores user accounts in the `accounts` table, so the generator will
127
175
  also create an `Account` model for custom use.
@@ -132,56 +180,83 @@ class Account < ApplicationRecord
132
180
  end
133
181
  ```
134
182
 
135
- ## Getting started
183
+ ## Usage
136
184
 
137
- Let's start by adding some basic authentication navigation links to our home
138
- page:
185
+ ### Routes
186
+
187
+ We can see the list of routes our Rodauth middleware handles:
188
+
189
+ ```sh
190
+ $ rails rodauth:routes
191
+ ```
192
+ ```
193
+ Routes handled by RodauthApp:
194
+
195
+ /login rodauth.login_path
196
+ /create-account rodauth.create_account_path
197
+ /verify-account-resend rodauth.verify_account_resend_path
198
+ /verify-account rodauth.verify_account_path
199
+ /change-password rodauth.change_password_path
200
+ /change-login rodauth.change_login_path
201
+ /logout rodauth.logout_path
202
+ /remember rodauth.remember_path
203
+ /reset-password-request rodauth.reset_password_request_path
204
+ /reset-password rodauth.reset_password_path
205
+ /verify-login-change rodauth.verify_login_change_path
206
+ /close-account rodauth.close_account_path
207
+ ```
208
+
209
+ Using this information, we could add some basic authentication links to our
210
+ navigation header:
139
211
 
140
212
  ```erb
141
- <ul>
142
- <% if rodauth.authenticated? %>
143
- <li><%= link_to "Sign out", rodauth.logout_path, method: :post %></li>
144
- <% else %>
145
- <li><%= link_to "Sign in", rodauth.login_path %></li>
146
- <li><%= link_to "Sign up", rodauth.create_account_path %></li>
147
- <% end %>
148
- </ul>
213
+ <% if rodauth.logged_in? %>
214
+ <%= link_to "Sign out", rodauth.logout_path, method: :post %>
215
+ <% else %>
216
+ <%= link_to "Sign in", rodauth.login_path %>
217
+ <%= link_to "Sign up", rodauth.create_account_path %>
218
+ <% end %>
149
219
  ```
150
220
 
151
- These links are fully functional, feel free to visit them and interact with the
221
+ These routes are fully functional, feel free to visit them and interact with the
152
222
  pages. The templates that ship with Rodauth aim to provide a complete
153
223
  authentication experience, and the forms use [Bootstrap] markup.
154
224
 
155
- Let's also load the account record for authenticated requests and expose it via
156
- `#current_account`:
225
+ ### Current account
226
+
227
+ To be able to fetch currently authenticated account, let's define a
228
+ `#current_account` method that fetches the account id from session and
229
+ retrieves the corresponding account record:
157
230
 
158
231
  ```rb
159
232
  # app/controllers/application_controller.rb
160
233
  class ApplicationController < ActionController::Base
161
- before_action :load_account, if: -> { rodauth.authenticated? }
234
+ before_action :current_account, if: -> { rodauth.logged_in? }
162
235
 
163
236
  private
164
237
 
165
- def load_account
166
- @current_account = Account.find(rodauth.session_value)
238
+ def current_account
239
+ @current_account ||= Account.find(rodauth.session_value)
167
240
  rescue ActiveRecord::RecordNotFound
168
241
  rodauth.logout
169
242
  rodauth.login_required
170
243
  end
171
-
172
- attr_reader :current_account
173
244
  helper_method :current_account
174
245
  end
175
246
  ```
247
+
248
+ This allows us to access the current account in controllers and views:
249
+
176
250
  ```erb
177
251
  <p>Authenticated as: <%= current_account.email %></p>
178
252
  ```
179
253
 
180
254
  ### Requiring authentication
181
255
 
182
- Next, we'll likely want to require authentication for certain sections/pages of
183
- our app. We can do this in our Rodauth app's routing block, which helps keep
184
- the authentication logic encapsulated:
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
259
+ encapsulated:
185
260
 
186
261
  ```rb
187
262
  # app/lib/rodauth_app.rb
@@ -239,9 +314,9 @@ end
239
314
 
240
315
  ### Views
241
316
 
242
- The templates built into Rodauth are useful when getting started, but at some
243
- point we'll probably want more control over the markup. For that we can run the
244
- following command:
317
+ The templates built into Rodauth are useful when getting started, but soon
318
+ you'll want to start editing the markup. You can run the following command to
319
+ copy Rodauth templates into your Rails app:
245
320
 
246
321
  ```sh
247
322
  $ rails generate rodauth:views
@@ -265,7 +340,7 @@ $ rails generate rodauth:views --all
265
340
  ```
266
341
 
267
342
  You can also tell the generator to create views into another directory (in this
268
- case don't forget to rename the Rodauth controller accordingly).
343
+ case make sure to rename the Rodauth controller accordingly):
269
344
 
270
345
  ```sh
271
346
  # generates views into app/views/authentication
@@ -300,8 +375,8 @@ end
300
375
 
301
376
  ### Mailer
302
377
 
303
- Rodauth may send emails as part of the authentication flow. Most email settings
304
- can be customized:
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:
305
380
 
306
381
  ```rb
307
382
  # app/lib/rodauth_app.rb
@@ -330,7 +405,7 @@ $ rails generate rodauth:mailer
330
405
  ```
331
406
 
332
407
  This will create a `RodauthMailer` with the associated mailer views in
333
- `app/views/rodauth_mailer` directory.
408
+ `app/views/rodauth_mailer` directory:
334
409
 
335
410
  ```rb
336
411
  # app/mailers/rodauth_mailer.rb
@@ -345,8 +420,8 @@ end
345
420
  ```
346
421
 
347
422
  You can then uncomment the lines in your Rodauth configuration to have it call
348
- your mailer. If you've enabled additional authentication features, make sure to
349
- override their `send_*_email` methods as well.
423
+ your mailer. If you've enabled additional authentication features that send
424
+ emails, make sure to override their `create_*_email` methods as well.
350
425
 
351
426
  ```rb
352
427
  # app/lib/rodauth_app.rb
@@ -354,67 +429,102 @@ class RodauthApp < Rodauth::Rails::App
354
429
  # ...
355
430
  configure do
356
431
  # ...
357
- send_reset_password_email do
358
- mailer_send(:reset_password, email_to, reset_password_email_link)
432
+ create_reset_password_email do
433
+ RodauthMailer.reset_password(email_to, reset_password_email_link)
359
434
  end
360
- send_verify_account_email do
361
- mailer_send(:verify_account, email_to, verify_account_email_link)
435
+ create_verify_account_email do
436
+ RodauthMailer.verify_account(email_to, verify_account_email_link)
362
437
  end
363
- send_verify_login_change_email do |login|
364
- mailer_send(:verify_login_change, login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
438
+ create_verify_login_change_email do |login|
439
+ RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
365
440
  end
366
- send_password_changed_email do
367
- mailer_send(:password_changed, email_to)
441
+ create_password_changed_email do
442
+ RodauthMailer.password_changed(email_to)
368
443
  end
369
- # send_email_auth_email do
370
- # mailer_send(:email_auth, email_to, email_auth_email_link)
444
+ # create_email_auth_email do
445
+ # RodauthMailer.email_auth(email_to, email_auth_email_link)
371
446
  # end
372
- # send_unlock_account_email do
373
- # mailer_send(:unlock_account, email_to, unlock_account_email_link)
447
+ # create_unlock_account_email do
448
+ # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
374
449
  # end
375
- auth_class_eval do
450
+ send_email do |email|
376
451
  # queue email delivery on the mailer after the transaction commits
377
- def mailer_send(type, *args)
378
- db.after_commit do
379
- RodauthMailer.public_send(type, *args).deliver_later
380
- end
381
- end
452
+ db.after_commit { email.deliver_later }
382
453
  end
383
454
  # ...
384
455
  end
385
456
  end
386
457
  ```
387
458
 
388
- ### JSON API
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.
463
+
464
+ ### Migrations
389
465
 
390
- JSON API support in Rodauth is provided by the [JWT feature]. First you'll need
391
- to add the [JWT gem] to your Gemfile:
466
+ The install generator will create a migration for tables used by the Rodauth
467
+ features enabled by default. For any additional features, you can use the
468
+ migration generator to create the corresponding tables:
392
469
 
470
+ ```sh
471
+ $ rails generate rodauth:migration otp sms_codes recovery_codes
472
+ ```
393
473
  ```rb
394
- gem "jwt"
474
+ # db/migration/*_create_rodauth_otp_sms_codes_recovery_codes.rb
475
+ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
476
+ def change
477
+ create_table :account_otp_keys do |t| ... end
478
+ create_table :account_sms_codes do |t| ... end
479
+ create_table :account_recovery_codes do |t| ... end
480
+ end
481
+ end
395
482
  ```
396
483
 
397
- The following configuration will enable the Rodauth endpoints to be accessed
398
- via JSON requests (in addition to HTML requests):
484
+ ### Calling controller methods
399
485
 
486
+ When using Rodauth before/after hooks or generally overriding your Rodauth
487
+ configuration, in some cases you might want to call methods defined on your
488
+ controllers. You can do so with `rails_controller_eval`, for example:
489
+
490
+ ```rb
491
+ # app/controllers/application_controller.rb
492
+ class ApplicationController < ActionController::Base
493
+ private
494
+ def setup_tracking(account_id)
495
+ # ... some implementation ...
496
+ end
497
+ end
498
+ ```
400
499
  ```rb
401
500
  # app/lib/rodauth_app.rb
402
501
  class RodauthApp < Rodauth::Rails::App
403
- configure(json: true) do
404
- # ...
405
- enable :jwt
406
- jwt_secret "...your secret key..."
407
- # ...
502
+ configure do
503
+ after_create_account do
504
+ rails_controller_eval { setup_tracking(account_id) }
505
+ end
408
506
  end
409
507
  end
410
508
  ```
411
509
 
412
- If you want the endpoints to be only accessible via JSON requests, or if your
413
- Rails app is in API-only mode, instead of `json: true` pass `json: :only` to
414
- the configure method.
510
+ ### Rodauth instance
511
+
512
+ In some cases you might need to use Rodauth more programmatically, and perform
513
+ Rodauth operations outside of the request context. rodauth-rails gives you the
514
+ ability to retrieve the Rodauth instance:
515
+
516
+ ```rb
517
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
518
+
519
+ rodauth.login_url #=> "https://example.com/login"
520
+ rodauth.account_from_login("user@example.com") # loads user by email
521
+ rodauth.password_match?("secret") #=> true
522
+ rodauth.setup_account_verification
523
+ rodauth.close_account
524
+ ```
415
525
 
416
- Make sure to store the `jwt_secret` in a secure place, such as Rails
417
- credentials or environment variables.
526
+ This Rodauth instance will be initialized with basic Rack env that allows is it
527
+ to generate URLs, using `config.action_mailer.default_url_options` options.
418
528
 
419
529
  ## How it works
420
530
 
@@ -460,19 +570,45 @@ end
460
570
  The `Rodauth::Rails::App` class is a [Roda] subclass that provides Rails
461
571
  integration for Rodauth:
462
572
 
463
- * uses Rails' flash instead of Roda's
464
- * uses Rails' CSRF protection instead of Roda's
573
+ * uses Action Dispatch flash instead of Roda's
574
+ * uses Action Dispatch CSRF protection instead of Roda's
465
575
  * sets [HMAC] secret to Rails' secret key base
466
- * uses ActionController for rendering templates
467
- * uses ActionMailer for sending emails
576
+ * uses Action Controller for rendering templates
577
+ * runs Action Controller callbacks & rescue handlers around Rodauth actions
578
+ * uses Action Mailer for sending emails
468
579
 
469
- The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
580
+ The `configure` method wraps configuring the Rodauth plugin, forwarding
470
581
  any additional [plugin options].
471
582
 
472
583
  ```rb
473
- configure { ... } # defining default Rodauth configuration
474
- configure(json: true) { ... } # passing options to the Rodauth plugin
475
- configure(:secondary) { ... } # defining multiple Rodauth configurations
584
+ class RodauthApp < Rodauth::Rails::App
585
+ configure { ... } # defining default Rodauth configuration
586
+ configure(json: true) { ... } # passing options to the Rodauth plugin
587
+ configure(:secondary) { ... } # defining multiple Rodauth configurations
588
+ end
589
+ ```
590
+
591
+ The `route` block is provided by Roda, and it's called on each request before
592
+ it reaches the Rails router.
593
+
594
+ ```rb
595
+ class RodauthApp < Rodauth::Rails::App
596
+ route do |r|
597
+ # ... called before each request ...
598
+ end
599
+ end
600
+ ```
601
+
602
+ Since `Rodauth::Rails::App` is just a Roda subclass, you can do anything you
603
+ would with a Roda app, such as loading additional Roda plugins:
604
+
605
+ ```rb
606
+ class RodauthApp < Rodauth::Rails::App
607
+ plugin :request_headers # easier access to request headers
608
+ plugin :typecast_params # methods for conversion of request params
609
+ plugin :default_headers, { "Foo" => "Bar" }
610
+ # ...
611
+ end
476
612
  ```
477
613
 
478
614
  ### Sequel
@@ -488,6 +624,142 @@ connection (using the [sequel-activerecord_connection] gem).
488
624
  This means that, from the usage perspective, Sequel can be considered just
489
625
  as an implementation detail of Rodauth.
490
626
 
627
+ ## JSON API
628
+
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:
631
+
632
+ ```sh
633
+ $ bundle add jwt
634
+ ```
635
+ ```rb
636
+ # app/lib/rodauth_app.rb
637
+ class RodauthApp < Rodauth::Rails::App
638
+ configure(json: :only) do
639
+ # ...
640
+ enable :jwt
641
+ # make sure to store the JWT secret below in a safe place
642
+ jwt_secret "...your secret key..."
643
+ # ...
644
+ end
645
+ end
646
+ ```
647
+
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`.
651
+
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.
655
+
656
+ ## OmniAuth
657
+
658
+ While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
659
+ ourselves using the existing Rodauth API.
660
+
661
+ In order to allow the user to login via multiple external providers, let's
662
+ create an `account_identities` table that will have a many-to-one relationship
663
+ with the `accounts` table:
664
+
665
+ ```sh
666
+ $ rails generate model AccountIdentity
667
+ ```
668
+ ```rb
669
+ # db/migrate/*_create_account_identities.rb
670
+ class CreateAccountIdentities < ActiveRecord::Migration
671
+ def change
672
+ create_table :account_identities do |t|
673
+ t.references :account, null: false, foreign_key: { on_delete: :cascade }
674
+ t.string :provider, null: false
675
+ t.string :uid, null: false
676
+ t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
677
+
678
+ t.timestamps
679
+
680
+ t.index [:provider, :uid], unique: true
681
+ end
682
+ end
683
+ end
684
+ ```
685
+ ```rb
686
+ # app/models/account_identity.rb
687
+ class AcccountIdentity < ApplicationRecord
688
+ belongs_to :account
689
+ end
690
+ ```
691
+ ```rb
692
+ # app/models/account.rb
693
+ class Account < ApplicationRecord
694
+ has_many :identities, class_name: "AccountIdentity"
695
+ end
696
+ ```
697
+
698
+ Let's assume we want to implement Facebook login, and have added the
699
+ corresponding OmniAuth strategy to the middleware stack, together with an
700
+ authorization link on the login form:
701
+
702
+ ```rb
703
+ Rails.application.config.middleware.use OmniAuth::Builder do
704
+ provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
705
+ scope: "email", callback_path: "/auth/facebook/callback"
706
+ end
707
+ ```
708
+ ```erb
709
+ <%= link_to "Login via Facebook", "/auth/facebook" %>
710
+ ```
711
+
712
+ Let's implement the OmniAuth callback endpoint on our Rodauth controller:
713
+
714
+ ```rb
715
+ # config/routes.rb
716
+ Rails.application.routes.draw do
717
+ # ...
718
+ get "/auth/:provider/callback", to: "rodauth#omniauth"
719
+ end
720
+ ```
721
+ ```rb
722
+ # app/controllres/rodauth_controller.rb
723
+ class RodauthController < ApplicationController
724
+ def omniauth
725
+ auth = request.env["omniauth.auth"]
726
+
727
+ # attempt to find existing identity directly
728
+ identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
729
+
730
+ if identity
731
+ # update any external info changes
732
+ identity.update!(info: auth["info"])
733
+ # set account from identity
734
+ account = identity.account
735
+ end
736
+
737
+ # attempt to find an existing account by email
738
+ account ||= Account.find_by(email: auth["info"]["email"])
739
+
740
+ # disallow login if account is not verified
741
+ if account && account.status != rodauth.account_open_status_value
742
+ redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
743
+ return
744
+ end
745
+
746
+ # create new account if it doesn't exist
747
+ unless account
748
+ account = Account.create!(email: auth["info"]["email"])
749
+ end
750
+
751
+ # create new identity if it doesn't exist
752
+ unless identity
753
+ account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
754
+ end
755
+
756
+ # login with Rodauth
757
+ rodauth.account_from_login(account.email)
758
+ rodauth.login("omniauth")
759
+ end
760
+ end
761
+ ```
762
+
491
763
  ## Configuring
492
764
 
493
765
  For the list of configuration methods provided by Rodauth, see the [feature
@@ -521,6 +793,37 @@ Rodauth::Rails.configure do |config|
521
793
  end
522
794
  ```
523
795
 
796
+ ## Custom extensions
797
+
798
+ 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.
801
+
802
+ ```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)
807
+ end
808
+
809
+ def password_hash_match?(hash, password)
810
+ Argon2::Password.verify_password(password, hash)
811
+ end
812
+ end
813
+ ```
814
+ ```rb
815
+ # app/lib/rodauth_app.rb
816
+ class RodauthApp < Rodauth::Rails::App
817
+ configure do
818
+ # ...
819
+ auth_class_eval do
820
+ include RodauthArgon2
821
+ end
822
+ # ...
823
+ end
824
+ end
825
+ ```
826
+
524
827
  ## Testing
525
828
 
526
829
  If you're writing system tests, it's generally better to go through the actual
@@ -588,6 +891,24 @@ disables the use of database functions, though you can always turn it back on.
588
891
  use_database_authentication_functions? true
589
892
  ```
590
893
 
894
+ To create the database functions, pass the Sequel database object into the
895
+ Rodauth method for creating database functions:
896
+
897
+ ```rb
898
+ # db/migrate/*_create_rodauth_database_functions.rb
899
+ require "rodauth/migrations"
900
+
901
+ class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
902
+ def up
903
+ Rodauth.create_database_authentication_functions(DB)
904
+ end
905
+
906
+ def down
907
+ Rodauth.drop_database_authentication_functions(DB)
908
+ end
909
+ end
910
+ ```
911
+
591
912
  ### Account statuses
592
913
 
593
914
  The recommended [Rodauth migration] stores possible account status values in a
@@ -640,9 +961,7 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
640
961
 
641
962
  [Rodauth]: https://github.com/jeremyevans/rodauth
642
963
  [Sequel]: https://github.com/jeremyevans/sequel
643
- [rendering views outside of controllers]: https://blog.bigbinary.com/2016/01/08/rendering-views-outside-of-controllers-in-rails-5.html
644
964
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
645
- [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
646
965
  [JWT gem]: https://github.com/jwt/ruby-jwt
647
966
  [Bootstrap]: https://getbootstrap.com/
648
967
  [Roda]: http://roda.jeremyevans.net/
@@ -651,3 +970,20 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
651
970
  [Rodauth migration]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Creating+tables
652
971
  [sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
653
972
  [plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
973
+ [hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
974
+ [OmniAuth]: https://github.com/omniauth/omniauth
975
+ [otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
976
+ [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
977
+ [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
978
+ [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
979
+ [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
980
+ [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
981
+ [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
982
+ [password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
983
+ [bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
984
+ [password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
985
+ [disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
986
+ [password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
987
+ [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
988
+ [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
989
+ [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html