rodauth-rails 0.4.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +177 -77
  4. data/lib/generators/rodauth/install_generator.rb +28 -18
  5. data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
  6. data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
  7. data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
  8. data/lib/generators/rodauth/migration/base.erb +19 -0
  9. data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
  10. data/lib/generators/rodauth/migration/email_auth.erb +7 -0
  11. data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
  12. data/lib/generators/rodauth/migration/lockout.erb +11 -0
  13. data/lib/generators/rodauth/migration/otp.erb +7 -0
  14. data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
  15. data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
  16. data/lib/generators/rodauth/migration/remember.erb +6 -0
  17. data/lib/generators/rodauth/migration/reset_password.erb +7 -0
  18. data/lib/generators/rodauth/migration/single_session.erb +5 -0
  19. data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
  20. data/lib/generators/rodauth/migration/verify_account.erb +7 -0
  21. data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
  22. data/lib/generators/rodauth/migration/webauthn.erb +12 -0
  23. data/lib/generators/rodauth/migration_generator.rb +32 -0
  24. data/lib/generators/rodauth/migration_helpers.rb +69 -0
  25. data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
  26. data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -18
  27. data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
  28. data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
  29. data/lib/rodauth/rails.rb +23 -4
  30. data/lib/rodauth/rails/app.rb +3 -1
  31. data/lib/rodauth/rails/app/flash.rb +1 -1
  32. data/lib/rodauth/rails/app/middleware.rb +26 -0
  33. data/lib/rodauth/rails/feature.rb +92 -25
  34. data/lib/rodauth/rails/railtie.rb +11 -0
  35. data/lib/rodauth/rails/tasks.rake +28 -0
  36. data/lib/rodauth/rails/version.rb +1 -1
  37. data/rodauth-rails.gemspec +3 -3
  38. metadata +29 -7
