rodauth-rails 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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"