rodauth-rails 0.6.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9805b35cefee7e30cc6f7190e2ace9e7ea75c20f40651eb364edafea2f2382f7
4
- data.tar.gz: 503b821866aaf2b6aa108265ed8015869a8c8a6a73e910aa3c38b35c5a542ac1
3
+ metadata.gz: 60fda35b195285a7c9cc14e07153d80faa9e939cf2944fbb04e1360baf30e306
4
+ data.tar.gz: 1f89bcfff28e6d08287fa67a9fe9228a2d61f15a8a9cdecb9fcf138137d72c47
5
5
  SHA512:
6
- metadata.gz: 5a3e69b6d62f20ee5bc5a13c89acd2974401830a4f0f8917cc7716c9a5ccaad021a20c0f3269a211336b648bd8eb65ae60094c90a039fa1d3968eaf322ec2e47
7
- data.tar.gz: 567cf154e656f7062029e207d92149fa8cf2c87404d1ba72fef6327cb31f928d0bcf453a4a0d55f71740251cb74f8b6829b8c775373daec4fe638690cd702104
6
+ metadata.gz: 8e4ed3afbe7a114ba36f19541d1c8c8ee62de07526400230b1386f027b05876cd78ad88bdb40cae9767e74f83d1532d4939d97d03657662933f81d7086df34d9
7
+ data.tar.gz: cae1fc15a86f1b2e2423a8e54f36b844f610ba23ff74fd9ced132200e9816260028682c0efea1dcf19cf8a722a7ae49882c8d31c670e268e229117b4f6fb84f2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## 0.9.0 (2021-02-07)
2
+
3
+ * Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
4
+
5
+ * Bump Rodauth dependency to 2.9+ (@janko)
6
+
7
+ * Add `--json` option for `rodauth:install` generator for configuring `json` feature (@janko)
8
+
9
+ * Add `--jwt` option for `rodauth:install` generator for configuring `jwt` feature (@janko)
10
+
11
+ * Remove the `--api` option from `rodauth:install` generator (@janko)
12
+
13
+ ## 0.8.2 (2021-01-10)
14
+
15
+ * Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
16
+
17
+ ## 0.8.1 (2021-01-04)
18
+
19
+ * Fix blank email body when `json: true` and `ActionController::API` descendant are used (@janko)
20
+
21
+ * Make view and email rendering work when there are multiple configurations and one is `json: :only` (@janko)
22
+
23
+ * Don't attempt to protect against forgery when `ActionController::API` descendant is used (@janko)
24
+
25
+ * Mark content of rodauth built-in partials as HTML-safe (@janko)
26
+
27
+ ## 0.8.0 (2021-01-03)
28
+
29
+ * Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
30
+
31
+ * Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
32
+
33
+ ## 0.7.0 (2020-11-27)
34
+
35
+ * Add `#rails_controller_eval` method for running code in context of a controller instance (@janko)
36
+
37
+ * Detect `secret_key_base` from credentials and `$SECRET_KEY_BASE` environment variable (@janko)
38
+
1
39
  ## 0.6.1 (2020-11-25)
2
40
 
3
41
  * Generate the Rodauth controller for API-only Rails apps as well (@janko)
data/README.md CHANGED
@@ -2,6 +2,35 @@
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
+
5
34
  ## Resources
6
35
 
7
36
  Useful links:
@@ -12,14 +41,49 @@ Useful links:
12
41
  Articles:
13
42
 
