rodauth-rails 0.14.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ module Rodauth
4
4
  module Base
5
5
  def self.included(feature)
6
6
  feature.auth_methods :rails_controller
7
+ feature.auth_value_methods :rails_account_model
7
8
  feature.auth_cached_method :rails_controller_instance
8
9
  end
9
10
 
@@ -30,6 +31,14 @@ module Rodauth
30
31
  end
31
32
  end
32
33
 
34
+ def rails_account_model
35
+ table = accounts_table
36
+ table = table.column if table.is_a?(Sequel::SQL::QualifiedIdentifier) # schema is specified
37
+ table.to_s.classify.constantize
38
+ rescue NameError
39
+ raise Error, "cannot infer account model, please set `rails_account_model` in your rodauth configuration"
40
+ end
41
+
33
42
  delegate :rails_routes, :rails_request, to: :scope
34
43
 
35
44
  private
@@ -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
 
@@ -13,23 +13,23 @@ module Rodauth
13
13
 
14
14
  # Render Rails CSRF tags in Rodauth templates.
15
15
  def csrf_tag(*)
16
- rails_csrf_tag
16
+ rails_csrf_tag if rails_controller_csrf?
17
17
  end
18
18
 
19
19
  # Verify Rails' authenticity token.
20
20
  def check_csrf
21
- rails_check_csrf!
21
+ rails_check_csrf! if rails_controller_csrf?
22
22
  end
23
23
 
24
24
  # Have Rodauth call #check_csrf automatically.
25
25
  def check_csrf?
26
- true
26
+ rails_check_csrf? if rails_controller_csrf?
27
27
  end
28
28
 
29
29
  private
30
30
 
31
31
  def rails_controller_callbacks
32
- return super if rails_api_controller?
32
+ return super unless rails_controller_csrf?
33
33
 
34
34
  # don't verify CSRF token as part of callbacks, Rodauth will do that
35
35
  rails_controller_instance.allow_forgery_protection = false
@@ -40,6 +40,12 @@ module Rodauth
40
40
  end
41
41
  end
42
42
 
43
+ # Checks whether ActionController::RequestForgeryProtection is included
44
+ # and that protect_from_forgery was called.
45
+ def rails_check_csrf?
46
+ !!rails_controller_instance.forgery_protection_strategy
47
+ end
48
+
43
49
  # Calls the controller to verify the authenticity token.
44
50
  def rails_check_csrf!
45
51
  rails_controller_instance.send(:verify_authenticity_token)
@@ -59,6 +65,11 @@ module Rodauth
59
65
  def rails_csrf_token
60
66
  rails_controller_instance.send(:form_authenticity_token)
61
67
  end
68
+
69
+ # Checks whether ActionController::RequestForgeryProtection is included.
70
+ def rails_controller_csrf?
71
+ rails_controller.respond_to?(:protect_from_forgery)
72
+ end
62
73
  end
63
74
  end
64
75
  end
@@ -0,0 +1,46 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module InternalRequest
5
+ def domain
6
+ return super unless missing_host?
7
+
8
+ Rodauth::Rails.url_options[:host]
9
+ end
10
+
11
+ def base_url
12
+ return super unless missing_host? && domain
13
+
14
+ url_options = Rodauth::Rails.url_options
15
+
16
+ url = "#{url_options[:protocol]}://#{domain}"
17
+ url << ":#{url_options[:port]}" if url_options[:port]
18
+ url
19
+ end
20
+
21
+ private
22
+
23
+ def rails_controller_around
24
+ return yield if internal_request?
25
+ super
26
+ end
27
+
28
+ def rails_instrument_request
29
+ return yield if internal_request?
30
+ super
31
+ end
32
+
33
+ def rails_instrument_redirection
34
+ return yield if internal_request?
35
+ super
36
+ end
37
+
38
+ # Checks whether we're in an internal request and host was not set,
39
+ # or the request doesn't exist such as with path_class_methods feature.
40
+ def missing_host?
41
+ internal_request? && request.host == INVALID_DOMAIN || scope.nil?
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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
@@ -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
@@ -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: type == :has_many ? :delete_all : :delete,
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
@@ -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.17.1"
4
4
  end
5
5
  end
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,47 +9,45 @@ 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
53
41
  end
54
42
 
55
- instance
43
+ auth_class.internal_request_eval(options) do
44
+ @account = account.attributes.symbolize_keys if account
45
+ self
46
+ end
47
+ end
48
+
49
+ def model(name = nil, **options)
50
+ Rodauth::Rails::Model.new(app.rodauth(name), **options)
56
51
  end
57
52
 
58
53
  # routing constraint that requires authentication
@@ -84,6 +79,12 @@ module Rodauth
84
79
  end
85
80
  end
86
81
 
82
+ def url_options
83
+ options = ::Rails.application.config.action_mailer.default_url_options || {}
84
+ options[:protocol] ||= "http"
85
+ options
86
+ end
87
+
87
88
  def configure
88
89
  yield self
89
90
  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.17.1
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-10-20 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