rodauth-rails 0.6.0 → 0.8.2

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: 00d7ab9dd749cbae17cddc2788005d8570d8d46f89f427ad624c7e61fe177665
4
- data.tar.gz: d62aed32823b0be9c74d7281de80650b8faf600c01db22ad941a35f30bfeb002
3
+ metadata.gz: 40b75a97a14021dafe773b2585d66d6ceef7498dddcef146498721bf39e57426
4
+ data.tar.gz: 270bac846036bfd32945e71116e6816db4d51ec752ec6ff354be8f38dd206da9
5
5
  SHA512:
6
- metadata.gz: 7ab0afe5e95fab1af706b64ef5c494252fac49481d49dad1c2ef3510c17ca96050c58bf0832a36af761673137f2fd63d7d49a692656bcf06748840de96f60875
7
- data.tar.gz: 729bf3b5887647c23f4b11d821d3829c4b4d290c546d2f19ba6b393dd47bf0b357d128577e877ac3e0a4352972b79325d4217d62935d4a683e78ff8e910d13a2
6
+ metadata.gz: 46e413275b4aca41959f36fb21d5fa3fd55eb6c73f87c6eda42a6eb8e36c5886913ad01e6e80394923f38236810aad4d608064f1f51661367798e8da70a9fd11
7
+ data.tar.gz: 22779086e32ced89a113d33e6b123c64f7f6094e1e41d0b99a741d38c3bbfc3ed64a0894d5b656754717cd11b50c83cd541039c5a8d094ffa3e37e1ed7f592ff
@@ -1,3 +1,35 @@
1
+ ## 0.8.2 (2021-01-10)
2
+
3
+ * Reset Rails session on `#clear_session`, protecting from potential session fixation attacks (@janko)
4
+
5
+ ## 0.8.1 (2021-01-04)
6
+
7
+ * Fix blank email body when `json: true` and `ActionController::API` descendant are used (@janko)
8
+
9
+ * Make view and email rendering work when there are multiple configurations and one is `json: :only` (@janko)
10
+
11
+ * Don't attempt to protect against forgery when `ActionController::API` descendant is used (@janko)
12
+
13
+ * Mark content of rodauth built-in partials as HTML-safe (@janko)
14
+
15
+ ## 0.8.0 (2021-01-03)
16
+
17
+ * Add `--api` option to `rodauth:install` generator for choosing JSON-only configuration (@janko)
18
+
19
+ * Don't blow up when a Rodauth request is made using an unsupported HTTP verb (@janko)
20
+
21
+ ## 0.7.0 (2020-11-27)
22
+
23
+ * Add `#rails_controller_eval` method for running code in context of a controller instance (@janko)
24
+
25
+ * Detect `secret_key_base` from credentials and `$SECRET_KEY_BASE` environment variable (@janko)
26
+
27
+ ## 0.6.1 (2020-11-25)
28
+
29
+ * Generate the Rodauth controller for API-only Rails apps as well (@janko)
30
+
31
+ * Fix remember cookie deadline not extending in remember feature (@janko)
32
+
1
33
  ## 0.6.0 (2020-11-22)
2
34
 
3
35
  * Add `Rodauth::Rails.rodauth` method for retrieving Rodauth instance outside of request context (@janko)
data/README.md CHANGED
@@ -12,7 +12,42 @@ Useful links:
12
12
  Articles:
13
13
 
14
14
  * [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/)
