rodauth-rails 0.7.0 → 0.9.1

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: 8163d64892cbebd867182d15148f3099abb3ed49ae3e07a89a5adea6606623d2
4
- data.tar.gz: 3cc7990e0af8e5ffb2ac959f989fb45cf538490412adfc908571823e5dd7b160
3
+ metadata.gz: b8f8aec1dbdc745a530aabec0d63bc2681499dd36f8185faed9ea09e7184636e
4
+ data.tar.gz: fbc5a75976a922978a6e37fee3bef8e7f04bb0a9a324066afdf79172b33f00e9
5
5
  SHA512:
6
- metadata.gz: 99005d6864310fa3a36f8314a13588900a5ac1559af7a77d75cb5aba66b0b829d32c83fe66a3f5a7ced098de32b39396edd666919177836bb84b35a0de3a558b
7
- data.tar.gz: 2d66b5ab43d05b26483cb3d69181c506b19a937fa77a2d7d66a38708f6357fae7bd2e605cc0a96affdd8fed822076dccb1603577338e68620c70a816fc45db7a
6
+ metadata.gz: 89d2f6ad377ba8e3f18bc747c3bfdf53e97c1a29f2731036987e5f7c1fde14db89732cda2d09026a153d81eabe26e51e021a129f02517d4d5582fcaf392876ca
7
+ data.tar.gz: 648b1297a9569b436113b5921a9ae37944d808ed42a03ef57a75452a74143dcc493e7d9c34a12f31f780745db5d2b1365d5a7b602dfa303571961730566852f4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ## 0.9.1 (2021-02-10)
2
+
3
+ * Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
4
+
5
+ * Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
6
+
7
+ ## 0.9.0 (2021-02-07)
8
+
9
+ * Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
10
+
11
+ * Bump Rodauth dependency to 2.9+ (@janko)
12
+
13
+ * Add `--json` option for `rodauth:install` generator for configuring `json` feature (@janko)
14
+
15
+ * Add `--jwt` option for `rodauth:install` generator for configuring `jwt` feature (@janko)
16
+
17
+ * Remove the `--api` option from `rodauth:install` generator (@janko)
18
+
19
+ ## 0.8.2 (2021-01-10)
20
+
21
+ * Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
22
+
23
+ ## 0.8.1 (2021-01-04)
24
+
25
+ * Fix blank email body when `json: true` and `ActionController::API` descendant are used (@janko)
26
+
27
+ * Make view and email rendering work when there are multiple configurations and one is `json: :only` (@janko)
28
+
29
+ * Don't attempt to protect against forgery when `ActionController::API` descendant is used (@janko)
30
+
31
+ * Mark content of rodauth built-in partials as HTML-safe (@janko)
32
+
33
+ ## 0.8.0 (2021-01-03)
34
+
35
+ * Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
36
+
37
+ * Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
38
+
1
39
  ## 0.7.0 (2020-11-27)
2
40
 
3
41
  * Add `#rails_controller_eval` method for running code in context of a controller instance (@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,7 +41,25 @@ 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
16
63
 
17
64
  ## Upgrading
18
65
 
@@ -21,14 +68,14 @@ Articles:
21
68
  Starting from version 0.7.0, rodauth-rails now correctly detects Rails
22
69
  application's `secret_key_base` when setting default `hmac_secret`, including
23
70
  when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
24
- means authentication will be more secure by default, and Rodauth features that
25
- require `hmac_secret` should now work automatically as well.
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.
26
73
 
27
74
  However, if you've already been using rodauth-rails in production, where the
28
75
  `secret_key_base` is set via credentials or environment variable and `hmac_secret`
29
76
  was not explicitly set, the fact that your authentication will now start using
30
77
  HMACs has backwards compatibility considerations. See the [Rodauth
31
- documentation](hmac) for instructions on how to safely transition, or just set
78
+ documentation][hmac] for instructions on how to safely transition, or just set
32
79
  `hmac_secret nil` in your Rodauth configuration.
33
80
 
34
81
  ## Installation
@@ -36,7 +83,7 @@ documentation](hmac) for instructions on how to safely transition, or just set
36
83
  Add the gem to your Gemfile:
37
84
 