14
43
  * [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
15
- * [Adding Authentication in Rails 6 with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
44
+ * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
45
+ * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
46
+
47
+ ## Why Rodauth?
48
+
49
+ There are already several popular authentication solutions for Rails (Devise,
50
+ Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Here are some
51
+ of the advantages that stand out for me:
52
+
53
+ * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
54
+ * standardized [JSON API support][json] for every feature (including [JWT][jwt])
55
+ * 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])
56
+ * [email authentication][email_auth] (aka "passwordless")
57
+ * [audit logging][audit_logging] (for any action)
58
+ * ability to protect password hashes even in case of SQL injection ([more details][password protection])
59
+ * additional bruteforce protection for tokens ([more details][bruteforce tokens])
60
+ * uniform configuration DSL (any setting can be static or dynamic)
61
+ * consistent before/after hooks around everything
62
+ * dedicated object encapsulating all authentication logic
63
+
64
+ ## Upgrading
65
+
66
+ ### Upgrading to 0.7.0
67
+
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.
73
+
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.
16
80
 
17
81
  ## Installation
18
82
 
19
83
  Add the gem to your Gemfile:
20
84
 
21
85
  ```rb
22
- gem "rodauth-rails", "~> 0.6"
86
+ gem "rodauth-rails", "~> 0.9"
23
87
 
24
88
  # gem "jwt", require: false # for JWT feature
25
89
  # gem "rotp", require: false # for OTP feature
@@ -31,10 +95,19 @@ Then run `bundle install`.
31
95
 
32
96
  Next, run the install generator:
33
97
 
34
- ```
98
+ ```sh
35
99
  $ rails generate rodauth:install
36
100
  ```
37
101
 
102
+ Or if you want Rodauth endpoints to be exposed via JSON API:
103
+
104
+ ```sh
105
+ $ rails generate rodauth:install --json # regular authentication using the Rails session
106
+ # or
107
+ $ rails generate rodauth:install --jwt # token authentication via the "Authorization" header
108
+ $ bundle add jwt
109
+ ```
110
+
38
111
  The generator will create the following files:
39
112
 
40
113
  * Rodauth migration at `db/migrate/*_create_rodauth.rb`
@@ -168,14 +241,12 @@ Using this information, we could add some basic authentication links to our
168
241
  navigation header:
169
242
 
170
243
  ```erb
171
- <ul>
172
- <% if rodauth.authenticated? %>
173
- <li><%= link_to "Sign out", rodauth.logout_path, method: :post %></li>
174
- <% else %>
175
- <li><%= link_to "Sign in", rodauth.login_path %></li>
176
- <li><%= link_to "Sign up", rodauth.create_account_path %></li>
177
- <% end %>
178
- </ul>
244
+ <% if rodauth.logged_in? %>
245
+ <%= link_to "Sign out", rodauth.logout_path, method: :post %>
246
+ <% else %>
247
+ <%= link_to "Sign in", rodauth.login_path %>
248
+ <%= link_to "Sign up", rodauth.create_account_path %>
249
+ <% end %>
179
250
  ```
180
251
 
181
252
  These routes are fully functional, feel free to visit them and interact with the
@@ -191,7 +262,7 @@ retrieves the corresponding account record:
191
262
  ```rb
192
263
  # app/controllers/application_controller.rb
193
264
  class ApplicationController < ActionController::Base
194
- before_action :current_account, if: -> { rodauth.authenticated? }
265
+ before_action :current_account, if: -> { rodauth.logged_in? }
195
266
 
196
267
  private
197
268
 
@@ -365,7 +436,7 @@ $ rails generate rodauth:mailer
365
436
  ```
366
437
 
367
438
  This will create a `RodauthMailer` with the associated mailer views in
368
- `app/views/rodauth_mailer` directory.
439
+ `app/views/rodauth_mailer` directory:
369
440
 
370
441
  ```rb
371
442
  # app/mailers/rodauth_mailer.rb
@@ -417,9 +488,9 @@ end
417
488
  ```
418
489
 
419
490
  This approach can be used even if you're using a 3rd-party service for
420
- transactional emails, where emails are sent via API requests instead of
421
- SMTP. Whatever the `create_*_email` block returns will be passed to
422
- `send_email`, so you can be creative.
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.
423
494
 
424
495
  ### Migrations
425
496
 
@@ -441,36 +512,67 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
441
512
  end
442
513
  ```
443
514
 
444
- ### JSON API
445
-
446
- JSON API support in Rodauth is provided by the [JWT feature]. First you'll need
447
- to add the [JWT gem] to your Gemfile:
448
-
449
- ```rb
450
- gem "jwt"
451
- ```
515
+ ### Multiple configurations
452
516
 
453
- The following configuration will enable the Rodauth endpoints to be accessed
454
- via JSON requests (in addition to HTML requests):
517
+ If you need to handle multiple types of accounts that require different
518
+ authentication logic, you can create different configurations for them:
455
519
 
456
520
  ```rb
457
521
  # app/lib/rodauth_app.rb
458
522
  class RodauthApp < Rodauth::Rails::App
459
- configure(json: true) do
523
+ # primary configuration
524
+ configure do
460
525
  # ...
461
- enable :jwt
462
- jwt_secret "...your secret key..."
526
+ end
527
+
528
+ # alternative configuration
529
+ configure(:admin) do
530
+ # ... enable features ...
531
+ prefix "/admin"
532
+ session_key_prefix "admin_"
533
+ remember_cookie_key "_admin_remember" # if using remember feature
534
+ # ...
535
+ end
536
+
537
+ route do |r|
538
+ r.rodauth
539
+ r.on("admin") { r.rodauth(:admin) }
463
540
  # ...
464
541
  end
465
542
  end
466
543
  ```
467
544
 
468
- If you want the endpoints to be only accessible via JSON requests, or if your
469
- Rails app is in API-only mode, instead of `json: true` pass `json: :only` to
470
- the configure method.
545
+ Then in your application you can reference the secondary Rodauth instance:
471
546
 
472
- Make sure to store the `jwt_secret` in a secure place, such as Rails
473
- credentials or environment variables.
547
+ ```rb
548
+ rodauth(:admin).login_path #=> "/admin/login"
549
+ ```
550
+
551
+ ### Calling controller methods
552
+
553
+ When using Rodauth before/after hooks or generally overriding your Rodauth
554
+ configuration, in some cases you might want to call methods defined on your
555
+ controllers. You can do so with `rails_controller_eval`, for example:
556
+
557
+ ```rb
558
+ # app/controllers/application_controller.rb
559
+ class ApplicationController < ActionController::Base
560
+ private
561
+ def setup_tracking(account_id)
562
+ # ... some implementation ...
563
+ end
564
+ end
565
+ ```
566
+ ```rb
567
+ # app/lib/rodauth_app.rb
568
+ class RodauthApp < Rodauth::Rails::App
569
+ configure do
570
+ after_create_account do
571
+ rails_controller_eval { setup_tracking(account_id) }
572
+ end
573
+ end
574
+ end
575
+ ```
474
576
 
475
577
  ### Rodauth instance
476
578
 
@@ -479,7 +581,7 @@ Rodauth operations outside of the request context. rodauth-rails gives you the
479
581
  ability to retrieve the Rodauth instance:
480
582
 
481
583
  ```rb
482
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
584
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
483
585
 
484
586
  rodauth.login_url #=> "https://example.com/login"
485
587
  rodauth.account_from_login("user@example.com") # loads user by email
@@ -510,8 +612,8 @@ The Rodauth app stores the `Rodauth::Auth` instance in the Rack env hash, which
510
612
  is then available in your Rails app:
511
613
 
512
614
  ```rb
513
- request.env["rodauth"] #=> #<Rodauth::Auth>
514
- request.env["rodauth.secondary"] #=> #<Rodauth::Auth> (if using multiple configurations)
615
+ request.env["rodauth"] #=> #<Rodauth::Auth>
616
+ request.env["rodauth.admin"] #=> #<Rodauth::Auth> (if using multiple configurations)
515
617
  ```
516
618
 
517
619
  For convenience, this object can be accessed via the `#rodauth` method in views
@@ -520,14 +622,14 @@ and controllers:
520
622
  ```rb
521
623
  class MyController < ApplicationController
522
624
  def my_action
523
- rodauth #=> #<Rodauth::Auth>
524
- rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations)
625
+ rodauth #=> #<Rodauth::Auth>
626
+ rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations)
525
627
  end
526
628
  end
527
629
  ```
528
630
  ```erb
529
- <% rodauth #=> #<Rodauth::Auth> %>
530
- <% rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations) %>
631
+ <% rodauth #=> #<Rodauth::Auth> %>
632
+ <% rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations) %>
531
633
  ```
532
634
 
533
635
  ### App
@@ -542,13 +644,38 @@ integration for Rodauth:
542
644
  * runs Action Controller callbacks & rescue handlers around Rodauth actions
543
645
  * uses Action Mailer for sending emails
544
646
 
545
- The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
647
+ The `configure` method wraps configuring the Rodauth plugin, forwarding
546
648
  any additional [plugin options].
547
649
 
548
650
  ```rb
549
- configure { ... } # defining default Rodauth configuration
550
- configure(json: true) { ... } # passing options to the Rodauth plugin
551
- configure(:secondary) { ... } # defining multiple Rodauth configurations
651
+ class RodauthApp < Rodauth::Rails::App
652
+ configure { ... } # defining default Rodauth configuration
653
+ configure(json: true) { ... } # passing options to the Rodauth plugin
654
+ configure(:admin) { ... } # defining multiple Rodauth configurations
655
+ end
656
+ ```
657
+
658
+ The `route` block is provided by Roda, and it's called on each request before
659
+ it reaches the Rails router.
660
+
661
+ ```rb
662
+ class RodauthApp < Rodauth::Rails::App
663
+ route do |r|
664
+ # ... called before each request ...
665
+ end
666
+ end
667
+ ```
668
+
669
+ Since `Rodauth::Rails::App` is just a Roda subclass, you can do anything you
670
+ would with a Roda app, such as loading additional Roda plugins:
671
+
672
+ ```rb
673
+ class RodauthApp < Rodauth::Rails::App
674
+ plugin :request_headers # easier access to request headers
675
+ plugin :typecast_params # methods for conversion of request params
676
+ plugin :default_headers, { "Foo" => "Bar" }
677
+ # ...
678
+ end
552
679
  ```
553
680
 
554
681
  ### Sequel
@@ -559,11 +686,156 @@ function calls).
559
686
 
