rodauth-rails 0.14.0 → 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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