rodauth-rails 0.9.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +417 -250
  4. data/lib/generators/rodauth/install_generator.rb +17 -0
  5. data/lib/generators/rodauth/migration/base.erb +2 -2
  6. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
  7. data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
  8. data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
  9. data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
  10. data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
  11. data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
  12. data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
  13. data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
  14. data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
  15. data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
  16. data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
  17. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
  18. data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
  19. data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
  20. data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
  21. data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
  22. data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
  23. data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
  24. data/lib/rodauth/rails.rb +32 -4
  25. data/lib/rodauth/rails/app.rb +20 -22
  26. data/lib/rodauth/rails/app/flash.rb +2 -8
  27. data/lib/rodauth/rails/app/middleware.rb +20 -10
  28. data/lib/rodauth/rails/auth.rb +40 -0
  29. data/lib/rodauth/rails/controller_methods.rb +1 -5
  30. data/lib/rodauth/rails/feature.rb +17 -210
  31. data/lib/rodauth/rails/feature/base.rb +62 -0
  32. data/lib/rodauth/rails/feature/callbacks.rb +61 -0
  33. data/lib/rodauth/rails/feature/csrf.rb +65 -0
  34. data/lib/rodauth/rails/feature/email.rb +30 -0
  35. data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
  36. data/lib/rodauth/rails/feature/render.rb +41 -0
  37. data/lib/rodauth/rails/version.rb +1 -1
  38. data/rodauth-rails.gemspec +1 -1
  39. metadata +12 -6
  40. data/lib/generators/rodauth/mailer_generator.rb +0 -37
@@ -3,20 +3,30 @@ module Rodauth
3
3
  class App
4
4
  # Roda plugin that extends middleware plugin by propagating response headers.
5
5
  module Middleware
6
- def self.load_dependencies(app)
7
- app.plugin :hooks
8
- end
9
-
10
6
  def self.configure(app)
11
- app.after do
12
- if response.empty? && response.headers.any?
13
- env["rodauth.rails.headers"] = response.headers
7
+ handle_result = -> (env, res) do
8
+ if headers = env.delete("rodauth.rails.headers")
9
+ res[1] = headers.merge(res[1])
14
10
  end
15
11
  end
16
12
 
17
- app.plugin :middleware, handle_result: -> (env, res) do
18
- if headers = env.delete("rodauth.rails.headers")
19
- res[1] = headers.merge(res[1])
13
+ app.plugin :middleware, handle_result: handle_result do |middleware|
14
+ middleware.plugin :hooks
15
+
16
+ middleware.after do
17
+ if response.empty? && response.headers.any?
18
+ env["rodauth.rails.headers"] = response.headers
19
+ end
20
+ end
21
+
22
+ middleware.class_eval do
23
+ def self.inspect
24
+ "#{superclass}::Middleware"
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.inspect} request=#{request.inspect} response=#{response.inspect}>"
29
+ end
20
30
  end
21
31
  end
22
32
  end
@@ -0,0 +1,40 @@
1
+ require "rodauth"
2
+ require "rodauth/rails/feature"
3
+
4
+ module Rodauth
5
+ module Rails
6
+ # Base auth class that applies some default configuration and supports
7
+ # multi-level inheritance.
8
+ class Auth < Rodauth::Auth
9
+ class << self
10
+ attr_writer :features
11
+ attr_writer :routes
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.inherited(auth_class)
16
+ super
17
+ auth_class.roda_class = Rodauth::Rails.app
18
+ auth_class.features = features.dup
19
+ auth_class.routes = routes.dup
20
+ auth_class.route_hash = route_hash.dup
21
+ auth_class.configuration = configuration.clone
22
+ auth_class.configuration.instance_variable_set(:@auth, auth_class)
23
+ end
24
+
25
+ # apply default configuration
26
+ configure do
27
+ enable :rails
28
+
29
+ # database functions are more complex to set up, so disable them by default
30
+ use_database_authentication_functions? false
31
+
32
+ # avoid having to set deadline values in column default values
33
+ set_deadline_values? true
34
+
35
+ # use HMACs for additional security
36
+ hmac_secret { Rodauth::Rails.secret_key_base }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -9,11 +9,7 @@ module Rodauth
9
9
  end
10
10
 
11
11
  def rodauth(name = nil)
12
- if name
13
- request.env["rodauth.#{name}"]
14
- else
15
- request.env["rodauth"]
16
- end
12
+ request.env.fetch ["rodauth", *name].join(".")
17
13
  end
18
14
  end
19
15
  end
@@ -1,214 +1,21 @@
1
1
  module Rodauth
2
2
  Feature.define(:rails) do
