rodauth-rails 0.4.1 → 0.7.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +187 -52
  4. data/lib/generators/rodauth/install_generator.rb +28 -20
  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 -20
  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 +33 -4
  30. data/lib/rodauth/rails/app.rb +4 -2
  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 +98 -30
  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,8 +1,4 @@
1
1
  require "sequel/core"
2
2
 
3
3
  # initialize Sequel and have it reuse Active Record's database connection
4
- <% if RUBY_ENGINE == "jruby" -%>
5
- DB = Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection, test: false)
6
- <% else -%>
7
- DB = Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection, test: false)
8
- <% end -%>
4
+ DB = Sequel.connect("<%= sequel_uri_scheme %>://", extensions: :activerecord_connection)
@@ -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,43 @@ 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
+ if ::Rails.gem_version >= Gem::Version.new("5.2")
36
+ def secret_key_base
37
+ ::Rails.application.secret_key_base
38
+ end
39
+ else
40
+ def secret_key_base
41
+ ::Rails.application.secrets.secret_key_base
42
+ end
43
+ end
44
+
45
+ def configure
46
+ yield self
47
+ end
48
+
20
49
  attr_writer :app
21
50
  attr_writer :middleware
22
51
 
@@ -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
 
@@ -25,7 +27,7 @@ module Rodauth
25
27
  set_deadline_values? true
26
28
 
27
29
  # use HMACs for additional security
28
- hmac_secret { ::Rails.application.secrets.secret_key_base }
30
+ hmac_secret { Rodauth::Rails.secret_key_base }
29
31
 
30
32
  # evaluate user configuration
31
33
  instance_exec(&block)
@@ -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,18 +44,71 @@ 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
49
50
  end
50
51
 
52
+ # Evaluates the block in context of a Rodauth controller instance.
53
+ def rails_controller_eval(&block)
54
+ rails_controller_instance.instance_exec(&block)
55
+ end
56
+
51
57
  private
52
58
 
59
+ # Runs controller callbacks and rescue handlers around Rodauth actions.
60
+ def _around_rodauth(&block)
61
+ result = nil
62
+
63
+ rails_controller_rescue do
64
+ rails_controller_callbacks do
65
+ result = catch(:halt) { super(&block) }
66
+ end
67
+ end
68
+
69
+ if rails_controller_instance.performed?
70
+ rails_controller_response
71
+ else
72
+ result[1].merge!(rails_controller_instance.response.headers)
73
+ throw :halt, result
74
+ end
75
+ end
76
+
77
+ # Runs any #(before|around|after)_action controller callbacks.
78
+ def rails_controller_callbacks
79
+ # don't verify CSRF token as part of callbacks, Rodauth will do that
80
+ rails_controller_instance.allow_forgery_protection = false
81
+
82
+ rails_controller_instance.run_callbacks(:process_action) do
83
+ # turn the setting back to default so that form tags generate CSRF tags
84
+ rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
85
+
86
+ yield
87
+ end
88
+ end
89
+
90
+ # Runs any registered #rescue_from controller handlers.
91
+ def rails_controller_rescue
92
+ yield
93
+ rescue Exception => exception
94
+ rails_controller_instance.rescue_with_handler(exception) || raise
95
+
96
+ unless rails_controller_instance.performed?
97
+ raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
98
+ end
99
+ end
100
+
101
+ # Returns Roda response from controller response if set.
102
+ def rails_controller_response
103
+ controller_response = rails_controller_instance.response
104
+
105
+ response.status = controller_response.status
106
+ response.headers.merge! controller_response.headers
107
+ response.write controller_response.body
108
+
109
+ request.halt
110
+ end
111
+
53
112
  # Create emails with ActionMailer which uses configured delivery method.
54
113
  def create_email_to(to, subject, body)
55
114
  Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
@@ -64,11 +123,14 @@ module Rodauth
64
123
  def rails_render(*args)
65
124
  return if only_json?
66
125
 
67
- begin
68
- rails_controller_instance.render_to_string(*args)
69
- rescue ActionView::MissingTemplate
70
- nil
71
- end
126
+ rails_controller_instance.render_to_string(*args)
127
+ rescue ActionView::MissingTemplate
128
+ nil
129
+ end
130
+
131
+ # Calls the controller to verify the authenticity token.
132
+ def rails_check_csrf!
133
+ rails_controller_instance.send(:verify_authenticity_token)
72
134
  end
73
135
 
74
136
  # Hidden tag with Rails CSRF token inserted into Rodauth templates.
@@ -86,30 +148,36 @@ module Rodauth
86
148
  rails_controller_instance.send(:form_authenticity_token)
87
149
  end
88
150
 
89
- # Calls the controller to verify the authenticity token.
90
- def rails_check_csrf!
91
- rails_controller_instance.send(:verify_authenticity_token)
92
- end
93
-
94
151
  # Instances of the configured controller with current request's env hash.
95
- def rails_controller_instance
96
- request = ActionDispatch::Request.new(scope.env)
97
- instance = rails_controller.new
152
+ def _rails_controller_instance
153
+ controller = rails_controller.new
154
+ rails_request = ActionDispatch::Request.new(scope.env)
98
155
 
99
- if ActionPack.version >= Gem::Version.new("5.0")
100
- instance.set_request! request
101
- instance.set_response! rails_controller.make_response!(request)
102
- else
103
- instance.send(:set_response!, request)
104
- instance.instance_variable_set(:@_request, request)
105
- end
156
+ prepare_rails_controller(controller, rails_request)
106
157
 
107
- instance
158
+ controller
108
159
  end
109
160
 
110
- # Controller class to use for rendering and CSRF protection.
111
- def rails_controller
112
- ActionController::Base
161
+ if ActionPack.version >= Gem::Version.new("5.0")
162
+ # Controller class to use for view rendering, CSRF protection, and
163
+ # running any registered action callbacks and rescue_from handlers.
164
+ def rails_controller
165
+ only_json? ? ActionController::API : ActionController::Base
166
+ end
167
+
168
+ def prepare_rails_controller(controller, rails_request)
169
+ controller.set_request! rails_request
170
+ controller.set_response! rails_controller.make_response!(rails_request)
171
+ end
172
+ else
173
+ def rails_controller
174
+ ActionController::Base
175
+ end
176
+
177
+ def prepare_rails_controller(controller, rails_request)
178
+ controller.send(:set_response!, rails_request)
179
+ controller.instance_variable_set(:@_request, rails_request)
180
+ end
113
181
  end
114
182
 
115
183
  # ActionMailer subclass for correct email delivering.