38
85
  ```rb
39
- gem "rodauth-rails", "~> 0.6"
86
+ gem "rodauth-rails", "~> 0.9"
40
87
 
41
88
  # gem "jwt", require: false # for JWT feature
42
89
  # gem "rotp", require: false # for OTP feature
@@ -48,10 +95,19 @@ Then run `bundle install`.
48
95
 
49
96
  Next, run the install generator:
50
97
 
51
- ```
98
+ ```sh
52
99
  $ rails generate rodauth:install
53
100
  ```
54
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
+
55
111
  The generator will create the following files:
56
112
 
57
113
  * Rodauth migration at `db/migrate/*_create_rodauth.rb`
@@ -185,14 +241,12 @@ Using this information, we could add some basic authentication links to our
185
241
  navigation header:
186
242
 
187
243
  ```erb
188
- <ul>
189
- <% if rodauth.authenticated? %>
190
- <li><%= link_to "Sign out", rodauth.logout_path, method: :post %></li>
191
- <% else %>
192
- <li><%= link_to "Sign in", rodauth.login_path %></li>
193
- <li><%= link_to "Sign up", rodauth.create_account_path %></li>
194
- <% end %>
195
- </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 %>
196
250
  ```
197
251
 
198
252
  These routes are fully functional, feel free to visit them and interact with the
@@ -208,7 +262,7 @@ retrieves the corresponding account record:
208
262
  ```rb
209
263
  # app/controllers/application_controller.rb
210
264
  class ApplicationController < ActionController::Base
211
- before_action :current_account, if: -> { rodauth.authenticated? }
265
+ before_action :current_account, if: -> { rodauth.logged_in? }
212
266
 
213
267
  private
214
268
 
@@ -382,7 +436,7 @@ $ rails generate rodauth:mailer
382
436
  ```
383
437
 
384
438
  This will create a `RodauthMailer` with the associated mailer views in
385
- `app/views/rodauth_mailer` directory.
439
+ `app/views/rodauth_mailer` directory:
386
440
 
387
441
  ```rb
388
442
  # app/mailers/rodauth_mailer.rb
@@ -434,9 +488,9 @@ end
434
488
  ```
435
489
 
436
490
  This approach can be used even if you're using a 3rd-party service for
437
- transactional emails, where emails are sent via API requests instead of
438
- SMTP. Whatever the `create_*_email` block returns will be passed to
439
- `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.
440
494
 
441
495
  ### Migrations
442
496
 
@@ -458,36 +512,41 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
458
512
  end
459
513
  ```
460
514
 
461
- ### JSON API
515
+ ### Multiple configurations
462
516
 
463
- JSON API support in Rodauth is provided by the [JWT feature]. First you'll need
464
- to add the [JWT gem] to your Gemfile:
465
-
466
- ```rb
467
- gem "jwt"
468
- ```
469
-
470
- The following configuration will enable the Rodauth endpoints to be accessed
471
- 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:
472
519
 
473
520
  ```rb
474
521
  # app/lib/rodauth_app.rb
475
522
  class RodauthApp < Rodauth::Rails::App
476
- configure(json: true) do
523
+ # primary configuration
524
+ configure do
477
525
  # ...
478
- enable :jwt
479
- 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) }
480
540
  # ...
481
541
  end
482
542
  end
483
543
  ```
484
544
 
485
- If you want the endpoints to be only accessible via JSON requests, or if your
486
- Rails app is in API-only mode, instead of `json: true` pass `json: :only` to
487
- the configure method.
545
+ Then in your application you can reference the secondary Rodauth instance:
488
546
 
489
- Make sure to store the `jwt_secret` in a secure place, such as Rails
490
- credentials or environment variables.
547
+ ```rb
548
+ rodauth(:admin).login_path #=> "/admin/login"
549
+ ```
491
550
 
492
551
  ### Calling controller methods
493
552
 
@@ -522,7 +581,7 @@ Rodauth operations outside of the request context. rodauth-rails gives you the
522
581
  ability to retrieve the Rodauth instance:
523
582
 
524
583
  ```rb
525
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:secondary)
584
+ rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
526
585
 
527
586
  rodauth.login_url #=> "https://example.com/login"
528
587
  rodauth.account_from_login("user@example.com") # loads user by email
@@ -553,8 +612,8 @@ The Rodauth app stores the `Rodauth::Auth` instance in the Rack env hash, which
553
612
  is then available in your Rails app:
554
613
 
555
614
  ```rb