3
- depends :email_base
4
-
5
- # List of overridable methods.
6
- auth_methods(
7
- :rails_render,
8
- :rails_csrf_tag,
9
- :rails_csrf_param,
10
- :rails_csrf_token,
11
- :rails_check_csrf!,
12
- :rails_controller,
13
- )
14
-
15
- auth_cached_method :rails_controller_instance
16
-
17
- # Renders templates with layout. First tries to render a user-defined
18
- # template, otherwise falls back to Rodauth's template.
19
- def view(page, *)
20
- rails_render(action: page.tr("-", "_"), layout: true) ||
21
- rails_render(html: super.html_safe, layout: true)
22
- end
23
-
24
- # Renders templates without layout. First tries to render a user-defined
25
- # template or partial, otherwise falls back to Rodauth's template.
26
- def render(page)
27
- rails_render(partial: page.tr("-", "_"), layout: false) ||
28
- rails_render(action: page.tr("-", "_"), layout: false) ||
29
- super.html_safe
30
- end
31
-
32
- # Render Rails CSRF tags in Rodauth templates.
33
- def csrf_tag(*)
34
- rails_csrf_tag
35
- end
36
-
37
- # Verify Rails' authenticity token.
38
- def check_csrf
39
- rails_check_csrf!
40
- end
41
-
42
- # Have Rodauth call #check_csrf automatically.
43
- def check_csrf?
44
- true
45
- end
46
-
47
- # Reset Rails session to protect from session fixation attacks.
48
- def clear_session
49
- rails_controller_instance.reset_session
50
- end
51
-
52
- # Default the flash error key to Rails' default :alert.
53
- def flash_error_key
54
- :alert
55
- end
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
-
66
- private
67
-
68
- # Runs controller callbacks and rescue handlers around Rodauth actions.
69
- def _around_rodauth(&block)
70
- result = nil
71
-
72
- rails_controller_rescue do
73
- rails_controller_callbacks do
74
- result = catch(:halt) { super(&block) }
75
- end
76
- end
77
-
78
- if rails_controller_instance.performed?
79
- rails_controller_response
80
- elsif result
81
- result[1].merge!(rails_controller_instance.response.headers)
82
- throw :halt, result
83
- else
84
- result
85
- end
86
- end
87
-
88
- # Runs any #(before|around|after)_action controller callbacks.
89
- def rails_controller_callbacks
90
- # don't verify CSRF token as part of callbacks, Rodauth will do that
91
- rails_controller_forgery_protection { false }
92
-
93
- rails_controller_instance.run_callbacks(:process_action) do
94
- # turn the setting back to default so that form tags generate CSRF tags
95
- rails_controller_forgery_protection { rails_controller.allow_forgery_protection }
96
-
97
- yield
98
- end
99
- end
100
-
101
- # Runs any registered #rescue_from controller handlers.
102
- def rails_controller_rescue
103
- yield
104
- rescue Exception => exception
105
- rails_controller_instance.rescue_with_handler(exception) || raise
106
-
107
- unless rails_controller_instance.performed?
108
- raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
109
- end
110
- end
111
-
112
- # Returns Roda response from controller response if set.
113
- def rails_controller_response
114
- controller_response = rails_controller_instance.response
115
-
116
- response.status = controller_response.status
117
- response.headers.merge! controller_response.headers
118
- response.write controller_response.body
119
-
120
- request.halt
121
- end
122
-
123
- # Create emails with ActionMailer which uses configured delivery method.
124
- def create_email_to(to, subject, body)
125
- Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
126
- end
127
-
128
- # Delivers the given email.
129
- def send_email(email)
130
- email.deliver_now
131
- end
132
-
133
- # Calls the Rails renderer, returning nil if a template is missing.
134
- def rails_render(*args)
135
- return if rails_api_controller?
136
-
137
- rails_controller_instance.render_to_string(*args)
138
- rescue ActionView::MissingTemplate
139
- nil
140
- end
141
-
142
- # Calls the controller to verify the authenticity token.
143
- def rails_check_csrf!
144
- rails_controller_instance.send(:verify_authenticity_token)
145
- end
146
-
147
- # Hidden tag with Rails CSRF token inserted into Rodauth templates.
148
- def rails_csrf_tag
149
- %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
150
- end
151
-
152
- # The request parameter under which to send the Rails CSRF token.
153
- def rails_csrf_param
154
- rails_controller.request_forgery_protection_token
155
- end
156
-
157
- # The Rails CSRF token value inserted into Rodauth templates.
158
- def rails_csrf_token
159
- rails_controller_instance.send(:form_authenticity_token)
160
- end
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
-
169
- # Instances of the configured controller with current request's env hash.
170
- def _rails_controller_instance
171
- controller = rails_controller.new
172
- rails_request = ActionDispatch::Request.new(scope.env)
173
-
174
- prepare_rails_controller(controller, rails_request)
175
-
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
189
- end
190
-
191
- def rails_api_controller?
192
- defined?(ActionController::API) && rails_controller <= ActionController::API
193
- end
194
-
195
- def rails_controller
196
- if only_json? && Rodauth::Rails.api_only?
197
- ActionController::API
198
- else
199
- ActionController::Base
200
- end
201
- end
202
-
203
- # ActionMailer subclass for correct email delivering.
204
- class Mailer < ActionMailer::Base
205
- def create_email(**options)
206
- mail(**options)
207
- end
208
- end
3
+ # Assign feature and feature configuration to constants for introspection.
4
+ Rodauth::Rails::Feature = self
5
+ Rodauth::Rails::FeatureConfiguration = self.configuration
6
+
7
+ require "rodauth/rails/feature/base"
8
+ require "rodauth/rails/feature/callbacks"
9
+ require "rodauth/rails/feature/csrf"
10
+ require "rodauth/rails/feature/render"
11
+ require "rodauth/rails/feature/email"
12
+ require "rodauth/rails/feature/instrumentation"
13
+
14
+ include Rodauth::Rails::Feature::Base
15
+ include Rodauth::Rails::Feature::Callbacks
16
+ include Rodauth::Rails::Feature::Csrf
17
+ include Rodauth::Rails::Feature::Render
18
+ include Rodauth::Rails::Feature::Email
19
+ include Rodauth::Rails::Feature::Instrumentation
209
20
  end