560
687
  If ActiveRecord is used in the application, the `rodauth:install` generator
561
688
  will have automatically configured Sequel to reuse ActiveRecord's database
562
- connection (using the [sequel-activerecord_connection] gem).
689
+ connection, using the [sequel-activerecord_connection] gem.
563
690
 
564
691
  This means that, from the usage perspective, Sequel can be considered just
565
692
  as an implementation detail of Rodauth.
566
693
 
694
+ ## JSON API
695
+
696
+ To make Rodauth endpoints accessible via JSON API, enable the [`json`][json]
697
+ feature:
698
+
699
+ ```rb
700
+ # app/lib/rodauth_app.rb
701
+ class RodauthApp < Rodauth::Rails::App
702
+ configure do
703
+ # ...
704
+ enable :json
705
+ only_json? true # accept only JSON requests
706
+ # ...
707
+ end
708
+ end
709
+ ```
710
+
711
+ This will store account session data into the Rails session. If you rather want
712
+ stateless token-based authentication via the `Authorization` header, enable the
713
+ [`jwt`][jwt] feature (which builds on top of the `json` feature) and add the
714
+ [JWT gem] to the Gemfile:
715
+
716
+ ```sh
717
+ $ bundle add jwt
718
+ ```
719
+ ```rb
720
+ # app/lib/rodauth_app.rb
721
+ class RodauthApp < Rodauth::Rails::App
722
+ configure do
723
+ # ...
724
+ enable :jwt
725
+ jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
726
+ only_json? true # accept only JSON requests
727
+ # ...
728
+ end
729
+ end
730
+ ```
731
+
732
+ ## OmniAuth
733
+
734
+ While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
735
+ ourselves using the existing Rodauth API.
736
+
737
+ In order to allow the user to login via multiple external providers, let's
738
+ create an `account_identities` table that will have a many-to-one relationship
739
+ with the `accounts` table:
740
+
741
+ ```sh
742
+ $ rails generate model AccountIdentity
743
+ ```
744
+ ```rb
745
+ # db/migrate/*_create_account_identities.rb
746
+ class CreateAccountIdentities < ActiveRecord::Migration
747
+ def change
748
+ create_table :account_identities do |t|
749
+ t.references :account, null: false, foreign_key: { on_delete: :cascade }
750
+ t.string :provider, null: false
751
+ t.string :uid, null: false
752
+ t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
753
+
754
+ t.timestamps
755
+
756
+ t.index [:provider, :uid], unique: true
757
+ end
758
+ end
759
+ end
760
+ ```
761
+ ```rb
762
+ # app/models/account_identity.rb
763
+ class AcccountIdentity < ApplicationRecord
764
+ belongs_to :account
765
+ end
766
+ ```
767
+ ```rb
768
+ # app/models/account.rb
769
+ class Account < ApplicationRecord
770
+ has_many :identities, class_name: "AccountIdentity"
771
+ end
772
+ ```
773
+
774
+ Let's assume we want to implement Facebook login, and have added the
775
+ corresponding OmniAuth strategy to the middleware stack, together with an
776
+ authorization link on the login form:
777
+
778
+ ```rb
779
+ Rails.application.config.middleware.use OmniAuth::Builder do
780
+ provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
781
+ scope: "email", callback_path: "/auth/facebook/callback"
782
+ end
783
+ ```
784
+ ```erb
785
+ <%= link_to "Login via Facebook", "/auth/facebook" %>
786
+ ```
787
+
788
+ Let's implement the OmniAuth callback endpoint on our Rodauth controller:
789
+
790
+ ```rb
791
+ # config/routes.rb
792
+ Rails.application.routes.draw do
793
+ # ...
794
+ get "/auth/:provider/callback", to: "rodauth#omniauth"
795
+ end
796
+ ```
797
+ ```rb
798
+ # app/controllres/rodauth_controller.rb
799
+ class RodauthController < ApplicationController
800
+ def omniauth
801
+ auth = request.env["omniauth.auth"]
802
+
803
+ # attempt to find existing identity directly
804
+ identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
805
+
806
+ if identity
807
+ # update any external info changes
808
+ identity.update!(info: auth["info"])
809
+ # set account from identity
810
+ account = identity.account
811
+ end
812
+
813
+ # attempt to find an existing account by email
814
+ account ||= Account.find_by(email: auth["info"]["email"])
815
+
816
+ # disallow login if account is not verified
817
+ if account && account.status != rodauth.account_open_status_value
818
+ redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
819
+ return
820
+ end
821
+
822
+ # create new account if it doesn't exist
823
+ unless account
824
+ account = Account.create!(email: auth["info"]["email"])
825
+ end
826
+
827
+ # create new identity if it doesn't exist
828
+ unless identity
829
+ account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
830
+ end
831
+
832
+ # login with Rodauth
833
+ rodauth.account_from_login(account.email)
834
+ rodauth.login("omniauth")
835
+ end
836
+ end
837
+ ```
838
+
567
839
  ## Configuring