556
- request.env["rodauth"] #=> #<Rodauth::Auth>
557
- 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)
558
617
  ```
559
618
 
560
619
  For convenience, this object can be accessed via the `#rodauth` method in views
@@ -563,14 +622,14 @@ and controllers:
563
622
  ```rb
564
623
  class MyController < ApplicationController
565
624
  def my_action
566
- rodauth #=> #<Rodauth::Auth>
567
- rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations)
625
+ rodauth #=> #<Rodauth::Auth>
626
+ rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations)
568
627
  end
569
628
  end
570
629
  ```
571
630
  ```erb
572
- <% rodauth #=> #<Rodauth::Auth> %>
573
- <% rodauth(:secondary) #=> #<Rodauth::Auth> (if using multiple configurations) %>
631
+ <% rodauth #=> #<Rodauth::Auth> %>
632
+ <% rodauth(:admin) #=> #<Rodauth::Auth> (if using multiple configurations) %>
574
633
  ```
575
634
 
576
635
  ### App
@@ -585,13 +644,38 @@ integration for Rodauth:
585
644
  * runs Action Controller callbacks & rescue handlers around Rodauth actions
586
645
  * uses Action Mailer for sending emails
587
646
 
588
- The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
647
+ The `configure` method wraps configuring the Rodauth plugin, forwarding
589
648
  any additional [plugin options].
590
649
 
591
650
  ```rb
592
- configure { ... } # defining default Rodauth configuration
593
- configure(json: true) { ... } # passing options to the Rodauth plugin
594
- 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
595
679
  ```
596
680
 
597
681
  ### Sequel
@@ -602,11 +686,174 @@ function calls).
602
686
 
603
687
  If ActiveRecord is used in the application, the `rodauth:install` generator
604
688
  will have automatically configured Sequel to reuse ActiveRecord's database
605
- connection (using the [sequel-activerecord_connection] gem).
689
+ connection, using the [sequel-activerecord_connection] gem.
606
690
 
607
691
  This means that, from the usage perspective, Sequel can be considered just
608
692
  as an implementation detail of Rodauth.
609
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
+ If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
733
+ corresponding Rodauth features and create the necessary tables:
734
+
735
+ ```sh
736
+ $ rails generate rodauth:migration jwt_refresh
737
+ $ rails db:migrate
738
+ ```
739
+ ```rb
740
+ # app/lib/rodauth_app.rb
741
+ class RodauthApp < Rodauth::Rails::App
742
+ configure do
743
+ # ...
744
+ enable :jwt, :jwt_cors, :jwt_refresh
745
+ # ...
746
+ end
747
+ end
748
+ ```
749
+
750
+ ## OmniAuth
751
+
752
+ While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
753
+ ourselves using the existing Rodauth API.
754
+
755
+ In order to allow the user to login via multiple external providers, let's
756
+ create an `account_identities` table that will have a many-to-one relationship
757
+ with the `accounts` table:
758
+
759
+ ```sh
760
+ $ rails generate model AccountIdentity
761
+ ```
762
+ ```rb
763
+ # db/migrate/*_create_account_identities.rb
764
+ class CreateAccountIdentities < ActiveRecord::Migration
765
+ def change
766
+ create_table :account_identities do |t|
767
+ t.references :account, null: false, foreign_key: { on_delete: :cascade }
768
+ t.string :provider, null: false
769
+ t.string :uid, null: false
770
+ t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
771
+
772
+ t.timestamps
773
+
774
+ t.index [:provider, :uid], unique: true
775
+ end
776
+ end
777
+ end
778
+ ```
779
+ ```rb
780
+ # app/models/account_identity.rb
781
+ class AcccountIdentity < ApplicationRecord
782
+ belongs_to :account
783
+ end
784
+ ```
785
+ ```rb
786
+ # app/models/account.rb
787
+ class Account < ApplicationRecord
788
+ has_many :identities, class_name: "AccountIdentity"
789
+ end
790
+ ```
791
+
792
+ Let's assume we want to implement Facebook login, and have added the
793
+ corresponding OmniAuth strategy to the middleware stack, together with an
794
+ authorization link on the login form:
795
+
796
+ ```rb
797
+ Rails.application.config.middleware.use OmniAuth::Builder do
798
+ provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
799
+ scope: "email", callback_path: "/auth/facebook/callback"
800
+ end
801
+ ```
802
+ ```erb
803
+ <%= link_to "Login via Facebook", "/auth/facebook" %>
804
+ ```
805
+
806
+ Let's implement the OmniAuth callback endpoint on our Rodauth controller:
807
+
808
+ ```rb
809
+ # config/routes.rb
810
+ Rails.application.routes.draw do
811
+ # ...
812
+ get "/auth/:provider/callback", to: "rodauth#omniauth"
813
+ end
814
+ ```
815
+ ```rb
816
+ # app/controllres/rodauth_controller.rb
817
+ class RodauthController < ApplicationController
818
+ def omniauth
819
+ auth = request.env["omniauth.auth"]
820
+
821
+ # attempt to find existing identity directly
822
+ identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
823
+
824
+ if identity
825
+ # update any external info changes
826
+ identity.update!(info: auth["info"])
827
+ # set account from identity
828
+ account = identity.account
829
+ end
830
+
831
+ # attempt to find an existing account by email
832
+ account ||= Account.find_by(email: auth["info"]["email"])
833
+
834
+ # disallow login if account is not verified
835
+ if account && account.status != rodauth.account_open_status_value
836
+ redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
837
+ return
838
+ end
839
+
840
+ # create new account if it doesn't exist
841
+ unless account
842
+ account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
843
+ end
844
+
845
+ # create new identity if it doesn't exist
846
+ unless identity
847
+ account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
848
+ end
849
+
850
+ # login with Rodauth
851
+ rodauth.account_from_login(account.email)
852
+ rodauth.login("omniauth")
853
+ end
854
+ end
855
+ ```
856
+
610
857
  ## Configuring
