rodauth-rails 0.8.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3f1fd01ddd20052bd15e3c822ff44ca4a6f5d425dd18892ecffa34146364437
4
- data.tar.gz: 8907b8616edf882d21ebff69b461cc12dae737f8ec34bdaf5c2d58ac5cc9b632
3
+ metadata.gz: b8063be8ad00634114f74f0eb549c672e2b62cd1fa81cb7f124cc9cd12505e3f
4
+ data.tar.gz: 6f466e29420f9e4bacb58c855e942cc20289d2c3fc69a12638b97628d25dbbfb
5
5
  SHA512:
6
- metadata.gz: 917915fc72c29b668716d3234f2f85e8c92406ea14a63e82284750a5536ec016acb8714064888732eed9fb1443cb803c11e0764b23a76de9de64a9fda11d577f
7
- data.tar.gz: f30214be433882a3be04fb988fcd1d4860c50c8cb8beced94f8b7529f45904c8a8c1eb12486e39f890ccce39f1321a02f1e92fce2cc07ca24f1a86509ddd4a59
6
+ metadata.gz: 8cc0af59c6ce29837fbc8a3401d456fd407ef76b74493b08ee9b4f2dfc8807d4a95c86f9bb0266401013d5162c009d46b7d07e3f741654af2cc267c0ee2c135e
7
+ data.tar.gz: 78c098dbaed458d5764ca2e7ee61f4710e01b2386d0cc04831b1732b9883d76c4b9f56c35c0a1e557c40951086d72bb0ed264f769313c1b45be30b2dd760024a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## 0.11.0 (2021-05-06)
2
+
3
+ * Add controller-like logging for requests to Rodauth endpoints (@janko)
4
+
5
+ * Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
6
+
7
+ * Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
8
+
9
+ ## 0.10.0 (2021-03-23)
10
+
11
+ * Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
12
+
13
+ * Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
14
+
15
+ * Improve Roda middleware inspect output (@janko)
16
+
17
+ * Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
18
+
19
+ * Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
20
+
21
+ * Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
22
+
23
+ ## 0.9.1 (2021-02-10)
24
+
25
+ * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
26
+
27
+ * Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
28
+
29
+ ## 0.9.0 (2021-02-07)
30
+
31
+ * Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
32
+
33
+ * Bump Rodauth dependency to 2.9+ (@janko)
34
+
35
+ * Add `--json` option for `rodauth:install` generator for configuring `json` feature (@janko)
36
+
37
+ * Add `--jwt` option for `rodauth:install` generator for configuring `jwt` feature (@janko)
38
+
39
+ * Remove the `--api` option from `rodauth:install` generator (@janko)
40
+
41
+ ## 0.8.2 (2021-01-10)
42
+
43
+ * Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
44
+
1
45
  ## 0.8.1 (2021-01-04)
2
46
 
3
47
  * Fix blank email body when `json: true` and `ActionController::API` descendant are used (@janko)
data/README.md CHANGED
@@ -14,15 +14,16 @@ Articles:
14
14
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
15
  * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