568
840
 
569
841
  For the list of configuration methods provided by Rodauth, see the [feature
@@ -597,6 +869,37 @@ Rodauth::Rails.configure do |config|
597
869
  end
598
870
  ```
599
871
 
872
+ ## Custom extensions
873
+
874
+ 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.
877
+
878
+ ```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)
883
+ end
884
+
885
+ def password_hash_match?(hash, password)
886
+ Argon2::Password.verify_password(password, hash)
887
+ end
888
+ end
889
+ ```
890
+ ```rb
891
+ # app/lib/rodauth_app.rb
892
+ class RodauthApp < Rodauth::Rails::App
893
+ configure do
894
+ # ...
895
+ auth_class_eval do
896
+ include RodauthArgon2
897
+ end
898
+ # ...
899
+ end
900
+ end
901
+ ```
902
+
600
903
  ## Testing
601
904
 
602
905
  If you're writing system tests, it's generally better to go through the actual
@@ -669,6 +972,8 @@ Rodauth method for creating database functions:
669
972
 
670
973
  ```rb
671
974
  # db/migrate/*_create_rodauth_database_functions.rb
975
+ require "rodauth/migrations"
976
+
672
977
  class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
673
978
  def up
674
979
  Rodauth.create_database_authentication_functions(DB)
@@ -733,7 +1038,6 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
733
1038
  [Rodauth]: https://github.com/jeremyevans/rodauth