15
+ * [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
16
+ * [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
17
+
18
+ ## Why Rodauth?
19
+
20
+ There are already several popular authentication solutions for Rails (Devise,
21
+ Sorcery, Clearance, Authlogic), so why would you choose Rodauth? Well, because
22
+ it has many advantages over the mentioned alternatives:
23
+
24
+ * multifactor authentication ([TOTP][otp], [SMS codes][sms_codes], [recovery codes][recovery_codes], [WebAuthn][webauthn])
25
+ * standardized [JSON API support][jwt] (for every feature)
26
+ * enterprise security features ([password complexity][password_complexity], [disallow password reuse][disallow_password_reuse], [password expiration][password_expiration], [session expiration][session_expiration], [single session][single_session], [account expiration][account_expiration])
27
+ * [email authentication][email_auth] (aka "passwordless")
28
+ * [audit logging][audit_logging] (for any action)
29
+ * ability to protect password hashes even in case of SQL injection ([more details][password protection])
30
+ * additional bruteforce protection for tokens ([more details][bruteforce tokens])
31
+ * uniform configuration DSL (any setting can be static or dynamic)
32
+ * consistent before/after hooks around everything
33
+ * dedicated object encapsulating all authentication logic
34
+
35
+ ## Upgrading
36
+
37
+ ### Upgrading to 0.7.0
38
+
39
+ Starting from version 0.7.0, rodauth-rails now correctly detects Rails
40
+ application's `secret_key_base` when setting default `hmac_secret`, including
41
+ when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
42
+ means that your authentication will now be more secure by default, and Rodauth
43
+ features that require `hmac_secret` should now work automatically as well.
44
+
45
+ However, if you've already been using rodauth-rails in production, where the
46
+ `secret_key_base` is set via credentials or environment variable and `hmac_secret`
47
+ was not explicitly set, the fact that your authentication will now start using
48
+ HMACs has backwards compatibility considerations. See the [Rodauth
49
+ documentation][hmac] for instructions on how to safely transition, or just set
50
+ `hmac_secret nil` in your Rodauth configuration.
16
51
 
17
52
  ## Installation
18
53
 
@@ -31,10 +66,17 @@ Then run `bundle install`.
31
66
 
32
67
  Next, run the install generator:
33
68
 
34
- ```
69
+ ```sh
35
70
  $ rails generate rodauth:install
36
71
  ```
37
72
 
73
+ Or if you want Rodauth endpoints to be exposed via JSON API:
74
+
75
+ ```sh
76
+ $ rails generate rodauth:install --api
77
+ $ bundle add jwt
78
+ ```
79
+
38
80
  The generator will create the following files:
39
81
 
40
82
  * Rodauth migration at `db/migrate/*_create_rodauth.rb`
@@ -168,14 +210,12 @@ Using this information, we could add some basic authentication links to our
168
210
  navigation header:
169
211
 
170
212
  ```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>
213
+ <% if rodauth.logged_in? %>
214
+ <%= link_to "Sign out", rodauth.logout_path, method: :post %>
215
+ <% else %>
216
+ <%= link_to "Sign in", rodauth.login_path %>
217
+ <%= link_to "Sign up", rodauth.create_account_path %>
218
+ <% end %>
179
219
  ```
180
220
 
181
221
  These routes are fully functional, feel free to visit them and interact with the
@@ -191,7 +231,7 @@ retrieves the corresponding account record:
191
231
  ```rb
192
232
  # app/controllers/application_controller.rb
193
233
  class ApplicationController < ActionController::Base
194
- before_action :current_account, if: -> { rodauth.authenticated? }
234
+ before_action :current_account, if: -> { rodauth.logged_in? }
195
235
 
196
236
  private
197
237
 
@@ -365,7 +405,7 @@ $ rails generate rodauth:mailer
365
405
  ```
366
406
 
367
407
  This will create a `RodauthMailer` with the associated mailer views in
368
- `app/views/rodauth_mailer` directory.
408
+ `app/views/rodauth_mailer` directory:
369
409
 
370
410
  ```rb
371
411
  # app/mailers/rodauth_mailer.rb
@@ -381,7 +421,7 @@ end
381
421
 
382
422
  You can then uncomment the lines in your Rodauth configuration to have it call
383
423
  your mailer. If you've enabled additional authentication features that send
384
- emails, make sure to override their `send_*_email` methods as well.
424
+ emails, make sure to override their `create_*_email` methods as well.
385
425
 
386
426
  ```rb
387
427
  # app/lib/rodauth_app.rb
@@ -389,37 +429,38 @@ class RodauthApp < Rodauth::Rails::App
389
429
  # ...
390
430
  configure do
391
431
  # ...
392
- send_reset_password_email do
393
- mailer_send(:reset_password, email_to, reset_password_email_link)
432
+ create_reset_password_email do
433
+ RodauthMailer.reset_password(email_to, reset_password_email_link)
394
434
  end
395
- send_verify_account_email do
396
- mailer_send(:verify_account, email_to, verify_account_email_link)
435
+ create_verify_account_email do
436
+ RodauthMailer.verify_account(email_to, verify_account_email_link)
397
437
  end
398
- send_verify_login_change_email do |login|
399
- mailer_send(:verify_login_change, login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
438
+ create_verify_login_change_email do |login|
439
+ RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
400
440
  end
401
- send_password_changed_email do
402
- mailer_send(:password_changed, email_to)
441
+ create_password_changed_email do
442
+ RodauthMailer.password_changed(email_to)
403
443
  end
404
- # send_email_auth_email do
405
- # mailer_send(:email_auth, email_to, email_auth_email_link)
444
+ # create_email_auth_email do
445
+ # RodauthMailer.email_auth(email_to, email_auth_email_link)
406
446
  # end
407
- # send_unlock_account_email do
408
- # mailer_send(:unlock_account, email_to, unlock_account_email_link)
447
+ # create_unlock_account_email do
448
+ # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
409
449
  # end
410
- auth_class_eval do
450
+ send_email do |email|
411
451
  # queue email delivery on the mailer after the transaction commits
412
- def mailer_send(type, *args)
413
- db.after_commit do
414
- RodauthMailer.public_send(type, *args).deliver_later
415
- end
416
- end
452
+ db.after_commit { email.deliver_later }
417
453
  end
418
454
  # ...
419
455
  end
420
456
  end
421
457
  ```
422
458
 
459
+ This approach can be used even if you're using a 3rd-party service for
460
+ transactional emails, where emails are sent via HTTP instead of SMTP. Whatever
461
+ the `create_*_email` block returns will be passed to `send_email`, so you can
462
+ be creative.
463
+
423
464
  ### Migrations
424
465
 
425
466
  The install generator will create a migration for tables used by the Rodauth
@@ -440,37 +481,32 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
440
481
  end
441
482
  ```
442
483
 
443
- ### JSON API
484
+ ### Calling controller methods
444
485
 
445
- JSON API support in Rodauth is provided by the [JWT feature]. First you'll need
446
- to add the [JWT gem] to your Gemfile:
486
+ When using Rodauth before/after hooks or generally overriding your Rodauth
487
+ configuration, in some cases you might want to call methods defined on your
488
+ controllers. You can do so with `rails_controller_eval`, for example:
447
489
 
448
490
  ```rb
449
- gem "jwt"
491
+ # app/controllers/application_controller.rb
492
+ class ApplicationController < ActionController::Base
493
+ private
494
+ def setup_tracking(account_id)
495
+ # ... some implementation ...
496
+ end
497
+ end
450
498
  ```
451
-
452
- The following configuration will enable the Rodauth endpoints to be accessed
453
- via JSON requests (in addition to HTML requests):
454
-
455
499
  ```rb
456
500
  # app/lib/rodauth_app.rb
457
501
  class RodauthApp < Rodauth::Rails::App
458
- configure(json: true) do
459
- # ...
460
- enable :jwt
461
- jwt_secret "...your secret key..."
462
- # ...
502
+ configure do
503
+ after_create_account do
504
+ rails_controller_eval { setup_tracking(account_id) }
505
+ end
463
506
  end
464
507
  end
465
508
  ```
466
509
 
467
- If you want the endpoints to be only accessible via JSON requests, or if your
468
- Rails app is in API-only mode, instead of `json: true` pass `json: :only` to
469
- the configure method.
470
-
471
- Make sure to store the `jwt_secret` in a secure place, such as Rails
472
- credentials or environment variables.
473
-
474
510
  ### Rodauth instance
475
511
 
476
512
  In some cases you might need to use Rodauth more programmatically, and perform
@@ -541,13 +577,38 @@ integration for Rodauth:
541
577
  * runs Action Controller callbacks & rescue handlers around Rodauth actions
542
578
  * uses Action Mailer for sending emails
543
579
 
544
- The `configure { ... }` method wraps configuring the Rodauth plugin, forwarding
580
+ The `configure` method wraps configuring the Rodauth plugin, forwarding
545
581
  any additional [plugin options].
546
582
 
547
583
  ```rb
548
- configure { ... } # defining default Rodauth configuration
549
- configure(json: true) { ... } # passing options to the Rodauth plugin
550
- configure(:secondary) { ... } # defining multiple Rodauth configurations
584
+ class RodauthApp < Rodauth::Rails::App
585
+ configure { ... } # defining default Rodauth configuration
586
+ configure(json: true) { ... } # passing options to the Rodauth plugin
587
+ configure(:secondary) { ... } # defining multiple Rodauth configurations
588
+ end
589
+ ```
590
+
591
+ The `route` block is provided by Roda, and it's called on each request before
592
+ it reaches the Rails router.
593
+
594
+ ```rb
595
+ class RodauthApp < Rodauth::Rails::App
596
+ route do |r|
597
+ # ... called before each request ...
598
+ end
599
+ end
600
+ ```
601
+
602
+ Since `Rodauth::Rails::App` is just a Roda subclass, you can do anything you
603
+ would with a Roda app, such as loading additional Roda plugins:
604
+
605
+ ```rb
606
+ class RodauthApp < Rodauth::Rails::App
607
+ plugin :request_headers # easier access to request headers
608
+ plugin :typecast_params # methods for conversion of request params
609
+ plugin :default_headers, { "Foo" => "Bar" }
610
+ # ...
611
+ end
551
612
  ```
552
613
 
553
614
  ### Sequel
@@ -563,6 +624,142 @@ connection (using the [sequel-activerecord_connection] gem).
563
624
  This means that, from the usage perspective, Sequel can be considered just
564
625
  as an implementation detail of Rodauth.
565
626
 
627
+ ## JSON API
628
+
629
+ JSON API support in Rodauth is provided by the [JWT feature][jwt]. You'll need
630
+ to install the [JWT gem], enable JSON support and enable the JWT feature:
631
+
632
+ ```sh
633
+ $ bundle add jwt
634
+ ```
635
+ ```rb
636
+ # app/lib/rodauth_app.rb
637
+ class RodauthApp < Rodauth::Rails::App
638
+ configure(json: :only) do
639
+ # ...
640
+ enable :jwt
641
+ # make sure to store the JWT secret below in a safe place
642
+ jwt_secret "...your secret key..."
643
+ # ...
644
+ end
645
+ end
646
+ ```
647
+
648
+ With the above configuration, Rodauth routes will only be accessible via JSON
649
+ requests. If you still want to allow HTML access alongside JSON, change `json:
650
+ :only` to `json: true`.
651
+
652
+ Emails will automatically work in JSON-only mode, because `Rodauth::Rails::App`
653
+ comes with Roda's `render` plugin loaded. They are customized the same as in
654
+ the non-JSON case.
655
+
656
+ ## OmniAuth
657
+
658
+ While Rodauth doesn't yet come with [OmniAuth] integration, we can build one
659
+ ourselves using the existing Rodauth API.
660
+
661
+ In order to allow the user to login via multiple external providers, let's
662
+ create an `account_identities` table that will have a many-to-one relationship
663
+ with the `accounts` table:
664
+
665
+ ```sh
666
+ $ rails generate model AccountIdentity
667
+ ```
668
+ ```rb
669
+ # db/migrate/*_create_account_identities.rb
670
+ class CreateAccountIdentities < ActiveRecord::Migration
671
+ def change
672
+ create_table :account_identities do |t|
673
+ t.references :account, null: false, foreign_key: { on_delete: :cascade }
674
+ t.string :provider, null: false
675
+ t.string :uid, null: false
676
+ t.jsonb :info, null: false, default: {} # adjust JSON column type for your database
677
+
678
+ t.timestamps
679
+
680
+ t.index [:provider, :uid], unique: true
681
+ end
682
+ end
683
+ end
684
+ ```
685
+ ```rb
686
+ # app/models/account_identity.rb
687
+ class AcccountIdentity < ApplicationRecord
688
+ belongs_to :account
689
+ end
690
+ ```
691
+ ```rb
692
+ # app/models/account.rb
693
+ class Account < ApplicationRecord
694
+ has_many :identities, class_name: "AccountIdentity"
695
+ end
696
+ ```
697
+
698
+ Let's assume we want to implement Facebook login, and have added the
699
+ corresponding OmniAuth strategy to the middleware stack, together with an
700
+ authorization link on the login form:
701
+
702
+ ```rb
703
+ Rails.application.config.middleware.use OmniAuth::Builder do
704
+ provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"],
705
+ scope: "email", callback_path: "/auth/facebook/callback"
706
+ end
707
+ ```
708
+ ```erb
709
+ <%= link_to "Login via Facebook", "/auth/facebook" %>
710
+ ```
711
+
712
+ Let's implement the OmniAuth callback endpoint on our Rodauth controller:
713
+
714
+ ```rb
715
+ # config/routes.rb
716
+ Rails.application.routes.draw do
717
+ # ...
718
+ get "/auth/:provider/callback", to: "rodauth#omniauth"
719
+ end
720
+ ```
721
+ ```rb
722
+ # app/controllres/rodauth_controller.rb
723
+ class RodauthController < ApplicationController
724
+ def omniauth
725
+ auth = request.env["omniauth.auth"]
726
+
727
+ # attempt to find existing identity directly
728
+ identity = AccountIdentity.find_by(provider: auth["provider"], uid: auth["uid"])
729
+
730
+ if identity
731
+ # update any external info changes
732
+ identity.update!(info: auth["info"])
733
+ # set account from identity
734
+ account = identity.account
735
+ end
736
+
737
+ # attempt to find an existing account by email
738
+ account ||= Account.find_by(email: auth["info"]["email"])
739
+
740
+ # disallow login if account is not verified
741
+ if account && account.status != rodauth.account_open_status_value
742
+ redirect_to rodauth.login_path, alert: rodauth.unverified_account_message
743
+ return
744
+ end
745
+
746
+ # create new account if it doesn't exist
747
+ unless account
748
+ account = Account.create!(email: auth["info"]["email"])
749
+ end
750
+
751
+ # create new identity if it doesn't exist
752
+ unless identity
753
+ account.identities.create!(provider: auth["provider"], uid: auth["uid"], info: auth["info"])
754
+ end
755
+
756
+ # login with Rodauth
757
+ rodauth.account_from_login(account.email)
758
+ rodauth.login("omniauth")
759
+ end
760
+ end
761
+ ```
762
+
566
763
  ## Configuring
567
764
 
568
765
  For the list of configuration methods provided by Rodauth, see the [feature
@@ -596,6 +793,37 @@ Rodauth::Rails.configure do |config|
596
793
  end
597
794
  ```
598
795
 
796
+ ## Custom extensions
797
+
798
+ When developing custom extensions for Rodauth inside your Rails project, it's
799
+ better to use plain modules (at least in the beginning), because Rodauth
800
+ feature API doesn't yet support Zeitwerk reloading well.
801
+
802
+ ```rb
803
+ # app/lib/rodauth_argon2.rb
804
+ module RodauthArgon2
805
+ def password_hash(password)
806
+ Argon2::Password.create(password, t_cost: password_hash_cost, m_cost: password_hash_cost)
807
+ end
808
+
809
+ def password_hash_match?(hash, password)
810
+ Argon2::Password.verify_password(password, hash)
811
+ end
812
+ end
813
+ ```
814
+ ```rb
815
+ # app/lib/rodauth_app.rb
816
+ class RodauthApp < Rodauth::Rails::App
817
+ configure do
818
+ # ...
819
+ auth_class_eval do
820
+ include RodauthArgon2
821
+ end
822
+ # ...
823
+ end
824
+ end
825
+ ```
826
+
599
827
  ## Testing
600
828
 
601
829
  If you're writing system tests, it's generally better to go through the actual
@@ -668,6 +896,8 @@ Rodauth method for creating database functions:
668
896
 
669
897
  ```rb
670
898
  # db/migrate/*_create_rodauth_database_functions.rb
899
+ require "rodauth/migrations"
900
+
671
901
  class CreateRodauthDatabaseFunctions < ActiveRecord::Migration
672
902
  def up
673
903
  Rodauth.create_database_authentication_functions(DB)
@@ -732,7 +962,6 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
732
962
  [Rodauth]: https://github.com/jeremyevans/rodauth
733
963
  [Sequel]: https://github.com/jeremyevans/sequel
734
964
  [feature documentation]: http://rodauth.jeremyevans.net/documentation.html
735
- [JWT feature]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
736
965
  [JWT gem]: https://github.com/jwt/ruby-jwt
737
966
  [Bootstrap]: https://getbootstrap.com/
738
967
  [Roda]: http://roda.jeremyevans.net/
@@ -741,3 +970,20 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
741
970
  [Rodauth migration]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Creating+tables
742
971
  [sequel-activerecord_connection]: https://github.com/janko/sequel-activerecord_connection
743
972
  [plugin options]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-Plugin+Options
973
+ [hmac]: http://rodauth.jeremyevans.net/rdoc/files/README_rdoc.html#label-HMAC
974
+ [OmniAuth]: https://github.com/omniauth/omniauth
975
+ [otp]: http://rodauth.jeremyevans.net/rdoc/files/doc/otp_rdoc.html
976
+ [sms_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/sms_codes_rdoc.html
977
+ [recovery_codes]: http://rodauth.jeremyevans.net/rdoc/files/doc/recovery_codes_rdoc.html
978
+ [webauthn]: http://rodauth.jeremyevans.net/rdoc/files/doc/webauthn_rdoc.html
979
+ [jwt]: http://rodauth.jeremyevans.net/rdoc/files/doc/jwt_rdoc.html
980
+ [email_auth]: http://rodauth.jeremyevans.net/rdoc/files/doc/email_auth_rdoc.html
981
+ [audit_logging]: http://rodauth.jeremyevans.net/rdoc/files/doc/audit_logging_rdoc.html
982
+ [password protection]: https://github.com/jeremyevans/rodauth#label-Password+Hash+Access+Via+Database+Functions
983
+ [bruteforce tokens]: https://github.com/jeremyevans/rodauth#label-Tokens
984
+ [password_complexity]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_complexity_rdoc.html
985
+ [disallow_password_reuse]: http://rodauth.jeremyevans.net/rdoc/files/doc/disallow_password_reuse_rdoc.html
986
+ [password_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/password_expiration_rdoc.html
987
+ [session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
988
+ [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
989
+ [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
@@ -13,6 +13,15 @@ module Rodauth
13
13
  source_root "#{__dir__}/templates"
14
14
  namespace "rodauth:install"
15
15
 
16
+ # The :api option is a Rails-recognized option that always
17
+ # defaults to false, so we make it use our provided default
18
+ # value instead.
19
+ def self.default_value_for_option(name, options)
20
+ name == :api ? options[:default] : super
21
+ end
22
+
23
+ class_option :api, type: :boolean, desc: "Generate JSON-only configuration"
24
+
16
25
  def create_rodauth_migration
17
26
  return unless defined?(ActiveRecord::Base)
18
27
 
@@ -35,8 +44,6 @@ module Rodauth
35
44
  end
36
45
 
37
46
  def create_rodauth_controller
38
- return if api_only?
39
-
40
47
  template "app/controllers/rodauth_controller.rb"
41
48
  end
42
49
 
@@ -77,9 +84,11 @@ module Rodauth
77
84
  end
78
85
 
79
86
  def api_only?
80
- return unless ::Rails.gem_version >= Gem::Version.new("5.0")
81
-
82
- ::Rails.application.config.api_only
87
+ if options.key?(:api)
88
+ options[:api]
89
+ elsif ::Rails.gem_version >= Gem::Version.new("5.0")
90
+ ::Rails.application.config.api_only
91
+ end
83
92
  end
84
93
 
85
94
  def migration_features
@@ -1,3 +1,4 @@
1
1
  class RodauthController < ApplicationController
2
- # used by Rodauth for rendering views and CSRF protection
2
+ # used by Rodauth for rendering views, CSRF protection, and running any
3
+ # registered action callbacks and rescue_from handlers
3
4
  end
@@ -15,11 +15,9 @@ class RodauthApp < Rodauth::Rails::App
15
15
  # Defaults to Rails `secret_key_base`, but you can use your own secret key.
16
16
  # hmac_secret "<%= SecureRandom.hex(64) %>"
17
17
 
18
- <% unless api_only? -%>
19
18
  # Specify the controller used for view rendering and CSRF verification.
20
19
  rails_controller { RodauthController }
21
20
 
22
- <% end -%>
23
21
  # Store account status in a text column.
24
22
  account_status_column :status
25
23
  account_unverified_status_value "unverified"
@@ -59,31 +57,27 @@ class RodauthApp < Rodauth::Rails::App
59
57
 
60
58
  # ==> Emails
61
59
  # Uncomment the lines below once you've imported mailer views.
62
- # send_reset_password_email do
63
- # mailer_send(:reset_password, email_to, reset_password_email_link)
60
+ # create_reset_password_email do
61
+ # RodauthMailer.reset_password(email_to, reset_password_email_link)
64
62
  # end
65
- # send_verify_account_email do
66
- # mailer_send(:verify_account, email_to, verify_account_email_link)
63
+ # create_verify_account_email do
64
+ # RodauthMailer.verify_account(email_to, verify_account_email_link)
67
65
  # end
68
- # send_verify_login_change_email do |login|
69
- # mailer_send(:verify_login_change, login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
66
+ # create_verify_login_change_email do |login|
67
+ # RodauthMailer.verify_login_change(login, verify_login_change_old_login, verify_login_change_new_login, verify_login_change_email_link)
70
68
  # end
71
- # send_password_changed_email do
72
- # mailer_send(:password_changed, email_to)
69
+ # create_password_changed_email do
70
+ # RodauthMailer.password_changed(email_to)
73
71
  # end
74
- # # send_email_auth_email do
75
- # # mailer_send(:email_auth, email_to, email_auth_email_link)
72
+ # # create_email_auth_email do
73
+ # # RodauthMailer.email_auth(email_to, email_auth_email_link)
76
74
  # # end
77
- # # send_unlock_account_email do
78
- # # mailer_send(:unlock_account, email_to, unlock_account_email_link)
75
+ # # create_unlock_account_email do
76
+ # # RodauthMailer.unlock_account(email_to, unlock_account_email_link)
79
77
  # # end
80
- # auth_class_eval do
78
+ # send_email do |email|
81
79
  # # queue email delivery on the mailer after the transaction commits
82
- # def mailer_send(type, *args)
83
- # db.after_commit do
84
- # RodauthMailer.public_send(type, *args).deliver_later
85
- # end
86
- # end
80
+ # db.after_commit { email.deliver_later }
87
81
  # end
88
82
 
89
83
  # In the meantime you can tweak settings for emails created by Rodauth
@@ -32,6 +32,16 @@ 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
+
35
45
  def configure
36
46
  yield self
37
47
  end
@@ -1,16 +1,20 @@
1
1
  require "roda"
2
+ require "rodauth"
3
+ require "rodauth/rails/feature"
2
4
 
3
5
  module Rodauth
4
6
  module Rails
5
7
  # The superclass for creating a Rodauth middleware.
6
8
  class App < Roda
7
- plugin :middleware
9
+ require "rodauth/rails/app/middleware"
10
+ plugin Middleware
11
+
8
12
  plugin :hooks
9
13
  plugin :render, layout: false
10
14
 
11
15
  def self.configure(name = nil, **options, &block)
12
16
  unless options[:json] == :only
13
- require "rodauth/rails/flash"
17
+ require "rodauth/rails/app/flash"
14
18
  plugin Flash
15
19
  end
16
20
 
@@ -18,6 +22,12 @@ module Rodauth
18
22
  # load the Rails integration
19
23
  enable :rails
20
24
 
25
+ if options[:json] == :only && ActionPack.version >= Gem::Version.new("5.0")
26
+ rails_controller { ActionController::API }
27
+ else
28
+ rails_controller { ActionController::Base }
29
+ end
30
+
21
31
  # database functions are more complex to set up, so disable them by default
22
32
  use_database_authentication_functions? false
23
33
 
@@ -25,7 +35,7 @@ module Rodauth
25
35
  set_deadline_values? true
26
36
 
27
37
  # use HMACs for additional security
28
- hmac_secret { ::Rails.application.secrets.secret_key_base }
38
+ hmac_secret { Rodauth::Rails.secret_key_base }
29
39
 
30
40
  # evaluate user configuration
31
41
  instance_exec(&block)
@@ -0,0 +1,50 @@
1
+ module Rodauth
2
+ module Rails
3
+ class App
4
+ # Roda plugin that sets up Rails flash integration.
5
+ module Flash
6
+ def self.load_dependencies(app)
7
+ app.plugin :hooks
8
+ end
9
+
10
+ def self.configure(app)
11
+ app.before { request.flash } # load flash
12
+ app.after { request.commit_flash } # save flash
13
+ end
14
+
15
+ module InstanceMethods
16
+ def flash
17
+ request.flash
18
+ end
19
+ end
20
+
21
+ module RequestMethods
22
+ # If the redirect would bubble up outside of the Roda app, the after
23
+ # hook would never get called, so we make sure to commit the flash.
24
+ def redirect(*)
25
+ commit_flash
26
+ super
27
+ end
28
+
29
+ def flash
30
+ rails_request.flash
31
+ end
32
+
33
+ def commit_flash
34
+ if ActionPack.version >= Gem::Version.new("5.0")
35
+ rails_request.commit_flash
36
+ else
37
+ # ActionPack 4.2 automatically commits flash
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def rails_request
44
+ ActionDispatch::Request.new(env)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ module Rodauth
2
+ module Rails
3
+ class App
4
+ # Roda plugin that extends middleware plugin by propagating response headers.
5
+ module Middleware
6
+ def self.load_dependencies(app)
7
+ app.plugin :hooks
8
+ end
9
+
10
+ def self.configure(app)
11
+ app.after do
12
+ if response.empty? && response.headers.any?
13
+ env["rodauth.rails.headers"] = response.headers
14
+ end
15
+ end
16
+
17
+ app.plugin :middleware, handle_result: -> (env, res) do
18
+ if headers = env.delete("rodauth.rails.headers")
19
+ res[1] = headers.merge(res[1])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ 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,25 +159,37 @@ 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
- request = ActionDispatch::Request.new(scope.env)
149
- instance = rails_controller.new
171
+ controller = rails_controller.new
172
+ rails_request = ActionDispatch::Request.new(scope.env)
150
173
 
151
- if ActionPack.version >= Gem::Version.new("5.0")
152
- instance.set_request! request
153
- instance.set_response! rails_controller.make_response!(request)
154
- else
155
- instance.send(:set_response!, request)
156
- instance.instance_variable_set(:@_request, request)
157
- end
174
+ prepare_rails_controller(controller, rails_request)
158
175
 
159
- instance
176
+ controller
177
+ end
178
+
179
+ if ActionPack.version >= Gem::Version.new("5.0")
180
+ def prepare_rails_controller(controller, rails_request)
181
+ controller.set_request! rails_request
182
+ controller.set_response! rails_controller.make_response!(rails_request)
183
+ end
184
+ else
185
+ def prepare_rails_controller(controller, rails_request)
186
+ controller.send(:set_response!, rails_request)
187
+ controller.instance_variable_set(:@_request, rails_request)
188
+ end
160
189
  end
161
190
 
162
- # Controller class to use for rendering and CSRF protection.
163
- def rails_controller
164
- ActionController::Base
191
+ def rails_api_controller?
192
+ defined?(ActionController::API) && rails_controller <= ActionController::API
165
193
  end
166
194
 
167
195
  # ActionMailer subclass for correct email delivering.
@@ -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.0"
3
+ VERSION = "0.8.2"
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.8"
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.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-22 00:00:00.000000000 Z
11
+ date: 2021-01-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.8'
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.8'
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,12 +201,12 @@ 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
206
+ - lib/rodauth/rails/app/flash.rb
207
+ - lib/rodauth/rails/app/middleware.rb
193
208
  - lib/rodauth/rails/controller_methods.rb
194
209
  - lib/rodauth/rails/feature.rb
195
- - lib/rodauth/rails/flash.rb
196
210
  - lib/rodauth/rails/middleware.rb
197
211
  - lib/rodauth/rails/railtie.rb
198
212
  - lib/rodauth/rails/tasks.rake
@@ -1 +0,0 @@
1
- require "rodauth/rails/feature"
@@ -1,48 +0,0 @@
1
- module Rodauth
2
- module Rails
3
- # Roda plugin that sets up Rails flash integration.
4
- module Flash
5
- def self.load_dependencies(app)
6
- app.plugin :hooks
7
- end
8
-
9
- def self.configure(app)
10
- app.before { request.flash } # load flash
11
- app.after { request.commit_flash } # save flash
12
- end
13
-
14
- module InstanceMethods
15
- def flash
16
- request.flash
17
- end
18
- end
19
-
20
- module RequestMethods
21
- # If the redirect would bubble up outside of the Roda app, the after
22
- # hook would never get called, so we make sure to commit the flash.
23
- def redirect(*)
24
- commit_flash
25
- super
26
- end
27
-
28
- def flash
29
- rails_request.flash
30
- end
31
-
32
- def commit_flash
33
- if ActionPack.version >= Gem::Version.new("5.0")
34
- rails_request.commit_flash
35
- else
36
- # ActionPack 4.2 automatically commits flash
37
- end
38
- end
39
-
40
- private
41
-
42
- def rails_request
43
- ActionDispatch::Request.new(env)
44
- end
45
- end
46
- end
47
- end
48
- end