611
858
 
612
859
  For the list of configuration methods provided by Rodauth, see the [feature
@@ -640,6 +887,39 @@ Rodauth::Rails.configure do |config|
640
887
  end
641
888
  ```
642
889
 
890
+ ## Custom extensions
891
+
892
+ When developing custom extensions for Rodauth inside your Rails project, it's
893
+ better to use plain modules (at least in the beginning), because Rodauth
894
+ feature design doesn't yet support Zeitwerk reloading well. Here is
895
+ an example of an LDAP authentication extension that uses the
896
+ [simple_ldap_authenticator] gem.
897
+
898
+ ```rb
899
+ # app/lib/rodauth_ldap.rb
900
+ module RodauthLdap
901
+ def require_bcrypt?
902
+ false
903
+ end
904
+
905
+ def password_match?(password)
906
+ SimpleLdapAuthenticator.valid?(account[:email], password)
907
+ end
908
+ end
909
+ ```
910
+ ```rb
911
+ # app/lib/rodauth_app.rb
912
+ class RodauthApp < Rodauth::Rails::App
913
+ configure do
914
+ # ...
915
+ auth_class_eval do
916
+ include RodauthLdap
917
+ end
918
+ # ...
919
+ end
920
+ end
921
+ ```
922
+
643
923
  ## Testing
644
924
 
645
925
  If you're writing system tests, it's generally better to go through the actual
@@ -712,6 +992,8 @@ Rodauth method for creating database functions:
712
992
 
713
993
  ```rb
714
994
  # db/migrate/*_create_rodauth_database_functions.rb
995
+ require "rodauth/migrations"
996
+
715
997
  class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
716
998
  def up
717
999
  Rodauth.create_database_authentication_functions(DB)
@@ -776,7 +1058,6 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
776
1058
  [Rodauth]: https://github.com/jeremyevans/rodauth
777
1059
  [Sequel]: https://github.com/jeremyevans/sequel
778
1060
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
779
- [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
780
1061
  [JWT gem]: https://github.com/jwt/ruby-jwt
781
1062
  [Bootstrap]: https://getbootstrap.com/
782
1063
  [Roda]: http://roda.jeremyevans.net/
@@ -786,3 +1067,21 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
786
1067
  [sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
787
1068
  [plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
788
1069
  [hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
1070
+ [OmniAuth]: https://github.com/omniauth/omniauth
1071
+ [otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
1072
+ [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
1073
+ [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
1074
+ [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
1075
+ [json]: http://rodauth.jeremyevans.net/rdoc/files/doc/json_rdoc.html
1076
+ [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
1077
+ [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
1078
+ [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
1079
+ [password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
1080
+ [bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
1081
+ [password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
1082
+ [disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
1083
+ [password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
1084
+ [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
1085
+ [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1086
+ [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1087
+ [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
@@ -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
@@ -5,11 +5,11 @@ enable_extension "citext"
5
5
  create_table :accounts<%= primary_key_type %> do |t|
6
6
  <% case activerecord_adapter -%>
7
7
  <% when "postgresql" -%>
8
- t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
8
+ t.citext :email, null: false, index: { unique: true, where: "status IN ('unverified', 'verified')" }
9
9
  <% else -%>
10
10
  t.string :email, null: false, index: { unique: true }
11
11
  <% end -%>
12
- t.string :status, null: false, default: "verified"
12
+ t.string :status, null: false, default: "unverified"
13
13
  end
14
14
 
15
15
  # Used if storing password hashes in a separate table (default)
@@ -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
@@ -42,6 +42,16 @@ module Rodauth
42
42
  end
43
43
  end
44
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
+
45
55
  def configure
46
56
  yield self
47
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
+ unless Rodauth::Rails.api_only?
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
 
@@ -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,6 +44,11 @@ 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
@@ -54,6 +59,10 @@ module Rodauth
54
59
  rails_controller_instance.instance_exec(&block)
55
60
  end
56
61
 
62
+ def button(*)
63
+ super.html_safe
64
+ end
65
+
57
66
  private
58
67
 
59
68
  # Runs controller callbacks and rescue handlers around Rodauth actions.
@@ -68,20 +77,22 @@ module Rodauth
68
77
 
69
78
  if rails_controller_instance.performed?
70
79
  rails_controller_response
71
- else
80
+ elsif result
72
81
  result[1].merge!(rails_controller_instance.response.headers)
73
82
  throw :halt, result
83
+ else
84
+ result
74
85
  end
75
86
  end
76
87
 
77
88
  # Runs any #(before|around|after)_action controller callbacks.
78
89
  def rails_controller_callbacks
79
90
  # don't verify CSRF token as part of callbacks, Rodauth will do that
80
- rails_controller_instance.allow_forgery_protection = false
91
+ rails_controller_forgery_protection { false }
81
92
 
82
93
  rails_controller_instance.run_callbacks(:process_action) do
83
94
  # turn the setting back to default so that form tags generate CSRF tags
84
- rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
95
+ rails_controller_forgery_protection { rails_controller.allow_forgery_protection }
85
96
 
86
97
  yield
87
98
  end
@@ -121,7 +132,7 @@ module Rodauth
121
132
 
122
133
  # Calls the Rails renderer, returning nil if a template is missing.
123
134
  def rails_render(*args)
124
- return if only_json?
135
+ return if rails_api_controller?
125
136
 
126
137
  rails_controller_instance.render_to_string(*args)
127
138
  rescue ActionView::MissingTemplate
@@ -148,6 +159,13 @@ module Rodauth
148
159
  rails_controller_instance.send(:form_authenticity_token)
149
160
  end
150
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
+
151
169
  # Instances of the configured controller with current request's env hash.
152
170
  def _rails_controller_instance
153
171
  controller = rails_controller.new
@@ -159,27 +177,29 @@ module Rodauth
159
177
  end
160
178
 
161
179
  if ActionPack.version >= Gem::Version.new("5.0")
162
- # Controller class to use for view rendering, CSRF protection, and
163
- # running any registered action callbacks and rescue_from handlers.
164
- def rails_controller
165
- only_json? ? ActionController::API : ActionController::Base
166
- end
167
-
168
180
  def prepare_rails_controller(controller, rails_request)
169
181
  controller.set_request! rails_request
170
182
  controller.set_response! rails_controller.make_response!(rails_request)
171
183
  end
172
184
  else
173
- def rails_controller
174
- ActionController::Base
175
- end
176
-
177
185
  def prepare_rails_controller(controller, rails_request)
178
186
  controller.send(:set_response!, rails_request)
179
187
  controller.instance_variable_set(:@_request, rails_request)
180
188
  end
181
189
  end
182
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
+
183
203
  # ActionMailer subclass for correct email delivering.
184
204
  class Mailer < ActionMailer::Base
185
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.7.0"
3
+ VERSION = "0.9.1"
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.7.0
4
+ version: 0.9.1
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-27 00:00:00.000000000 Z
11
+ date: 2021-02-10 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"