210
-
211
- # Assign feature and feature configuration to constants for introspection.
212
- Rails::Feature = FEATURES[:rails]
213
- Rails::FeatureConfiguration = FEATURES[:rails].configuration
214
21
  end
@@ -0,0 +1,62 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Base
5
+ def self.included(feature)
6
+ feature.auth_methods :rails_controller
7
+ feature.auth_cached_method :rails_controller_instance
8
+ end
9
+
10
+ # Reset Rails session to protect from session fixation attacks.
11
+ def clear_session
12
+ rails_controller_instance.reset_session
13
+ end
14
+
15
+ # Default the flash error key to Rails' default :alert.
16
+ def flash_error_key
17
+ :alert
18
+ end
19
+
20
+ # Evaluates the block in context of a Rodauth controller instance.
21
+ def rails_controller_eval(&block)
22
+ rails_controller_instance.instance_exec(&block)
23
+ end
24
+
25
+ delegate :rails_routes, :rails_request, to: :scope
26
+
27
+ private
28
+
29
+ # Instances of the configured controller with current request's env hash.
30
+ def _rails_controller_instance
31
+ controller = rails_controller.new
32
+ prepare_rails_controller(controller, rails_request)
33
+ controller
34
+ end
35
+
36
+ if ActionPack.version >= Gem::Version.new("5.0")
37
+ def prepare_rails_controller(controller, rails_request)
38
+ controller.set_request! rails_request
39
+ controller.set_response! rails_controller.make_response!(rails_request)
40
+ end
41
+ else
42
+ def prepare_rails_controller(controller, rails_request)
43
+ controller.send(:set_response!, rails_request)
44
+ controller.instance_variable_set(:@_request, rails_request)
45
+ end
46
+ end
47
+
48
+ def rails_api_controller?
49
+ defined?(ActionController::API) && rails_controller <= ActionController::API
50
+ end
51
+
52
+ def rails_controller
53
+ if only_json? && Rodauth::Rails.api_only?
54
+ ActionController::API
55
+ else
56
+ ActionController::Base
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ module Rodauth
2
+ module Rails
3
+ module Feature
4
+ module Callbacks
5
+ private
6
+
7
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
8
+ def _around_rodauth(&block)
9
+ result = nil
10
+
11
+ rails_controller_rescue do
12
+ rails_controller_callbacks do
13
+ result = catch(:halt) { super(&block) }
14
+ end
15
+ end
16
+
17
+ result = handle_rails_controller_response(result)
18
+
19
+ throw :halt, result if result
20
+ end
21
+
22
+ # Runs any #(before|around|after)_action controller callbacks.
23
+ def rails_controller_callbacks(&block)
24
+ rails_controller_instance.run_callbacks(:process_action, &block)
25
+ end
26
+
27
+ # Runs any registered #rescue_from controller handlers.
28
+ def rails_controller_rescue
29
+ yield
30
+ rescue Exception => exception
31
+ rails_controller_instance.rescue_with_handler(exception) || raise
32
+
33
+ unless rails_controller_instance.performed?
34
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
35
+ end
36
+ end
37
+
38
+ # Handles controller rendering a response or setting response headers.
39
+ def handle_rails_controller_response(result)
40
+ if rails_controller_instance.performed?
41
+ rails_controller_response
42
+ elsif result
43
+ result[1].merge!(rails_controller_instance.response.headers)
44
+ result
45
+ end
46
+ end
47
+
48
+ # Returns Roda response from controller response if set.
49
+ def rails_controller_response
50
+ controller_response = rails_controller_instance.response
51
+
52
+ response.status = controller_response.status
53
+ response.headers.merge! controller_response.headers
54
+ response.write controller_response.body
55
+
56
+ response.finish
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end