@@ -1,179 +1,5 @@
1
- class CreateRodauth < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- <% if activerecord_adapter == "postgresql" -%>
4
- enable_extension "citext"
5
-
6
- <% end -%>
7
- create_table :accounts do |t|
8
- <% case activerecord_adapter -%>
9
- <% when "postgresql" -%>
10
- t.citext :email, null: false, index: { unique: true, where: "status IN ('verified', 'unverified')" }
11
- <% else -%>
12
- t.string :email, null: false, index: { unique: true }
13
- <% end -%>
14
- t.string :status, null: false, default: "verified"
15
- end
16
-
17
- # Used if storing password hashes in a separate table (default)
18
- create_table :account_password_hashes do |t|
19
- t.foreign_key :accounts, column: :id
20
- t.string :password_hash, null: false
21
- end
22
-
23
- # Used by the password reset feature
24
- create_table :account_password_reset_keys do |t|
25
- t.foreign_key :accounts, column: :id
26
- t.string :key, null: false
27
- t.datetime :deadline, null: false
28
- t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
29
- end
30
-
31
- # Used by the account verification feature
32
- create_table :account_verification_keys do |t|
33
- t.foreign_key :accounts, column: :id
34
- t.string :key, null: false
35
- t.datetime :requested_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
36
- t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
37
- end
38
-
39
- # Used by the verify login change feature
40
- create_table :account_login_change_keys do |t|
41
- t.foreign_key :accounts, column: :id
42
- t.string :key, null: false
43
- t.string :login, null: false
44
- t.datetime :deadline, null: false
45
- end
46
-
47
- <% unless api_only? -%>
48
- # Used by the remember me feature
49
- create_table :account_remember_keys do |t|
50
- t.foreign_key :accounts, column: :id
51
- t.string :key, null: false
52
- t.datetime :deadline, null: false
53
- end
54
- <% else -%>
55
- # # Used by the remember me feature
56
- # create_table :account_remember_keys do |t|
57
- # t.foreign_key :accounts, column: :id
58
- # t.string :key, null: false
59
- # t.datetime :deadline, null: false
60
- # end
61
- <% end -%>
62
-
63
- # # Used by the audit logging feature
64
- # create_table :account_authentication_audit_logs do |t|
65
- # t.references :account, foreign_key: true, null: false
66
- # t.datetime :at, null: false, default: -> { "CURRENT_TIMESTAMP" }
67
- # t.text :message, null: false
68
- <% case activerecord_adapter -%>
69
- <% when "postgresql" -%>
70
- # t.jsonb :metadata
71
- <% when "sqlite3", "mysql2" -%>
72
- # t.json :metadata
73
- <% else -%>
74
- # t.string :metadata
75
- <% end -%>
76
- # t.index [:account_id, :at], name: "audit_account_at_idx"
77
- # t.index :at, name: "audit_at_idx"
78
- # end
79
-
80
- # # Used by the jwt refresh feature
81
- # create_table :account_jwt_refresh_keys do |t|
82
- # t.references :account, foreign_key: true, null: false
83
- # t.string :key, null: false
84
- # t.datetime :deadline, null: false
85
- # t.index :account_id, name: "account_jwt_rk_account_id_idx"
86
- # end
87
-
88
- # # Used by the disallow_password_reuse feature
89
- # create_table :account_previous_password_hashes do |t|
90
- # t.references :account, foreign_key: true
91
- # t.string :password_hash, null: false
92
- # end
93
-
94
- # # Used by the lockout feature
95
- # create_table :account_login_failures do |t|
96
- # t.foreign_key :accounts, column: :id
97
- # t.integer :number, null: false, default: 1
98
- # end
99
- # create_table :account_lockouts do |t|
100
- # t.foreign_key :accounts, column: :id
101
- # t.string :key, null: false
102
- # t.datetime :deadline, null: false
103
- # t.datetime :email_last_sent
104
- # end
105
-
106
- # # Used by the email auth feature
107
- # create_table :account_email_auth_keys do |t|
108
- # t.foreign_key :accounts, column: :id
109
- # t.string :key, null: false
110
- # t.datetime :deadline, null: false
111
- # t.datetime :email_last_sent, null: false, default: -> { "CURRENT_TIMESTAMP" }
112
- # end
113
-
114
- # # Used by the password expiration feature
115
- # create_table :account_password_change_times do |t|
116
- # t.foreign_key :accounts, column: :id
117
- # t.datetime :changed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
118
- # end
119
-
120
- # # Used by the account expiration feature
121
- # create_table :account_activity_times do |t|
122
- # t.foreign_key :accounts, column: :id
123
- # t.datetime :last_activity_at, null: false
124
- # t.datetime :last_login_at, null: false
125
- # t.datetime :expired_at
126
- # end
127
-
128
- # # Used by the single session feature
129
- # create_table :account_session_keys do |t|
130
- # t.foreign_key :accounts, column: :id
131
- # t.string :key, null: false
132
- # end
133
-
134
- # # Used by the active sessions feature
135
- # create_table :account_active_session_keys, primary_key: [:account_id, :session_id] do |t|
136
- # t.references :account, foreign_key: true
137
- # t.string :session_id
138
- # t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
139
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
140
- # end
141
-
142
- # # Used by the webauthn feature
143
- # create_table :account_webauthn_user_ids do |t|
144
- # t.foreign_key :accounts, column: :id
145
- # t.string :webauthn_id, null: false
146
- # end
147
- # create_table :account_webauthn_keys, primary_key: [:account_id, :webauthn_id] do |t|
148
- # t.references :account, foreign_key: true
149
- # t.string :webauthn_id
150
- # t.string :public_key, null: false
151
- # t.integer :sign_count, null: false
152
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
153
- # end
154
-
155
- # # Used by the otp feature
156
- # create_table :account_otp_keys do |t|
157
- # t.foreign_key :accounts, column: :id
158
- # t.string :key, null: false
159
- # t.integer :num_failures, null: false, default: 0
160
- # t.datetime :last_use, null: false, default: -> { "CURRENT_TIMESTAMP" }
161
- # end
162
-
163
- # # Used by the recovery codes feature
164
- # create_table :account_recovery_codes, primary_key: [:id, :code] do |t|
165
- # t.integer :id
166
- # t.foreign_key :accounts, column: :id
167
- # t.string :code
168
- # end
169
-
170
- # # Used by the sms codes feature
171
- # create_table :account_sms_codes do |t|
172
- # t.foreign_key :accounts, column: :id
173
- # t.string :phone_number, null: false
174
- # t.integer :num_failures
175
- # t.string :code
176
- # t.datetime :code_issued_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
177
- # end
3
+ <%= migration_content -%>
178
4
  end
179
5
  end
@@ -9,14 +9,33 @@ module Rodauth
9
9
  # This allows the developer to avoid loading Rodauth at boot time.
10
10
  autoload :App, "rodauth/rails/app"
11
11
 
12
- def self.configure
13
- yield self
14
- end
15
-
16
12
  @app = nil
17
13
  @middleware = true
18
14
 
19
15
  class << self
