rodauth-rails 0.9.0 → 0.13.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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +417 -250
- data/lib/generators/rodauth/install_generator.rb +17 -0
- data/lib/generators/rodauth/migration/base.erb +2 -2
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
- data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
- data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
- data/lib/rodauth/rails.rb +32 -4
- data/lib/rodauth/rails/app.rb +20 -22
- data/lib/rodauth/rails/app/flash.rb +2 -8
- data/lib/rodauth/rails/app/middleware.rb +20 -10
- data/lib/rodauth/rails/auth.rb +40 -0
- data/lib/rodauth/rails/controller_methods.rb +1 -5
- data/lib/rodauth/rails/feature.rb +17 -210
- data/lib/rodauth/rails/feature/base.rb +62 -0
- data/lib/rodauth/rails/feature/callbacks.rb +61 -0
- data/lib/rodauth/rails/feature/csrf.rb +65 -0
- data/lib/rodauth/rails/feature/email.rb +30 -0
- data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
- data/lib/rodauth/rails/feature/render.rb +41 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +1 -1
- metadata +12 -6
- 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
|
-
|
12
|
-
if
|
13
|
-
|
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:
|
18
|
-
|
19
|
-
|
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
|
@@ -1,214 +1,21 @@
|
|
1
1
|
module Rodauth
|
2
2
|
Feature.define(:rails) do
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|