734
1039
  [Sequel]: https://github.com/jeremyevans/sequel
735
1040
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
736
- [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
737
1041
  [JWT gem]: https://github.com/jwt/ruby-jwt
738
1042
  [Bootstrap]: https://getbootstrap.com/
739
1043
  [Roda]: http://roda.jeremyevans.net/
@@ -742,3 +1046,21 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
742
1046
  [Rodauth migration]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Creating+tables
743
1047
  [sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
744
1048
  [plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
1049
+ [hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
1050
+ [OmniAuth]: https://github.com/omniauth/omniauth
1051
+ [otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
1052
+ [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
1053
+ [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
1054
+ [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
1055
+ [json]: http://rodauth.jeremyevans.net/rdoc/files/doc/json_rdoc.html
1056
+ [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
1057
+ [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
1058
+ [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
1059
+ [password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
1060
+ [bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
1061
+ [password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
1062
+ [disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
1063
+ [password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
1064
+ [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
1065
+ [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1066
+ [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
@@ -13,6 +13,9 @@ module Rodauth
13
13
  source_root "#{__dir__}/templates"
14
14
  namespace "rodauth:install"
15
15
 
16
+ class_option :json, type: :boolean, desc: "Configure JSON support"
17
+ class_option :jwt, type: :boolean, desc: "Configure JWT support"
18
+
16
19
  def create_rodauth_migration
17
20
  return unless defined?(ActiveRecord::Base)
18
21
 
@@ -74,15 +77,17 @@ module Rodauth
74
77
  end
75
78
  end
76
79
 
77
- def api_only?
78
- return unless ::Rails.gem_version >= Gem::Version.new("5.0")
80
+ def json?
81
+ options[:json]
82
+ end
79
83
 
80
- ::Rails.application.config.api_only
84
+ def jwt?
85
+ options[:jwt] || Rodauth::Rails.api_only?
81
86
  end
82
87
 
83
88
  def migration_features
84
89
  features = [:base, :reset_password, :verify_account, :verify_login_change]
85
- features << :remember unless api_only?
90
+ features << :remember unless jwt?
86
91
  features
87
92
  end
88
93
  end
@@ -1,11 +1,11 @@
1
1
  class RodauthApp < Rodauth::Rails::App
2
- configure<%= " json: :only" if api_only? %> do
2
+ configure do
3
3
  # List of authentication features that are loaded.
4
4
  enable :create_account, :verify_account, :verify_account_grace_period,
5
- :login, :logout, <%= api_only? ? ":jwt" : ":remember" %>,
5
+ :login, :logout<%= ", :remember" unless jwt? %>,
6
6
  :reset_password, :change_password, :change_password_notify,
7
7
  :change_login, :verify_login_change,
8
- :close_account
8
+ :close_account<%= ", :json" if json? %><%= ", :jwt" if jwt? %>
9
9
 
10
10
  # See the Rodauth documentation for the list of available config options:
11
11
  # http://rodauth.jeremyevans.net/documentation.html
@@ -14,6 +14,16 @@ class RodauthApp < Rodauth::Rails::App
14
14
  # The secret key used for hashing public-facing tokens for various features.
15
15
  # Defaults to Rails `secret_key_base`, but you can use your own secret key.
16
16
  # hmac_secret "<%= SecureRandom.hex(64) %>"
17
+ <% if jwt? -%>
18
+
19
+ # Set JWT secret, which is used to cryptographically protect the token.
20
+ jwt_secret "<%= SecureRandom.hex(64) %>"
21
+ <% end -%>
22
+ <% if json? || jwt? -%>
23
+
24
+ # Accept only JSON requests.
25
+ only_json? true
26
+ <% end -%>
17
27
 
18
28
  # Specify the controller used for view rendering and CSRF verification.
19
29
  rails_controller { RodauthController }
@@ -42,18 +52,6 @@ class RodauthApp < Rodauth::Rails::App
42
52
 
43
53
  # Redirect to the app from login and registration pages if already logged in.
44
54
  # already_logged_in { redirect login_redirect }
45
- <% if api_only? -%>
46
-
47
- # ==> JWT
48
- # Set JWT secret, which is used to cryptographically protect the token.
49
- jwt_secret "<%= SecureRandom.hex(64) %>"
50
-
51
- # Don't require login confirmation param.
52
- require_login_confirmation? false
53
-
54
- # Don't require password confirmation param.
55
- require_password_confirmation? false
56
- <% end -%>
57
55
 
58
56
  # ==> Emails
59
57
  # Uncomment the lines below once you've imported mailer views.
@@ -80,14 +78,14 @@ class RodauthApp < Rodauth::Rails::App
80
78
  # db.after_commit { email.deliver_later }
81
79
  # end
82
80
 
83
- # In the meantime you can tweak settings for emails created by Rodauth
81
+ # In the meantime, you can tweak settings for emails created by Rodauth.
84
82
  # email_subject_prefix "[MyApp] "
85
83
  # email_from "noreply@myapp.com"
86
84
  # send_email(&:deliver_later)
87
85
  # reset_password_email_body { "Click here to reset your password: #{reset_password_email_link}" }
88
86
 
89
87
  # ==> Flash
90
- <% unless api_only? -%>
88
+ <% unless json? || jwt? -%>
91
89
  # Match flash keys with ones already used in the Rails app.
92
90
  # flash_notice_key :success # default is :notice
93
91
  # flash_error_key :error # default is :alert
@@ -107,7 +105,7 @@ class RodauthApp < Rodauth::Rails::App
107
105
 
108
106
  # Change minimum number of password characters required when creating an account.
109
107
  # password_minimum_length 8
110
- <% unless api_only? -%>
108
+ <% unless jwt? -%>
111
109
 
112
110
  # ==> Remember Feature
113
111
  # Remember all logged in users.
@@ -128,13 +126,14 @@ class RodauthApp < Rodauth::Rails::App
128
126
 
129
127
  # Perform additional actions after the account is created.
130
128
  # after_create_account do
131
- # Profile.create!(account_id: account[:id], name: param("name"))
129
+ # Profile.create!(account_id: account_id, name: param("name"))
132
130
  # end
133
131
 
134
132
  # Do additional cleanup after the account is closed.
135
133
  # after_close_account do
136
- # Profile.find_by!(account_id: account[:id]).destroy
134
+ # Profile.find_by!(account_id: account_id).destroy
137
135
  # end
136
+ <% unless json? || jwt? -%>
138
137
 
139
138
  # ==> Redirects
140
139
  # Redirect to home page after logout.
@@ -145,6 +144,7 @@ class RodauthApp < Rodauth::Rails::App
145
144
 
146
145
  # Redirect to login page after password reset.
147
146
  reset_password_redirect { login_path }
147
+ <% end -%>
148
148
 
149
149
  # ==> Deadlines
150
150
  # Change default deadlines for some actions.
@@ -156,14 +156,13 @@ class RodauthApp < Rodauth::Rails::App
156
156
 
157
157
  # ==> Multiple configurations
158
158
  # configure(:admin) do
159
- # enable :http_basic_auth
160
- #
159
+ # enable :http_basic_auth # enable different set of features
161
160
  # prefix "/admin"
162
- # session_key :admin_id
161
+ # session_key_prefix "admin_"
163
162
  # end
164
163
 
165
164
  route do |r|
166
- <% unless api_only? -%>
165
+ <% unless jwt? -%>
167
166
  rodauth.load_memory # autologin remembered users
168
167
 
169
168
  <% end -%>
data/lib/rodauth/rails.rb CHANGED
@@ -32,6 +32,26 @@ module Rodauth
32
32
  scope.rodauth(name)
33
33
  end
34
34
 
35
+ if ::Rails.gem_version >= Gem::Version.new("5.2")
36
+ def secret_key_base
37
+ ::Rails.application.secret_key_base
38
+ end
39
+ else
40
+ def secret_key_base
41
+ ::Rails.application.secrets.secret_key_base
42
+ end
43
+ end
44
+
45
+ if ::Rails.gem_version >= Gem::Version.new("5.0")
46
+ def api_only?
47
+ ::Rails.application.config.api_only
48
+ end
49
+ else
50
+ def api_only?
51
+ false
52
+ end
53
+ end
54
+
35
55
  def configure
36
56
  yield self
37
57
  end
@@ -1,4 +1,6 @@
1
1
  require "roda"
2
+ require "rodauth"
3
+ require "rodauth/rails/feature"
2
4
 
3
5
  module Rodauth
4
6
  module Rails
@@ -10,13 +12,13 @@ module Rodauth
10
12
  plugin :hooks
11
13
  plugin :render, layout: false
12
14
 
13
- def self.configure(name = nil, **options, &block)
14
- unless options[:json] == :only
15
- require "rodauth/rails/app/flash"
16
- plugin Flash
17
- end
15
+ if defined?(ActionDispatch::Flash) # not in API-only mode
16
+ require "rodauth/rails/app/flash"
17
+ plugin Flash
18
+ end
18
19
 
19
- plugin :rodauth, name: name, csrf: false, flash: false, **options do
20
+ def self.configure(name = nil, **options, &block)
21
+ plugin :rodauth, name: name, csrf: false, flash: false, json: true, **options do
20
22
  # load the Rails integration
21
23
  enable :rails
22
24
 
@@ -27,7 +29,7 @@ module Rodauth
27
29
  set_deadline_values? true
28
30
 
29
31
  # use HMACs for additional security
30
- hmac_secret { ::Rails.application.secrets.secret_key_base }
32
+ hmac_secret { Rodauth::Rails.secret_key_base }
31
33
 
32
34
  # evaluate user configuration
33
35
  instance_exec(&block)
@@ -30,10 +30,12 @@ module Rodauth
30
30
  rails_request.flash
31
31
  end
32
32
 
33
- def commit_flash
34
- if ActionPack.version >= Gem::Version.new("5.0")
33
+ if ActionPack.version >= Gem::Version.new("5.0")
34
+ def commit_flash
35
35
  rails_request.commit_flash
36
- else
36
+ end
37
+ else
38
+ def commit_flash
37
39
  # ActionPack 4.2 automatically commits flash
38
40
  end
39
41
  end
@@ -26,7 +26,7 @@ module Rodauth
26
26
  def render(page)
27
27
  rails_render(partial: page.tr("-", "_"), layout: false) ||
28
28
  rails_render(action: page.tr("-", "_"), layout: false) ||
29
- super
29
+ super.html_safe
30
30
  end
31
31
 
32
32
  # Render Rails CSRF tags in Rodauth templates.
@@ -44,11 +44,25 @@ module Rodauth
44
44
  true
45
45
  end
46
46
 
47
+ # Reset Rails session to protect from session fixation attacks.
48
+ def clear_session
49
+ rails_controller_instance.reset_session
50
+ end
51
+
47
52
  # Default the flash error key to Rails' default :alert.
48
53
  def flash_error_key
49
54
  :alert
50
55
  end
51
56
 
57
+ # Evaluates the block in context of a Rodauth controller instance.
58
+ def rails_controller_eval(&block)
59
+ rails_controller_instance.instance_exec(&block)
60
+ end
61
+
62
+ def button(*)
63
+ super.html_safe
64
+ end
65
+
52
66
  private
53
67
 
54
68
  # Runs controller callbacks and rescue handlers around Rodauth actions.
@@ -63,20 +77,22 @@ module Rodauth
63
77
 
64
78
  if rails_controller_instance.performed?
65
79
  rails_controller_response
66
- else
80
+ elsif result
67
81
  result[1].merge!(rails_controller_instance.response.headers)
68
82
  throw :halt, result
83
+ else
84
+ result
69
85
  end
70
86
  end
71
87
 
72
88
  # Runs any #(before|around|after)_action controller callbacks.
73
89
  def rails_controller_callbacks
74
90
  # don't verify CSRF token as part of callbacks, Rodauth will do that
75
- rails_controller_instance.allow_forgery_protection = false
91
+ rails_controller_forgery_protection { false }
76
92
 
77
93
  rails_controller_instance.run_callbacks(:process_action) do
78
94
  # turn the setting back to default so that form tags generate CSRF tags
79
- rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
95
+ rails_controller_forgery_protection { rails_controller.allow_forgery_protection }
80
96
 
81
97
  yield
82
98
  end
@@ -116,7 +132,7 @@ module Rodauth
116
132
 
117
133
  # Calls the Rails renderer, returning nil if a template is missing.
118
134
  def rails_render(*args)
119
- return if only_json?
135
+ return if rails_api_controller?
120
136
 
121
137
  rails_controller_instance.render_to_string(*args)
122
138
  rescue ActionView::MissingTemplate
@@ -143,6 +159,13 @@ module Rodauth
143
159
  rails_controller_instance.send(:form_authenticity_token)
144
160
  end
145
161
 
162
+ # allows/disables forgery protection
163
+ def rails_controller_forgery_protection(&value)
164
+ return if rails_api_controller?
165
+
166
+ rails_controller_instance.allow_forgery_protection = value.call
167
+ end
168
+
146
169
  # Instances of the configured controller with current request's env hash.
147
170
  def _rails_controller_instance
148
171
  controller = rails_controller.new
@@ -154,27 +177,29 @@ module Rodauth
154
177
  end
155
178
 
156
179
  if ActionPack.version >= Gem::Version.new("5.0")
157
- # Controller class to use for view rendering, CSRF protection, and
158
- # running any registered action callbacks and rescue_from handlers.
159
- def rails_controller
160
- only_json? ? ActionController::API : ActionController::Base
161
- end
162
-
163
180
  def prepare_rails_controller(controller, rails_request)
164
181
  controller.set_request! rails_request
165
182
  controller.set_response! rails_controller.make_response!(rails_request)
166
183
  end
167
184
  else
168
- def rails_controller
169
- ActionController::Base
170
- end
171
-
172
185
  def prepare_rails_controller(controller, rails_request)
173
186
  controller.send(:set_response!, rails_request)
174
187
  controller.instance_variable_set(:@_request, rails_request)
175
188
  end
176
189
  end
177
190
 
191
+ def rails_api_controller?
192
+ defined?(ActionController::API) && rails_controller <= ActionController::API
193
+ end
194
+
195
+ def rails_controller
196
+ if only_json? && Rodauth::Rails.api_only?
197
+ ActionController::API
198
+ else
199
+ ActionController::Base
200
+ end
201
+ end
202
+
178
203
  # ActionMailer subclass for correct email delivering.
179
204
  class Mailer < ActionMailer::Base
180
205
  def create_email(**options)
@@ -22,7 +22,7 @@ namespace :rodauth do
22
22
  "#{path.ljust(padding)} #{code}"
23
23
  end
24
24
 
25
- puts "\n #{route_lines.join("\n ")}"
25
+ puts "\n #{route_lines.join("\n ")}" unless route_lines.empty?
26
26
  end
27
27
  end
28
28
  end
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.6.1"
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
@@ -17,8 +17,10 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency "railties", ">= 4.2", "< 7"
20
- spec.add_dependency "rodauth", "~> 2.6"
20
+ spec.add_dependency "rodauth", "~> 2.9"
21
21
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
22
  spec.add_dependency "tilt"
23
23
  spec.add_dependency "bcrypt"
24
+
25
+ spec.add_development_dependency "jwt"
24
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-25 00:00:00.000000000 Z
11
+ date: 2021-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '2.6'
39
+ version: '2.9'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.6'
46
+ version: '2.9'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: jwt
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
89
103
  description: Provides Rails integration for Rodauth.
90
104
  email:
91
105
  - janko.marohnic@gmail.com
@@ -187,7 +201,6 @@ files:
187
201
  - lib/generators/rodauth/templates/db/migrate/create_rodauth.rb
188
202
  - lib/generators/rodauth/views_generator.rb
189
203
  - lib/rodauth-rails.rb
190
- - lib/rodauth/features/rails.rb
191
204
  - lib/rodauth/rails.rb
192
205
  - lib/rodauth/rails/app.rb
193
206
  - lib/rodauth/rails/app/flash.rb
@@ -203,7 +216,7 @@ homepage: https://github.com/janko/rodauth-rails
203
216
  licenses:
204
217
  - MIT
205
218
  metadata: {}
206
- post_install_message:
219
+ post_install_message:
207
220
  rdoc_options: []
208
221
  require_paths:
209
222
  - lib
@@ -218,8 +231,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
231
  - !ruby/object:Gem::Version
219
232
  version: '0'
220
233
  requirements: []
221
- rubygems_version: 3.1.4
222
- signing_key:
234
+ rubygems_version: 3.2.3
235
+ signing_key:
223
236
  specification_version: 4
224
237
  summary: Provides Rails integration for Rodauth.
225
238
  test_files: []
@@ -1 +0,0 @@
1
- require "rodauth/rails/feature"