16
+ def rodauth(name = nil)
17
+ url_options = ActionMailer::Base.default_url_options
18
+
19
+ scheme = url_options[:protocol] || "http"
20
+ port = url_options[:port]
21
+ port ||= Rack::Request::DEFAULT_PORTS[scheme] if Gem::Version.new(Rack.release) < Gem::Version.new("2.0")
22
+ host = url_options[:host]
23
+ host += ":#{port}" if port
24
+
25
+ rack_env = {
26
+ "HTTP_HOST" => host,
27
+ "rack.url_scheme" => scheme,
28
+ }
29
+
30
+ scope = app.new(rack_env)
31
+
32
+ scope.rodauth(name)
33
+ end
34
+
35
+ def configure
36
+ yield self
37
+ end
38
+
20
39
  attr_writer :app
21
40
  attr_writer :middleware
22
41
 
@@ -4,7 +4,9 @@ module Rodauth
4
4
  module Rails
5
5
  # The superclass for creating a Rodauth middleware.
6
6
  class App < Roda
7
- plugin :middleware
7
+ require "rodauth/rails/app/middleware"
8
+ plugin Middleware
9
+
8
10
  plugin :hooks
9
11
  plugin :render, layout: false
10
12
 
@@ -1,7 +1,7 @@
1
1
  module Rodauth
2
2
  module Rails
3
3
  class App
4
- # Sets up Rails' flash integration.
4
+ # Roda plugin that sets up Rails flash integration.
5
5
  module Flash
6
6
  def self.load_dependencies(app)
7
7
  app.plugin :hooks
@@ -0,0 +1,26 @@
1
+ module Rodauth
2
+ module Rails
3
+ class App
4
+ # Roda plugin that extends middleware plugin by propagating response headers.
5
+ module Middleware
6
+ def self.load_dependencies(app)
7
+ app.plugin :hooks
8
+ end
9
+
10
+ def self.configure(app)
11
+ app.after do
12
+ if response.empty? && response.headers.any?
13
+ env["rodauth.rails.headers"] = response.headers
14
+ end
15
+ end
16
+
17
+ app.plugin :middleware, handle_result: -> (env, res) do
18
+ if headers = env.delete("rodauth.rails.headers")
19
+ res[1] = headers.merge(res[1])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -9,10 +9,11 @@ module Rodauth
9
9
  :rails_csrf_param,
10
10
  :rails_csrf_token,
11
11
  :rails_check_csrf!,
12
- :rails_controller_instance,
13
12
  :rails_controller,
14
13
  )
15
14
 
15
+ auth_cached_method :rails_controller_instance
16
+
16
17
  # Renders templates with layout. First tries to render a user-defined
17
18
  # template, otherwise falls back to Rodauth's template.
18
19
  def view(page, *)
@@ -28,6 +29,11 @@ module Rodauth
28
29
  super
29
30
  end
30
31
 
32
+ # Render Rails CSRF tags in Rodauth templates.
33
+ def csrf_tag(*)
34
+ rails_csrf_tag
35
+ end
36
+
31
37
  # Verify Rails' authenticity token.
32
38
  def check_csrf
33
39
  rails_check_csrf!
@@ -38,11 +44,6 @@ module Rodauth
38
44
  true
39
45
  end
40
46
 
41
- # Render Rails CSRF tags in Rodauth templates.
42
- def csrf_tag(*)
43
- rails_csrf_tag
44
- end
45
-
46
47
  # Default the flash error key to Rails' default :alert.
47
48
  def flash_error_key
48
49
  :alert
@@ -50,6 +51,59 @@ module Rodauth
50
51
 
51
52
  private
52
53
 
54
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
55
+ def _around_rodauth(&block)
56
+ result = nil
57
+
58
+ rails_controller_rescue do
59
+ rails_controller_callbacks do
60
+ result = catch(:halt) { super(&block) }
61
+ end
62
+ end
63
+
64
+ if rails_controller_instance.performed?
65
+ rails_controller_response
66
+ else
67
+ result[1].merge!(rails_controller_instance.response.headers)
68
+ throw :halt, result
69
+ end
70
+ end
71
+
72
+ # Runs any #(before|around|after)_action controller callbacks.
73
+ def rails_controller_callbacks
74
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
75
+ rails_controller_instance.allow_forgery_protection = false
76
+
77
+ rails_controller_instance.run_callbacks(:process_action) do
78
+ # turn the setting back to default so that form tags generate CSRF tags
79
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
80
+
81
+ yield
82
+ end
83
+ end
84
+
85
+ # Runs any registered #rescue_from controller handlers.
86
+ def rails_controller_rescue
87
+ yield
88
+ rescue Exception => exception
89
+ rails_controller_instance.rescue_with_handler(exception) || raise
90
+
91
+ unless rails_controller_instance.performed?
92
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
93
+ end
94
+ end
95
+
96
+ # Returns Roda response from controller response if set.
97
+ def rails_controller_response
98
+ controller_response = rails_controller_instance.response
99
+
100
+ response.status = controller_response.status
101
+ response.headers.merge! controller_response.headers
102
+ response.write controller_response.body
103
+
104
+ request.halt
105
+ end
106
+
53
107
  # Create emails with ActionMailer which uses configured delivery method.