16
  * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+ * [How to build an OIDC provider using rodauth-oauth on Rails](https://honeyryderchuck.gitlab.io/httpx/2021/03/15/oidc-provider-on-rails-using-rodauth-oauth.html)
17
18
 
18
19
  ## Why Rodauth?
19
20
 
20
21
  There are already several popular authentication solutions for Rails (Devise,
21
- Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Well, because
22
- it has many advantages over the mentioned alternatives:
22
+ Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Here are some
23
+ of the advantages that stand out for me:
23
24
 
24
25
  * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
25
- * standardized [JSON API support][jwt] (for every feature)
26
+ * standardized [JSON API support][json] for every feature (including [JWT][jwt])
26
27
  * enterprise security features ([password complexity][password_complexity], [disallow password reuse][disallow_password_reuse], [password expiration][password_expiration], [session expiration][session_expiration], [single session][single_session], [account expiration][account_expiration])
27
28
  * [email authentication][email_auth] (aka "passwordless")
28
29
  * [audit logging][audit_logging] (for any action)
@@ -32,6 +33,12 @@ it has many advantages over the mentioned alternatives:
32
33
  * consistent before/after hooks around everything
33
34
  * dedicated object encapsulating all authentication logic
34
35
 
36
+ One commmon concern is the fact that, unlike most other authentication
37
+ frameworks for Rails, Rodauth uses [Sequel] for database interaction instead of
38
+ Active Record. There are good reasons for this, and to make Rodauth work
39
+ smoothly alongside Active Record, rodauth-rails configures Sequel to [reuse
40
+ Active Record's database connection][sequel-activerecord_connection].
41
+
35
42
  ## Upgrading
36
43
 
37
44
  ### Upgrading to 0.7.0
@@ -54,7 +61,7 @@ documentation][hmac] for instructions on how to safely transition, or just set
54
61
  Add the gem to your Gemfile:
55
62
 
56
63
  ```rb
57
- gem "rodauth-rails", "~> 0.6"
64
+ gem "rodauth-rails", "~> 0.10"
58
65
 
59
66
  # gem "jwt", require: false # for JWT feature
60
67
  # gem "rotp", require: false # for OTP feature
@@ -73,7 +80,9 @@ $ rails generate rodauth:install
73
80
  Or if you want Rodauth endpoints to be exposed via JSON API:
74
81
 
75
82
  ```sh
76
- $ rails generate rodauth:install --api
83
+ $ rails generate rodauth:install --json # regular authentication using the Rails session
84
+ # or
85
+ $ rails generate rodauth:install --jwt # token authentication via the "Authorization" header
77
86
  $ bundle add jwt
78
87
  ```
79
88
 
@@ -85,6 +94,7 @@ The generator will create the following files:
85
94
  * Rodauth app at `app/lib/rodauth_app.rb`
86
95
  * Rodauth controller at `app/controllers/rodauth_controller.rb`
87
96
  * Account model at `app/models/account.rb`
97
+ * Rodauth mailer at `app/mailers/rodauth_mailer.rb` with views
88
98
 
89
99
  ### Migration
90
100
 
@@ -180,6 +190,23 @@ class Account < ApplicationRecord
180
190
  end
181
191
  ```
182
192
 
193
+ ### Rodauth mailer
194
+
195
+ The default Rodauth app is configured to use `RodauthMailer` mailer
196
+ for sending authentication emails.
197
+
198
+ ```rb
199
+ # app/mailers/rodauth_mailer.rb
200
+ class RodauthMailer < ApplicationMailer
201
+ def verify_account(recipient, email_link) ... end
202
+ def reset_password(recipient, email_link) ... end
203
+ def verify_login_change(recipient, old_login, new_login, email_link) ... end
204
+ def password_changed(recipient) ... end
205
+ # def email_auth(recipient, email_link) ... end
206
+ # def unlock_account(recipient, email_link) ... end
207
+ end
208
+ ```
209
+
183
210
  ## Usage
184
211
 
185
212
  ### Routes
@@ -222,6 +249,19 @@ These routes are fully functional, feel free to visit them and interact with the
222
249
  pages. The templates that ship with Rodauth aim to provide a complete
223
250
  authentication experience, and the forms use [Bootstrap] markup.
224
251
 
252
+ Inside Rodauth configuration and the `route` block you can access Rails route
253
+ helpers through `#rails_routes`:
254
+
255
+ ```rb
256
+ class RodauthApp < Rodauth::Rails::App
257
+ configure do
258
+ # ...
259
+ login_redirect { rails_routes.activity_path }
260
+ # ...
261
+ end
262
+ end
263
+ ```
264
+
225
265
  ### Current account
226
266
 
227
267
  To be able to fetch currently authenticated account, let's define a
@@ -241,7 +281,7 @@ class ApplicationController < ActionController::Base
241
281
  rodauth.logout
242
282
  rodauth.login_required
243
283
  end
244
- helper_method :current_account
284
+ helper_method :current_account # skip if inheriting from ActionController:API
245
285
  end
246
286
  ```
247
287
 
@@ -299,15 +339,52 @@ class PostsController < ApplicationController
299
339
  end
300
340
  ```
301
341
 
302
- Or at the Rails router level:
342
+ #### Routing constraints
343
+
344
+ You can also require authentication at the Rails router level by
345
+ using a built-in `authenticated` routing constraint:
303
346
 
304
347
  ```rb
305
348
  # config/routes.rb
306
349
  Rails.application.routes.draw do
307
- constraints -> (r) { r.env["rodauth"].require_authentication } do
308
- namespace :admin do
309
- # ...
310
- end
350
+ constraints Rodauth::Rails.authenticated do
351
+ # ... authenticated routes ...
352
+ end
353
+ end
354
+ ```
355
+
356
+ If you want additional conditions, you can pass in a block, which is
357
+ called with the Rodauth instance:
358
+
359
+ ```rb
360
+ # config/routes.rb
361
+ Rails.application.routes.draw do
362
+ # require multifactor authentication to be setup
363
+ constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
364
+ # ...
365
+ end
366
+ end
367
+ ```
368
+
369
+ You can specify the Rodauth configuration by passing the configuration name:
370
+
371
+ ```rb
372
+ # config/routes.rb
373
+ Rails.application.routes.draw do
374
+ constraints Rodauth::Rails.authenticated(:admin) do
375
+ # ...
376
+ end
377
+ end
378
+ ```
379
+
380
+ If you need something more custom, you can always create the routing constraint
381
+ manually:
382
+
383
+ ```rb
384
+ # config/routes.rb
385
+ Rails.application.routes.draw do
386
+ constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
387
+ # routes when the user is not logged in
311
388
  end
312
389
  end
313
390
  ```
@@ -375,58 +452,36 @@ end
375
452
 
376
453
  ### Mailer
377
454
 
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:
455
+ The install generator will create `RodauthMailer` with default email templates,
456
+ and configure Rodauth features that send emails as part of the authentication
457
+ flow to use it.
380
458
 
381
459
  ```rb
382
- # app/lib/rodauth_app.rb
383
- class RodauthApp < Rodauth::Rails::App
384
- # ...
385
- configure do
460
+ # app/mailers/rodauth_mailer.rb
461
+ class RodauthMailer < ApplicationMailer
462
+ def verify_account(recipient, email_link)
386
463
  # ...
387
- # general settings
388
- email_from "no-reply@myapp.com"
389
- email_subject_prefix "[MyApp] "
390
- send_email(&:deliver_later)
464
+ end
465
+ def reset_password(recipient, email_link)
391
466
  # ...
392
- # feature settings
393
- verify_account_email_subject "Verify your account"
394
- verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
467
+ end
468
+ def verify_login_change(recipient, old_login, new_login, email_link)
395
469
  # ...
396
470
  end
471
+ def password_changed(recipient)
472
+ # ...
473
+ end
474
+ # def email_auth(recipient, email_link)
475
+ # ...
476
+ # end
477
+ # def unlock_account(recipient, email_link)
478
+ # ...
479
+ # end
397
480
  end
398
481
  ```
399
-
400
- This is convenient when starting out, but eventually you might want to use your
401
- own mailer. You can start by running the following command:
402
-
403
- ```sh
404
- $ rails generate rodauth:mailer
405
- ```
406
-
407
- This will create a `RodauthMailer` with the associated mailer views in
408
- `app/views/rodauth_mailer` directory:
409
-
410
- ```rb
411
- # app/mailers/rodauth_mailer.rb
412
- class RodauthMailer < ApplicationMailer
413
- def verify_account(recipient, email_link) ... end
414
- def reset_password(recipient, email_link) ... end
415
- def verify_login_change(recipient, old_login, new_login, email_link) ... end
416
- def password_changed(recipient) ... end
417
- # def email_auth(recipient, email_link) ... end
418
- # def unlock_account(recipient, email_link) ... end
419
- end
420
- ```
421
-
422
- You can then uncomment the lines in your Rodauth configuration to have it call
423
- your mailer. If you've enabled additional authentication features that send
424
- emails, make sure to override their `create_*_email` methods as well.
425
-
426
482
  ```rb
427
483
  # app/lib/rodauth_app.rb
428
484
  class RodauthApp < Rodauth::Rails::App
429
- # ...
430
485
  configure do
431
486
  # ...
432
487
  create_reset_password_email do
@@ -456,10 +511,17 @@ class RodauthApp < Rodauth::Rails::App
456
511
  end
457
512
  ```
458
513
 
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.
514
+ This configuration calls `#deliver_later`, which uses Active Job to deliver
515
+ emails in a background job. It's generally recommended to send emails
516
+ asynchronously for better request throughput and the ability to retry
517
+ deliveries. However, if you want to send emails synchronously, modify the
518
+ configuration to call `#deliver_now` instead.
519
+
520
+ If you're using a background processing library without an Active Job adapter,
521
+ or a 3rd-party service for sending transactional emails, this two-phase API
522
+ might not be suitable. In this case, instead of overriding `#create_*_email`
523
+ and `#send_email`, override the `#send_*_email` methods instead, which are
524
+ required to send the email immediately.
463
525
 
464
526
  ### Migrations
465
527
 
@@ -481,6 +543,143 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
481
543
  end
482
544
  ```
483
545
 
546
+ ### Multiple configurations
547
+
548
+ If you need to handle multiple types of accounts that require different
549
+ authentication logic, you can create different configurations for them:
550
+
551
+ ```rb
552
+ # app/lib/rodauth_app.rb
553
+ class RodauthApp < Rodauth::Rails::App
554
+ # primary configuration
555
+ configure do
556
+ # ...
557
+ end
558
+
559
+ # alternative configuration
560
+ configure(:admin) do
561
+ # ... enable features ...
562
+ prefix "/admin"
563
+ session_key_prefix "admin_"
564
+ remember_cookie_key "_admin_remember" # if using remember feature
565
+
566
+ # if you want separate tables
567
+ accounts_table :admin_accounts
568
+ password_hash_table :admin_account_password_hashes
569
+ # ...
570
+ end
571
+
572
+ route do |r|
573
+ r.rodauth
574
+
575
+ r.on "admin" do
576
+ r.rodauth(:admin)
577
+ r.pass # allow the Rails app to handle other "/admin/*" requests
578
+ end
579
+
580
+ # ...
581
+ end
582
+ end
583
+ ```
584
+
585
+ Then in your application you can reference the secondary Rodauth instance:
586
+
587
+ ```rb
588
+ rodauth(:admin).login_path #=> "/admin/login"
589
+ ```
590
+
591
+ #### Named auth classes
592
+
593
+ A `configure` block inside `Rodauth::Rails::App` will internally create an
594
+ anonymous `Rodauth::Auth` subclass, and register it under the given name.
595
+ However, you can also define the auth classes explicitly, by creating
596
+ subclasses of `Rodauth::Rails::Auth`:
597
+
598
+ ```rb
599
+ # app/lib/rodauth_main.rb
600
+ class RodauthMain < Rodauth::Rails::Auth
601
+ configure do
602
+ # ... main configuration ...
603
+ end
604
+ end
605
+ ```
606
+ ```rb
607
+ # app/lib/rodauth_admin.rb
608
+ class RodauthAdmin < Rodauth::Rails::Auth
609
+ configure do
610
+ # ...
611
+ prefix "/admin"
612
+ session_key_prefix "admin_"
613
+ # ...
614
+ end
615
+ end
616
+ ```
617
+ ```rb
618
+ # app/lib/rodauth_app.rb
619
+ class RodauthApp < Rodauth::Rails::App
620
+ configure RodauthMain
621
+ configure RodauthAdmin, :admin
622
+ # ...
623
+ end
624
+ ```
625
+
626
+ This allows having each configuration in a dedicated file, and named constants
627
+ improve introspection and error messages. You can also use inheritance to share
628
+ common settings:
629
+
630
+ ```rb
631
+ # app/lib/rodauth_base.rb
632
+ class RodauthBase < Rodauth::Rails::Auth
633
+ # common settings that can be shared between multiple configurations
634
+ configure do
635
+ enable :login, :logout
636
+ login_return_to_requested_location? true
637
+ logout_redirect "/"
638
+ # ...
639
+ end
640
+ end
641
+ ```
642
+ ```rb
643
+ # app/lib/rodauth_main.rb
644
+ class RodauthMain < RodauthBase # inherit common settings
645
+ configure do
646
+ # ... customize main ...
647
+ end
648
+ end
649
+ ```
650
+ ```rb
651
+ # app/lib/rodauth_admin.rb
652
+ class RodauthAdmin < RodauthBase # inherit common settings
653
+ configure do
654
+ # ... customize admin ...
655
+ end
656
+ end
657
+ ```
658
+
659
+ Another benefit is that you can define custom methods directly on the class
660
+ instead of through `auth_class_eval`:
661
+
662
+ ```rb
663
+ # app/lib/rodauth_admin.rb
664
+ class RodauthAdmin < Rodauth::Rails::Auth
665
+ configure do
666
+ # ...
667
+ end
668
+
669
+ def superadmin?
670
+ Role.where(account_id: session_id, type: "superadmin").any?
671
+ end
672
+ end
673
+ ```
674
+ ```rb
675
+ # config/routes.rb
676
+ Rails.application.routes.draw do
677
+ constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
678
+ mount Sidekiq::Web => "sidekiq"
679
+ end
680
+ end
681
+ ```
682
+
484
683
  ### Calling controller methods
485
684
 
486
685
  When using Rodauth before/after hooks or generally overriding your Rodauth
@@ -514,7 +713,7 @@ Rodauth operations outside of the request context. rodauth-rails gives you the
514
713
  ability to retrieve the Rodauth instance:
515
714
 
516
715
  ```rb
517
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
716
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
518
717
 
519
718
  rodauth.login_url #=> "https://example.com/login"
520
719
  rodauth.account_from_login("user@example.com") # loads user by email
@@ -545,8 +744,8 @@ The Rodauth app stores the `Rodauth::Auth` instance in the Rack env hash, which
545
744
  is then available in your Rails app:
546
745
 
547
746
  ```rb
548
- request.env["rodauth"] #=> #<Rodauth::Auth>
549
- request.env["rodauth.secondary"] #=> #<Rodauth::Auth> (if using multiple configurations)
747
+ request.env["rodauth"] #=> #<Rodauth::Auth>
748
+ request.env["rodauth.admin"] #=> #<Rodauth::Auth> (if using multiple configurations)
550
749
  ```
551
750
 
552
751
  For convenience, this object can be accessed via the `#rodauth` method in views
@@ -555,14 +754,14 @@ and controllers:
555
754
  ```rb
556
755
  class MyController < ApplicationController
557
756
  def my_action
558
- rodauth #=> #<Rodauth::Auth>
559
- rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations)
757
+ rodauth #=> #<Rodauth::Auth>
758
+ rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations)
560
759
  end
561
760
  end
562
761
  ```
563
762
  ```erb
564
- <% rodauth #=> #<Rodauth::Auth> %>
565
- <% rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations) %>
763
+ <% rodauth #=> #<Rodauth::Auth> %>
764
+ <% rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations) %>
566
765
  ```
567
766
 
568
767
  ### App
@@ -584,7 +783,7 @@ any additional [plugin options].
584
783
  class RodauthApp < Rodauth::Rails::App
585
784
  configure { ... } # defining default Rodauth configuration
586
785
  configure(json: true) { ... } # passing options to the Rodauth plugin
587
- configure(:secondary) { ... } # defining multiple Rodauth configurations
786
+ configure(:admin) { ... } # defining multiple Rodauth configurations
588
787
  end
589
788
  ```
590
789
 
@@ -619,15 +818,32 @@ function calls).
619
818
 
620
819
  If ActiveRecord is used in the application, the `rodauth:install` generator
621
820
  will have automatically configured Sequel to reuse ActiveRecord's database
622
- connection (using the [sequel-activerecord_connection] gem).
821
+ connection, using the [sequel-activerecord_connection] gem.
623
822
 
624
823
  This means that, from the usage perspective, Sequel can be considered just
625
824
  as an implementation detail of Rodauth.
626
825
 
627
826
  ## JSON API
628
827
 
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:
828
+ To make Rodauth endpoints accessible via JSON API, enable the [`json`][json]
829
+ feature:
830
+
831
+ ```rb
832
+ # app/lib/rodauth_app.rb
833
+ class RodauthApp < Rodauth::Rails::App
834
+ configure do
835
+ # ...
836
+ enable :json
837
+ only_json? true # accept only JSON requests
838
+ # ...
839
+ end
840
+ end
841
+ ```
842
+
843
+ This will store account session data into the Rails session. If you rather want
844
+ stateless token-based authentication via the `Authorization` header, enable the
845
+ [`jwt`][jwt] feature (which builds on top of the `json` feature) and add the
846
+ [JWT gem] to the Gemfile:
631
847
 
632
848
  ```sh
633
849
  $ bundle add jwt
@@ -635,23 +851,33 @@ $ bundle add jwt
635
851
  ```rb
636
852
  # app/lib/rodauth_app.rb
637
853
  class RodauthApp < Rodauth::Rails::App
638
- configure(json: :only) do
854
+ configure do
639
855
  # ...
640
856
  enable :jwt
641
- # make sure to store the JWT secret below in a safe place
642
- jwt_secret "...your secret key..."
857
+ jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
858
+ only_json? true # accept only JSON requests
643
859
  # ...
644
860
  end
645
861
  end
646
862
  ```
647
863
 
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`.
864
+ If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
865
+ corresponding Rodauth features and create the necessary tables:
651
866
 
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.
867
+ ```sh
868
+ $ rails generate rodauth:migration jwt_refresh
869
+ $ rails db:migrate
870
+ ```
871
+ ```rb
872
+ # app/lib/rodauth_app.rb
873
+ class RodauthApp < Rodauth::Rails::App
874
+ configure do
875
+ # ...
876
+ enable :jwt, :jwt_cors, :jwt_refresh
877
+ # ...
878
+ end
879
+ end
880
+ ```
655
881
 
656
882
  ## OmniAuth
657
883
 
@@ -745,7 +971,7 @@ class RodauthController < ApplicationController
745
971
 
746
972
  # create new account if it doesn't exist
747
973
  unless account
748
- account = Account.create!(email: auth["info"]["email"])
974
+ account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
749
975
  end
750
976
 
751
977
  # create new identity if it doesn't exist
@@ -797,17 +1023,19 @@ end
797
1023
 
798
1024
  When developing custom extensions for Rodauth inside your Rails project, it's
799
1025
  better to use plain modules (at least in the beginning), because Rodauth
800
- feature API doesn't yet support Zeitwerk reloading well.
1026
+ feature design doesn't yet support Zeitwerk reloading well. Here is
1027
+ an example of an LDAP authentication extension that uses the
1028
+ [simple_ldap_authenticator] gem.
801
1029
 
802
1030
  ```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)
1031
+ # app/lib/rodauth_ldap.rb
1032
+ module RodauthLdap
1033
+ def require_bcrypt?
1034
+ false
807
1035
  end
808
1036
 
809
- def password_hash_match?(hash, password)
810
- Argon2::Password.verify_password(password, hash)
1037
+ def password_match?(password)
1038
+ SimpleLdapAuthenticator.valid?(account[:email], password)
811
1039
  end
812
1040
  end
813
1041
  ```
@@ -817,7 +1045,7 @@ class RodauthApp < Rodauth::Rails::App
817
1045
  configure do
818
1046
  # ...
819
1047
  auth_class_eval do
820
- include RodauthArgon2
1048
+ include RodauthLdap
821
1049
  end
822
1050
  # ...
823
1051
  end
@@ -826,48 +1054,156 @@ end
826
1054
 
827
1055
  ## Testing
828
1056
 
829
- If you're writing system tests, it's generally better to go through the actual
830
- authentication flow with tools like Capybara, and to not use any stubbing.
831
-
832
- In functional and integration tests you can just make requests to Rodauth
833
- routes:
1057
+ System (browser) tests for Rodauth actions could look something like this:
834
1058
 
835
1059
  ```rb
836
- # test/controllers/posts_controller_test.rb
837
- class PostsControllerTest < ActionDispatch::IntegrationTest
838
- test "should require authentication" do
839
- get posts_url
840
- assert_redirected_to "/login"
1060
+ # test/system/authentication_test.rb
1061
+ require "test_helper"
1062
+
1063
+ class AuthenticationTest < ActionDispatch::SystemTestCase
1064
+ include ActiveJob::TestHelper
1065
+ driven_by :rack_test
1066
+
1067
+ test "creating and verifying an account" do
1068
+ create_account
1069
+ assert_match "An email has been sent to you with a link to verify your account", page.text
1070
+
1071
+ verify_account
1072
+ assert_match "Your account has been verified", page.text
1073
+ end
1074
+
1075
+ test "logging in and logging out" do
1076
+ create_account(verify: true)
1077
+
1078
+ logout
1079
+ assert_match "You have been logged out", page.text
841
1080
 
842
1081
  login
843
- get posts_url
1082
+ assert_match "You have been logged in", page.text
1083
+ end
1084
+
1085
+ private
1086
+
1087
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1088
+ visit "/create-account"
1089
+ fill_in "Login", with: email
1090
+ fill_in "Password", with: password
1091
+ fill_in "Confirm Password", with: password
1092
+ click_on "Create Account"
1093
+ verify_account if verify
1094
+ end
1095
+
1096
+ def verify_account
1097
+ perform_enqueued_jobs # run enqueued email deliveries
1098
+ email = ActionMailer::Base.deliveries.last
1099
+ verify_account_link = email.body.to_s[/\S+verify-account\S+/]
1100
+ visit verify_account_link
1101
+ click_on "Verify Account"
1102
+ end
1103
+
1104
+ def login(email: "user@example.com", password: "secret")
1105
+ visit "/login"
1106
+ fill_in "Login", with: email
1107
+ fill_in "Password", with: password
1108
+ click_on "Login"
1109
+ end
1110
+
1111
+ def logout
1112
+ visit "/logout"
1113
+ click_on "Logout"
1114
+ end
1115
+ end
1116
+ ```
1117
+
1118
+ While request tests in JSON API mode with JWT tokens could look something like
1119
+ this:
1120
+
1121
+ ```rb
1122
+ # test/integration/authentication_test.rb
1123
+ require "test_helper"
1124
+
1125
+ class AuthenticationTest < ActionDispatch::IntegrationTest
1126
+ test "creating and verifying an account" do
1127
+ create_account
844
1128
  assert_response :success
1129
+ assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
1130
+
1131
+ verify_account
1132
+ assert_response :success
1133
+ assert_match "Your account has been verified", JSON.parse(body)["success"]
1134
+ end
1135
+
1136
+ test "logging in and logging out" do
1137
+ create_account(verify: true)
845
1138
 
846
1139
  logout
847
- assert_redirected_to "/login"
1140
+ assert_response :success
1141
+ assert_match "You have been logged out", JSON.parse(body)["success"]
1142
+
1143
+ login
1144
+ assert_response :success
1145
+ assert_match "You have been logged in", JSON.parse(body)["success"]
848
1146
  end
849
1147
 
850
1148
  private
851
1149
 
852
- def login(login: "user@example.com", password: "secret")
853
- post "/create-account", params: {
854
- "login" => login,
855
- "password" => password,
856
- "password-confirm" => password,
857
- }
1150
+ def create_account(email: "user@example.com", password: "secret", verify: false)
1151
+ post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
1152
+ verify_account if verify
1153
+ end
1154
+
1155
+ def verify_account
1156
+ perform_enqueued_jobs # run enqueued email deliveries
1157
+ email = ActionMailer::Base.deliveries.last
1158
+ verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
1159
+ post "/verify-account", as: :json, params: { key: verify_account_key }
1160
+ end
858
1161
 
859
- post "/login", params: {
860
- "login" => login,
861
- "password" => password,
862
- }
1162
+ def login(email: "user@example.com", password: "secret")
1163
+ post "/login", as: :json, params: { login: email, password: password }
863
1164
  end
864
1165
 
865
1166
  def logout
866
- post "/logout"
1167
+ post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
867
1168
  end
868
1169
  end
869
1170
  ```
870
1171
 
1172
+ If you're delivering emails in the background, make sure to set Active Job
1173
+ queue adapter to `:test` or `:inline`:
1174
+
1175
+ ```rb
1176
+ # config/environments/test.rb
1177
+ Rails.application.configure do |config|
1178
+ # ...
1179
+ config.active_job.queue_adapter = :test # or :inline
1180
+ # ...
1181
+ end
1182
+ ```
1183
+
1184
+ If you need to create an account record with a password directly, you can do it
1185
+ as follows:
1186
+
1187
+ ```rb
1188
+ # app/models/account.rb
1189
+ class Account < ApplicationRecord
1190
+ has_one :password_hash, foreign_key: :id
1191
+ end
1192
+ ```
1193
+ ```rb
1194
+ # app/models/account/password_hash.rb
1195
+ class Account::PasswordHash < ApplicationRecord
1196
+ belongs_to :account, foreign_key: :id
1197
+ end
1198
+ ```
1199
+ ```rb
1200
+ require "bcrypt"
1201
+
1202
+ account = Account.create!(email: "user@example.com", status: "verified")
1203
+ password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1204
+ account.create_password_hash!(id: account.id, password_hash: password_hash)
1205
+ ```
1206
+
871
1207
  ## Rodauth defaults
872
1208
 
873
1209
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -976,6 +1312,7 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
976
1312
  [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
977
1313
  [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
978
1314
  [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
1315
+ [json]: http://rodauth.jeremyevans.net/rdoc/files/doc/json_rdoc.html
979
1316
  [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
980
1317
  [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
981
1318
  [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
@@ -987,3 +1324,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
987
1324
  [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
988
1325
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
989
1326
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1327
+ [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator