rodauth-rails 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d447d09fef8c29feb6240523286b8906049e85965f20a6410d1a475f913d9051
4
- data.tar.gz: bca9b6eadec6b32f2193291c6922467a554105d290ffd7b34bc2606d62121926
3
+ metadata.gz: '097625662e9cefbf7484ea775c9b1930968fbbc3a1b8ad24df9e229e2194e301'
4
+ data.tar.gz: fbbfe9dd849646e859aecbf3c600168fc0b65255f7b2bf933a2e42591f8b0b79
5
5
  SHA512:
6
- metadata.gz: 1f512f9fe9a3e22dcddf477d8906d1ea63a548241fd93b43bbcaf274ff39e0104e20f64c6a2836e5b243e812ffde654deae55a0beca69f4ba917cd5943da8a3c
7
- data.tar.gz: dbbd99959dfd42134cd3374f1f9767cf3e8d49327c195d4c35c4ecf281d0c3dad52db76b7e2fbf030c9e3ea2131bfcb1b6a120cc4a310983d8db564e63b97cda
6
+ metadata.gz: 762d0c1725dcd0017cdd6722894e546dfdf246e245af688a8bc99f177e43765fe8bd7a79639e45d3146ca5bedadce9f34389f61bf3a6957b406cbc664cf93829
7
+ data.tar.gz: 8c6624c70668356b8434dde9bd237cced89f23d4d241b770989f1af68ee687b7186f305ada409885d619b9974e7d2d47b6fddc9faf6935653cab805ee8709167
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.15.0 (2021-07-29)
2
+
3
+ * Add `Rodauth::Rails::Model` mixin that defines password attribute and associations on the model (@janko)
4
+
5
+ * Add support for the new internal_request feature (@janko)
6
+
7
+ * Implement `Rodauth::Rails.rodauth` in terms of the internal_request feature (@janko)
8
+
1
9
  ## 0.14.0 (2021-07-10)
2
10
 
3
11
  * Speed up template rendering by only searching formats accepted by the request (@janko)
data/README.md CHANGED
@@ -49,7 +49,7 @@ For instructions on upgrading from previous rodauth-rails versions, see
49
49
  Add the gem to your Gemfile:
50
50
 
51
51
  ```rb
52
- gem "rodauth-rails", "~> 0.14"
52
+ gem "rodauth-rails", "~> 0.15"
53
53
 
54
54
  # gem "jwt", require: false # for JWT feature
55
55
  # gem "rotp", require: false # for OTP feature
@@ -453,6 +453,130 @@ class CreateRodauthOtpSmsCodesRecoveryCodes < ActiveRecord::Migration
453
453
  end
454
454
  ```
455
455
 
456
+ ### Model
457
+
458
+ The `Rodauth::Rails::Model` mixin can be included into the account model, which
459
+ defines a password attribute and associations for tables used by enabled
460
+ authentication features.
461
+
462
+ ```rb
463
+ class Account < ApplicationRecord
464
+ include Rodauth::Rails.model # or `Rodauth::Rails.model(:admin)`
465
+ end
466
+ ```
467
+
468
+ #### Password attribute
469
+
470
+ Regardless of whether you're storing the password hash in a column in the
471
+ accounts table, or in a separate table, the `#password` attribute can be used
472
+ to set or clear the password hash.
473
+
474
+ ```rb
475
+ account = Account.create!(email: "user@example.com", password: "secret")
476
+
477
+ # when password hash is stored in a column on the accounts table
478
+ account.password_hash #=> "$2a$12$k/Ub1I2iomi84RacqY89Hu4.M0vK7klRnRtzorDyvOkVI.hKhkNw."
479
+
480
+ # when password hash is stored in a separate table
481
+ account.password_hash #=> #<Account::PasswordHash...> (record from `account_password_hashes` table)
482
+ account.password_hash.password_hash #=> "$2a$12$k/Ub1..." (inaccessible when using database authentication functions)
483
+
484
+ account.password = nil # clears password hash
485
+ account.password_hash #=> nil
486
+ ```
487
+
488
+ Note that the password attribute doesn't come with validations, making it
489
+ unsuitable for forms. It was primarily intended to allow easily creating
490
+ accounts in development console and in tests.
491
+
492
+ #### Associations
493
+
494
+ The `Rodauth::Rails::Model` mixin defines associations for Rodauth tables
495
+ associated to the accounts table:
496
+
497
+ ```rb
498
+ account.remember_key #=> #<Account::RememberKey> (record from `account_remember_keys` table)
499
+ account.active_session_keys #=> [#<Account::ActiveSessionKey>,...] (records from `account_active_session_keys` table)
500
+ ```
501
+
502
+ You can also reference the associated models directly:
503
+
504
+ ```rb
505
+ # model referencing the `account_authentication_audit_logs` table
506
+ Account::AuthenticationAuditLog.where(message: "login").group(:account_id)
507
+ ```
508
+
509
+ The associated models define the inverse `belongs_to :account` association:
510
+
511
+ ```rb
512
+ Account::ActiveSessionKey.includes(:account).map(&:account)
513
+ ```
514
+
515
+ Here is an example of using associations to create a method that returns
516
+ whether the account has multifactor authentication enabled:
517
+
518
+ ```rb
519
+ class Account < ApplicationRecord
520
+ include Rodauth::Rails.model
521
+
522
+ def mfa_enabled?
523
+ otp_key || (sms_code && sms_code.num_failures.nil?) || recovery_codes.any?
524
+ end
525
+ end
526
+ ```
527
+
528
+ Here is another example of creating a query scope that selects accounts with
529
+ multifactor authentication enabled:
530
+
531
+ ```rb
532
+ class Account < ApplicationRecord
533
+ include Rodauth::Rails.model
534
+
535
+ scope :otp_setup, -> { where(otp_key: OtpKey.all) }
536
+ scope :sms_codes_setup, -> { where(sms_code: SmsCode.where(num_failures: nil)) }
537
+ scope :recovery_codes_setup, -> { where(recovery_codes: RecoveryCode.all) }
538
+ scope :mfa_enabled, -> { merge(otp_setup.or(sms_codes_setup).or(recovery_codes_setup)) }
539
+ end
540
+ ```
541
+
542
+ Below is a list of all associations defined depending on the features loaded:
543
+
544
+ | Feature | Association | Type | Model | Table (default) |
545
+ | :------ | :---------- | :--- | :---- | :---- |
546
+ | account_expiration | `:activity_time` | `has_one` | `ActivityTime` | `account_activity_times` |
547
+ | active_sessions | `:active_session_keys` | `has_many` | `ActiveSessionKey` | `account_active_session_keys` |
548
+ | audit_logging | `:authentication_audit_logs` | `has_many` | `AuthenticationAuditLog` | `account_authentication_audit_logs` |
549
+ | disallow_password_reuse | `:previous_password_hashes` | `has_many` | `PreviousPasswordHash` | `account_previous_password_hashes` |
550
+ | email_auth | `:email_auth_key` | `has_one` | `EmailAuthKey` | `account_email_auth_keys` |
551
+ | jwt_refresh | `:jwt_refresh_keys` | `has_many` | `JwtRefreshKey` | `account_jwt_refresh_keys` |
552
+ | lockout | `:lockout` | `has_one` | `Lockout` | `account_lockouts` |
553
+ | lockout | `:login_failure` | `has_one` | `LoginFailure` | `account_login_failures` |
554
+ | otp | `:otp_key` | `has_one` | `OtpKey` | `account_otp_keys` |
555
+ | password_expiration | `:password_change_time` | `has_one` | `PasswordChangeTime` | `account_password_change_times` |
556
+ | recovery_codes | `:recovery_codes` | `has_many` | `RecoveryCode` | `account_recovery_codes` |
557
+ | remember | `:remember_key` | `has_one` | `RememberKey` | `account_remember_keys` |
558
+ | reset_password | `:password_reset_key` | `has_one` | `PasswordResetKey` | `account_password_reset_keys` |
559
+ | single_session | `:session_key` | `has_one` | `SessionKey` | `account_session_keys` |
560
+ | sms_codes | `:sms_code` | `has_one` | `SmsCode` | `account_sms_codes` |
561
+ | verify_account | `:verification_key` | `has_one` | `VerificationKey` | `account_verification_keys` |
562
+ | verify_login_change | `:login_change_key` | `has_one` | `LoginChangeKey` | `account_login_change_keys` |
563
+ | webauthn | `:webauthn_keys` | `has_many` | `WebauthnKey` | `account_webauthn_keys` |
564
+ | webauthn | `:webauthn_user_id` | `has_one` | `WebauthnUserId` | `account_webauthn_user_ids` |
565
+
566
+ By default, all associations except for audit logs have `dependent: :destroy`
567
+ set, to allow for easy deletion of account records in the console. You can use
568
+ `:association_options` to modify global or per-association options:
569
+
570
+ ```rb
571
+ # don't auto-delete associations when account model is deleted
572
+ Rodauth::Rails.model(association_options: { dependent: nil })
573
+
574
+ # require authentication audit logs to be eager loaded before retrieval
575
+ Rodauth::Rails.model(association_options: -> (name) {
576
+ { strict_loading: true } if name == :authentication_audit_logs
577
+ })
578
+ ```
579
+
456
580
  ### Multiple configurations
457
581
 
458
582
  If you need to handle multiple types of accounts that require different
@@ -656,37 +780,53 @@ class RodauthApp < Rodauth::Rails::App
656
780
  end
657
781
  ```
658
782
 
659
- ### Rodauth instance
783
+ ### Outside of a request
784
+
785
+ In some cases you might need to use Rodauth more programmatically. If you would
786
+ like to perform Rodauth operations outside of request context, Rodauth ships
787
+ with the [internal_request] feature just for that. The rodauth-rails gem
788
+ additionally updates the internal rack env hash with your
789
+ `config.action_mailer.default_url_options`, which is used for generating URLs.
660
790
 
661
- In some cases you might need to use Rodauth more programmatically, and perform
662
- Rodauth operations outside of the request context. rodauth-rails gives you a
663
- helper method for building a Rodauth instance:
791
+ If you need to access Rodauth methods not exposed as internal requests, you can
792
+ use `Rodauth::Rails.rodauth` to retrieve the Rodauth instance used by the
793
+ internal_request feature:
664
794
 
665
795
  ```rb
666
- rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
796
+ # app/lib/rodauth_app.rb
797
+ class RodauthApp < Rodauth::Rails::App
798
+ configure do
799
+ enable :internal_request # this is required
800
+ end
801
+ end
802
+ ```
803
+ ```rb
804
+ account = Account.find_by!(email: "user@example.com")
805
+ rodauth = Rodauth::Rails.rodauth(account: account)
667
806
 
668
- rodauth.login_url #=> "https://example.com/login"
669
- rodauth.account_from_login("user@example.com") # loads user by email
670
- rodauth.password_match?("secret") #=> true
671
- rodauth.setup_account_verification
672
- rodauth.close_account
807
+ rodauth.compute_hmac("token") #=> "TpEJTKfKwqYvIDKWsuZhkhKlhaBXtR1aodskBAflD8U"
808
+ rodauth.open_account? #=> true
809
+ rodauth.two_factor_authentication_setup? #=> true
810
+ rodauth.password_meets_requirements?("foo") #=> false
811
+ rodauth.locked_out? #=> false
673
812
  ```
674
813
 
675
- The base URL is taken from Action Mailer's `default_url_options` setting if
676
- configured. The `Rodauth::Rails.rodauth` method accepts additional keyword
677
- arguments:
814
+ In addition to the `:account` option, the `Rodauth::Rails.rodauth`
815
+ method accepts any options supported by the internal_request feature.
816
+
817
+ ```rb
818
+ Rodauth::Rails.rodauth(
819
+ env: { "HTTP_USER_AGENT" => "programmatic" },
820
+ session: { two_factor_auth_setup: true },
821
+ params: { "param" => "value" }
822
+ )
823
+ ```
678
824
 
679
- * `:account` Active Record model instance from which to set `account` and `session[:account_id]`
680
- * `:query` & `:form` – set specific query/form parameters
681
- * `:session` – set any session values
682
- * `:env` – set any additional Rack env values
825
+ Secondary Rodauth configurations are specified by passing the configuration
826
+ name:
683
827
 
684
828
  ```rb
685
- Rodauth::Rails.rodauth(account: Account.find(account_id))
686
- Rodauth::Rails.rodauth(query: { "param" => "value" })
687
- Rodauth::Rails.rodauth(form: { "param" => "value" })
688
- Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
689
- Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
829
+ Rodauth::Rails.rodauth(:admin)
690
830
  ```
691
831
 
692
832
  ## How it works
@@ -825,21 +965,23 @@ class RodauthApp < Rodauth::Rails::App
825
965
  end
826
966
  ```
827
967
 
828
- If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
829
- corresponding Rodauth features and create the necessary tables:
968
+ The JWT token will be returned after each request to Rodauth routes. To also
969
+ return the JWT token on requests to your app's routes, you can add the
970
+ following code to your base controller:
830
971
 
831
- ```sh
832
- $ rails generate rodauth:migration jwt_refresh
833
- $ rails db:migrate
834
- ```
835
972
  ```rb
836
- # app/lib/rodauth_app.rb
837
- class RodauthApp < Rodauth::Rails::App
838
- configure do
839
- # ...
840
- enable :jwt, :jwt_cors, :jwt_refresh
841
- # ...
973
+ class ApplicationController < ActionController::Base
974
+ # ...
975
+ after_action :set_jwt_token
976
+
977
+ private
978
+
979
+ def set_jwt_token
980
+ if rodauth.use_jwt? && rodauth.valid_jwt?
981
+ response.headers["Authorization"] = rodauth.session_jwt
982
+ end
842
983
  end
984
+ # ...
843
985
  end
844
986
  ```
845
987
 
@@ -1147,29 +1289,6 @@ Rails.application.configure do |config|
1147
1289
  end
1148
1290
  ```
1149
1291
 
1150
- If you need to create an account record with a password directly, you can do it
1151
- as follows:
1152
-
1153
- ```rb
1154
- # app/models/account.rb
1155
- class Account < ApplicationRecord
1156
- has_one :password_hash, foreign_key: :id
1157
- end
1158
- ```
1159
- ```rb
1160
- # app/models/account/password_hash.rb
1161
- class Account::PasswordHash < ApplicationRecord
1162
- belongs_to :account, foreign_key: :id
1163
- end
1164
- ```
1165
- ```rb
1166
- require "bcrypt"
1167
-
1168
- account = Account.create!(email: "user@example.com", status: "verified")
1169
- password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
1170
- account.create_password_hash!(id: account.id, password_hash: password_hash)
1171
- ```
1172
-
1173
1292
  ## Rodauth defaults
1174
1293
 
1175
1294
  rodauth-rails changes some of the default Rodauth settings for easier setup:
@@ -1303,3 +1422,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
1303
1422
  [single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
1304
1423
  [account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
1305
1424
  [simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
1425
+ [internal_request]: http://rodauth.jeremyevans.net/rdoc/files/doc/internal_request_rdoc.html
@@ -154,7 +154,7 @@ class RodauthApp < Rodauth::Rails::App
154
154
  <% end -%>
155
155
  end
156
156
 
157
- # ==> Multiple configurations
157
+ # ==> Secondary configurations
158
158
  # configure(:admin) do
159
159
  # # ... enable features ...
160
160
  # prefix "/admin"
@@ -163,11 +163,6 @@ class RodauthApp < Rodauth::Rails::App
163
163
  #
164
164
  # # search views in `app/views/admin/rodauth` directory
165
165
  # rails_controller { Admin::RodauthController }
166
- #
167
- # # use separate tables (requires creating the new tables)
168
- # methods.grep(/_table$/) do |table_method|
169
- # public_send(table_method) { :"admin_#{super()}" }
170
- # end
171
166
  # end
172
167
 
173
168
  route do |r|
@@ -189,7 +184,7 @@ class RodauthApp < Rodauth::Rails::App
189
184
  # rodauth.require_authentication
190
185
  # end
191
186
 
192
- # ==> Multiple configurations
187
+ # ==> Secondary configurations
193
188
  # r.on "admin" do
194
189
  # r.rodauth(:admin)
195
190
  #
@@ -197,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
197
192
  # rodauth(:admin).require_http_basic_auth
198
193
  # end
199
194
  #
200
- # r.pass # allow the Rails app to handle other "/admin/*" requests
195
+ # break # allow the Rails app to handle other "/admin/*" requests
201
196
  # end
202
197
  end
203
198
  end
@@ -1,2 +1,3 @@
1
1
  class Account < ApplicationRecord
2
+ include Rodauth::Rails.model
2
3
  end
@@ -129,8 +129,9 @@ module Rodauth
129
129
  end
130
130
 
131
131
  def controller
132
- rodauth = Rodauth::Rails.rodauth(configuration_name)
133
- rodauth.rails_controller
132
+ rodauth = Rodauth::Rails.app.rodauth(configuration_name)
133
+ fail ArgumentError, "unknown rodauth configuration: #{configuration_name.inspect}" unless rodauth
134
+ rodauth.allocate.rails_controller
134
135
  end
135
136
 
136
137
  def configuration_name
data/lib/rodauth/rails.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  require "rodauth/rails/version"
2
2
  require "rodauth/rails/railtie"
3
3
 
4
- require "rack/utils"
5
- require "stringio"
6
-
7
4
  module Rodauth
8
5
  module Rails
9
6
  class Error < StandardError
@@ -12,49 +9,49 @@ module Rodauth
12
9
  # This allows the developer to avoid loading Rodauth at boot time.
13
10
  autoload :App, "rodauth/rails/app"
14
11
  autoload :Auth, "rodauth/rails/auth"
12
+ autoload :Model, "rodauth/rails/model"
15
13
 
16
14
  @app = nil
17
15
  @middleware = true
18
16
 
17
+ LOCK = Mutex.new
18
+
19
19
  class << self
20
- def rodauth(name = nil, query: {}, form: {}, session: {}, account: nil, env: {})
21
- unless app.rodauth(name)
20
+ def rodauth(name = nil, query: nil, form: nil, account: nil, **options)
21
+ auth_class = app.rodauth(name)
22
+
23
+ unless auth_class
22
24
  fail ArgumentError, "undefined rodauth configuration: #{name.inspect}"
23
25
  end
24
26
 
25
- url_options = ActionMailer::Base.default_url_options
26
-
27
- scheme = url_options[:protocol] || "http"
28
- port = url_options[:port]
29
- port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
30
- host = url_options[:host]
31
- host += ":#{port}" if port
32
-
33
- content_type = "application/x-www-form-urlencoded" if form.any?
34
-
35
- rack_env = {
36
- "QUERY_STRING" => Rack::Utils.build_nested_query(query),
37
- "rack.input" => StringIO.new(Rack::Utils.build_nested_query(form)),
38
- "CONTENT_TYPE" => content_type,
39
- "rack.session" => {},
40
- "HTTP_HOST" => host,
41
- "rack.url_scheme" => scheme,
42
- }.merge(env)
43
-
44
- scope = app.new(rack_env)
45
- instance = scope.rodauth(name)
27
+ LOCK.synchronize do
28
+ unless auth_class.features.include?(:internal_request)
29
+ auth_class.configure { enable :internal_request }
30
+ warn "Rodauth::Rails.rodauth requires the internal_request feature to be enabled. For now it was enabled automatically, but this behaviour will be removed in version 1.0."
31
+ end
32
+ end
46
33
 
47
- # update session hash here to make it work with JWT session
48
- instance.session.merge!(session)
34
+ if query || form
35
+ warn "The :query and :form keyword arguments for Rodauth::Rails.rodauth have been deprecated. Please use the :params argument supported by internal_request feature instead."
36
+ options[:params] = query || form
37
+ end
49
38
 
50
39
  if account
51
- instance.instance_variable_set(:@account, account.attributes.symbolize_keys)
52
- instance.session[instance.session_key] = instance.account_session_value
40
+ options[:account_id] = account.id
41
+ end
42
+
43
+ instance = auth_class.internal_request_eval(options) do
44
+ @account = account.attributes.symbolize_keys if account
45
+ self
53
46
  end
54
47
 
55
48
  instance
56
49
  end
57
50
 
51
+ def model(name = nil, **options)
52
+ Rodauth::Rails::Model.new(app.rodauth(name), **options)
53
+ end
54
+
58
55
  # routing constraint that requires authentication
59
56
  def authenticated(name = nil, &condition)
60
57
  lambda do |request|
@@ -10,6 +10,7 @@ module Rodauth
10
10
  require "rodauth/rails/feature/render"
11
11
  require "rodauth/rails/feature/email"
12
12
  require "rodauth/rails/feature/instrumentation"
13
+ require "rodauth/rails/feature/internal_request"
13
14
 
14
15
  include Rodauth::Rails::Feature::Base
15
16
  include Rodauth::Rails::Feature::Callbacks
@@ -17,5 +18,6 @@ module Rodauth
17
18
  include Rodauth::Rails::Feature::Render
18
19
  include Rodauth::Rails::Feature::Email
19
20
  include Rodauth::Rails::Feature::Instrumentation
21
+ include Rodauth::Rails::Feature::InternalRequest
20
22
  end
21
23
  end
@@ -4,13 +4,17 @@ module Rodauth
4
4
  module Callbacks
5
5
  private
6
6
 
7
+ def _around_rodauth
8
+ rails_controller_around { super }
9
+ end
10
+
7
11
  # Runs controller callbacks and rescue handlers around Rodauth actions.
8
- def _around_rodauth(&block)
12
+ def rails_controller_around
9
13
  result = nil
10
14
 
11
15
  rails_controller_rescue do
12
16
  rails_controller_callbacks do
13
- result = catch(:halt) { super(&block) }
17
+ result = catch(:halt) { yield }
14
18
  end
15
19
  end
16
20
 
@@ -0,0 +1,50 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module InternalRequest
5
+ def post_configure
6
+ super
7
+ return unless internal_request?
8
+
9
+ self.class.define_singleton_method(:internal_request) do |route, opts = {}, &blk|
10
+ url_options = ::Rails.application.config.action_mailer.default_url_options || {}
11
+
12
+ scheme = url_options[:protocol]
13
+ port = url_options[:port]
14
+ port||= Rack::Request::DEFAULT_PORTS[scheme] if Rack.release < "2"
15
+ host = url_options[:host]
16
+ host_with_port = host && port ? "#{host}:#{port}" : host
17
+
18
+ env = {
19
+ "HTTP_HOST" => host_with_port,
20
+ "rack.url_scheme" => scheme,
21
+ "SERVER_NAME" => host,
22
+ "SERVER_PORT" => port,
23
+ }.compact
24
+
25
+ opts = opts.merge(env: env) { |k, v1, v2| v2.merge(v1) }
26
+
27
+ super(route, opts, &blk)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def rails_controller_around
34
+ return yield if internal_request?
35
+ super
36
+ end
37
+
38
+ def rails_instrument_request
39
+ return yield if internal_request?
40
+ super
41
+ end
42
+
43
+ def rails_instrument_redirection
44
+ return yield if internal_request?
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,101 @@
1
+ module Rodauth
2
+ module Rails
3
+ class Model < Module
4
+ require "rodauth/rails/model/associations"
5
+
6
+ def initialize(auth_class, association_options: {})
7
+ @auth_class = auth_class
8
+ @association_options = association_options
9
+
10
+ define_methods
11
+ end
12
+
13
+ def included(model)
14
+ fail Rodauth::Rails::Error, "must be an Active Record model" unless model < ActiveRecord::Base
15
+
16
+ define_associations(model)
17
+ end
18
+
19
+ private
20
+
21
+ def define_methods
22
+ rodauth = @auth_class.allocate.freeze
23
+
24
+ attr_reader :password
25
+
26
+ define_method(:password=) do |password|
27
+ @password = password
28
+ password_hash = rodauth.send(:password_hash, password) if password
29
+ set_password_hash(password_hash)
30
+ end
31
+
32
+ define_method(:set_password_hash) do |password_hash|
33
+ if rodauth.account_password_hash_column
34
+ public_send(:"#{rodauth.account_password_hash_column}=", password_hash)
35
+ else
36
+ if password_hash
37
+ record = self.password_hash || build_password_hash
38
+ record.public_send(:"#{rodauth.password_hash_column}=", password_hash)
39
+ else
40
+ self.password_hash&.mark_for_destruction
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def define_associations(model)
47
+ define_password_hash_association(model) unless rodauth.account_password_hash_column
48
+
49
+ feature_associations.each do |association|
50
+ define_association(model, **association)
51
+ end
52
+ end
53
+
54
+ def define_password_hash_association(model)
55
+ password_hash_id_column = rodauth.password_hash_id_column
56
+ scope = -> { select(password_hash_id_column) } if rodauth.send(:use_database_authentication_functions?)
57
+
58
+ define_association model,
59
+ type: :has_one,
60
+ name: :password_hash,
61
+ table: rodauth.password_hash_table,
62
+ foreign_key: password_hash_id_column,
63
+ scope: scope,
64
+ autosave: true
65
+ end
66
+
67
+ def define_association(model, type:, name:, table:, foreign_key:, scope: nil, **options)
68
+ associated_model = Class.new(model.superclass)
69
+ associated_model.table_name = table
70
+ associated_model.belongs_to :account,
71
+ class_name: model.name,
72
+ foreign_key: foreign_key,
73
+ inverse_of: name
74
+
75
+ model.const_set(name.to_s.singularize.camelize, associated_model)
76
+
77
+ model.public_send type, name, scope,
78
+ class_name: associated_model.name,
79
+ foreign_key: foreign_key,
80
+ dependent: :destroy,
81
+ inverse_of: :account,
82
+ **options,
83
+ **association_options(name)
84
+ end
85
+
86
+ def feature_associations
87
+ Rodauth::Rails::Model::Associations.call(rodauth)
88
+ end
89
+
90
+ def association_options(name)
91
+ options = @association_options
92
+ options = options.call(name) if options.respond_to?(:call)
93
+ options || {}
94
+ end
95
+
96
+ def rodauth
97
+ @auth_class.allocate
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,195 @@
1
+ module Rodauth
2
+ module Rails
3
+ class Model
4
+ class Associations
5
+ attr_reader :rodauth
6
+
7
+ def self.call(rodauth)
8
+ new(rodauth).call
9
+ end
10
+
11
+ def initialize(rodauth)
12
+ @rodauth = rodauth
13
+ end
14
+
15
+ def call
16
+ rodauth.features
17
+ .select { |feature| respond_to?(feature, true) }
18
+ .flat_map { |feature| send(feature) }
19
+ end
20
+
21
+ private
22
+
23
+ def remember
24
+ {
25
+ name: :remember_key,
26
+ type: :has_one,
27
+ table: rodauth.remember_table,
28
+ foreign_key: rodauth.remember_id_column,
29
+ }
30
+ end
31
+
32
+ def verify_account
33
+ {
34
+ name: :verification_key,
35
+ type: :has_one,
36
+ table: rodauth.verify_account_table,
37
+ foreign_key: rodauth.verify_account_id_column,
38
+ }
39
+ end
40
+
41
+ def reset_password
42
+ {
43
+ name: :password_reset_key,
44
+ type: :has_one,
45
+ table: rodauth.reset_password_table,
46
+ foreign_key: rodauth.reset_password_id_column,
47
+ }
48
+ end
49
+
50
+ def verify_login_change
51
+ {
52
+ name: :login_change_key,
53
+ type: :has_one,
54
+ table: rodauth.verify_login_change_table,
55
+ foreign_key: rodauth.verify_login_change_id_column,
56
+ }
57
+ end
58
+
59
+ def lockout
60
+ [
61
+ {
62
+ name: :lockout,
63
+ type: :has_one,
64
+ table: rodauth.account_lockouts_table,
65
+ foreign_key: rodauth.account_lockouts_id_column,
66
+ },
67
+ {
68
+ name: :login_failure,
69
+ type: :has_one,
70
+ table: rodauth.account_login_failures_table,
71
+ foreign_key: rodauth.account_login_failures_id_column,
72
+ }
73
+ ]
74
+ end
75
+
76
+ def email_auth
77
+ {
78
+ name: :email_auth_key,
79
+ type: :has_one,
80
+ table: rodauth.email_auth_table,
81
+ foreign_key: rodauth.email_auth_id_column,
82
+ }
83
+ end
84
+
85
+ def account_expiration
86
+ {
87
+ name: :activity_time,
88
+ type: :has_one,
89
+ table: rodauth.account_activity_table,
90
+ foreign_key: rodauth.account_activity_id_column,
91
+ }
92
+ end
93
+
94
+ def active_sessions
95
+ {
96
+ name: :active_session_keys,
97
+ type: :has_many,
98
+ table: rodauth.active_sessions_table,
99
+ foreign_key: rodauth.active_sessions_account_id_column,
100
+ }
101
+ end
102
+
103
+ def audit_logging
104
+ {
105
+ name: :authentication_audit_logs,
106
+ type: :has_many,
107
+ table: rodauth.audit_logging_table,
108
+ foreign_key: rodauth.audit_logging_account_id_column,
109
+ dependent: nil,
110
+ }
111
+ end
112
+
113
+ def disallow_password_reuse
114
+ {
115
+ name: :previous_password_hashes,
116
+ type: :has_many,
117
+ table: rodauth.previous_password_hash_table,
118
+ foreign_key: rodauth.previous_password_account_id_column,
119
+ }
120
+ end
121
+
122
+ def jwt_refresh
123
+ {
124
+ name: :jwt_refresh_keys,
125
+ type: :has_many,
126
+ table: rodauth.jwt_refresh_token_table,
127
+ foreign_key: rodauth.jwt_refresh_token_account_id_column,
128
+ }
129
+ end
130
+
131
+ def password_expiration
132
+ {
133
+ name: :password_change_time,
134
+ type: :has_one,
135
+ table: rodauth.password_expiration_table,
136
+ foreign_key: rodauth.password_expiration_id_column,
137
+ }
138
+ end
139
+
140
+ def single_session
141
+ {
142
+ name: :session_key,
143
+ type: :has_one,
144
+ table: rodauth.single_session_table,
145
+ foreign_key: rodauth.single_session_id_column,
146
+ }
147
+ end
148
+
149
+ def otp
150
+ {
151
+ name: :otp_key,
152
+ type: :has_one,
153
+ table: rodauth.otp_keys_table,
154
+ foreign_key: rodauth.otp_keys_id_column,
155
+ }
156
+ end
157
+
158
+ def sms_codes
159
+ {
160
+ name: :sms_code,
161
+ type: :has_one,
162
+ table: rodauth.sms_codes_table,
163
+ foreign_key: rodauth.sms_id_column,
164
+ }
165
+ end
166
+
167
+ def recovery_codes
168
+ {
169
+ name: :recovery_codes,
170
+ type: :has_many,
171
+ table: rodauth.recovery_codes_table,
172
+ foreign_key: rodauth.recovery_codes_id_column,
173
+ }
174
+ end
175
+
176
+ def webauthn
177
+ [
178
+ {
179
+ name: :webauthn_user_id,
180
+ type: :has_one,
181
+ table: rodauth.webauthn_user_ids_table,
182
+ foreign_key: rodauth.webauthn_user_ids_account_id_column,
183
+ },
184
+ {
185
+ name: :webauthn_keys,
186
+ type: :has_many,
187
+ table: rodauth.webauthn_keys_table,
188
+ foreign_key: rodauth.webauthn_keys_account_id_column,
189
+ }
190
+ ]
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -4,15 +4,15 @@ namespace :rodauth do
4
4
 
5
5
  puts "Routes handled by #{app}:"
6
6
 
7
- app.opts[:rodauths].each_key do |rodauth_name|
8
- rodauth = Rodauth::Rails.rodauth(rodauth_name)
7
+ app.opts[:rodauths].each do |configuration_name, auth_class|
8
+ auth_class.configure { enable :path_class_methods }
9
9
 
10
- routes = rodauth.class.routes.map do |handle_method|
10
+ routes = auth_class.routes.map do |handle_method|
11
11
  path_method = "#{handle_method.to_s.sub(/\Ahandle_/, "")}_path"
12
12
 
13
13
  [
14
- rodauth.public_send(path_method),
15
- "rodauth#{rodauth_name && "(:#{rodauth_name})"}.#{path_method}",
14
+ auth_class.public_send(path_method),
15
+ "rodauth#{configuration_name && "(:#{configuration_name})"}.#{path_method}",
16
16
  ]
17
17
  end
18
18
 
@@ -1,5 +1,5 @@
1
1
  module Rodauth
2
2
  module Rails
3
- VERSION = "0.14.0"
3
+ VERSION = "0.15.0"
4
4
  end
5
5
  end
@@ -17,10 +17,13 @@ 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.11"
20
+ spec.add_dependency "rodauth", "~> 2.15"
21
21
  spec.add_dependency "sequel-activerecord_connection", "~> 1.1"
22
22
  spec.add_dependency "tilt"
23
23
  spec.add_dependency "bcrypt"
24
24
 
25
25
  spec.add_development_dependency "jwt"
26
+ spec.add_development_dependency "rotp"
27
+ spec.add_development_dependency "rqrcode"
28
+ spec.add_development_dependency "webauthn" unless RUBY_ENGINE == "jruby"
26
29
  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.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-10 00:00:00.000000000 Z
11
+ date: 2021-07-29 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.11'
39
+ version: '2.15'
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.11'
46
+ version: '2.15'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sequel-activerecord_connection
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -100,6 +100,48 @@ dependencies:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
102
  version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rotp
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rqrcode
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: webauthn
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
103
145
  description: Provides Rails integration for Rodauth.
104
146
  email:
105
147
  - janko.marohnic@gmail.com
@@ -212,8 +254,11 @@ files:
212
254
  - lib/rodauth/rails/feature/csrf.rb
213
255
  - lib/rodauth/rails/feature/email.rb
214
256
  - lib/rodauth/rails/feature/instrumentation.rb
257
+ - lib/rodauth/rails/feature/internal_request.rb
215
258
  - lib/rodauth/rails/feature/render.rb
216
259
  - lib/rodauth/rails/middleware.rb
260
+ - lib/rodauth/rails/model.rb
261
+ - lib/rodauth/rails/model/associations.rb
217
262
  - lib/rodauth/rails/railtie.rb
218
263
  - lib/rodauth/rails/tasks.rake
219
264
  - lib/rodauth/rails/version.rb