rodauth-rails 0.7.0 → 0.9.1

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: 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"