54
108
  def create_email_to(to, subject, body)
55
109
  Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
@@ -62,11 +116,18 @@ module Rodauth
62
116
 
63
117
  # Calls the Rails renderer, returning nil if a template is missing.
64
118
  def rails_render(*args)
119
+ return if only_json?
120
+
65
121
  rails_controller_instance.render_to_string(*args)
66
122
  rescue ActionView::MissingTemplate
67
123
  nil
68
124
  end
69
125
 
126
+ # Calls the controller to verify the authenticity token.
127
+ def rails_check_csrf!
128
+ rails_controller_instance.send(:verify_authenticity_token)
129
+ end
130
+
70
131
  # Hidden tag with Rails CSRF token inserted into Rodauth templates.
71
132
  def rails_csrf_tag
72
133
  %(<input type="hidden" name="#{rails_csrf_param}" value="#{rails_csrf_token}">)
@@ -82,30 +143,36 @@ module Rodauth
82
143
  rails_controller_instance.send(:form_authenticity_token)
83
144
  end
84
145
 
85
- # Calls the controller to verify the authenticity token.
86
- def rails_check_csrf!
87
- rails_controller_instance.send(:verify_authenticity_token)
88
- end
89
-
90
146
  # Instances of the configured controller with current request's env hash.
91
- def rails_controller_instance
92
- request = ActionDispatch::Request.new(scope.env)
93
- instance = rails_controller.new
147
+ def _rails_controller_instance
148
+ controller = rails_controller.new
149
+ rails_request = ActionDispatch::Request.new(scope.env)
94
150
 
95
- if ActionPack.version >= Gem::Version.new("5.0")
96
- instance.set_request! request
97
- instance.set_response! rails_controller.make_response!(request)
98
- else
99
- instance.send(:set_response!, request)
100
- instance.instance_variable_set(:@_request, request)
101
- end
151
+ prepare_rails_controller(controller, rails_request)
102
152
 
103
- instance
153
+ controller
104
154
  end
105
155
 
106
- # Controller class to use for rendering and CSRF protection.
107
- def rails_controller
108
- ActionController::Base
156
+ if ActionPack.version >= Gem::Version.new("5.0")
157
+ # Controller class to use for view rendering, CSRF protection, and
158
+ # running any registered action callbacks and rescue_from handlers.
159
+ def rails_controller
160
+ only_json? ? ActionController::API : ActionController::Base
161
+ end
162
+
163
+ def prepare_rails_controller(controller, rails_request)
164
+ controller.set_request! rails_request
165
+ controller.set_response! rails_controller.make_response!(rails_request)
166
+ end
167
+ else
168
+ def rails_controller
169
+ ActionController::Base
170
+ end
171
+
172
+ def prepare_rails_controller(controller, rails_request)
173
+ controller.send(:set_response!, rails_request)
174
+ controller.instance_variable_set(:@_request, rails_request)
175
+ end
109
176
  end
110
177
 
111
178
  # ActionMailer subclass for correct email delivering.
@@ -1,6 +1,8 @@
1
1
  require "rodauth/rails/middleware"
2
2
  require "rodauth/rails/controller_methods"
3
3
 
4
+ require "rails"
5
+
4
6
  module Rodauth
5
7
  module Rails
6
8
  class Railtie < ::Rails::Railtie
@@ -13,6 +15,15 @@ module Rodauth
13
15
  include Rodauth::Rails::ControllerMethods
14
16
  end
15
17
  end
18
+
19
+ initializer "rodauth.test" do
20
+ # Rodauth uses RACK_ENV to set the default bcrypt hash cost
21
+ ENV["RACK_ENV"] = "test" if ::Rails.env.test?
22
+ end
23
+
24
+ rake_tasks do
25
+ load "rodauth/rails/tasks.rake"
26
+ end
16
27
  end
17
28
  end
18
29
  end