rodauth-rails 0.4.2 → 0.8.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 +39 -1
- data/README.md +419 -83
- data/lib/generators/rodauth/install_generator.rb +38 -23
- data/lib/generators/rodauth/migration/account_expiration.erb +7 -0
- data/lib/generators/rodauth/migration/active_sessions.erb +7 -0
- data/lib/generators/rodauth/migration/audit_logging.erb +16 -0
- data/lib/generators/rodauth/migration/base.erb +19 -0
- data/lib/generators/rodauth/migration/disallow_password_reuse.erb +5 -0
- data/lib/generators/rodauth/migration/email_auth.erb +7 -0
- data/lib/generators/rodauth/migration/jwt_refresh.erb +7 -0
- data/lib/generators/rodauth/migration/lockout.erb +11 -0
- data/lib/generators/rodauth/migration/otp.erb +7 -0
- data/lib/generators/rodauth/migration/password_expiration.erb +5 -0
- data/lib/generators/rodauth/migration/recovery_codes.erb +6 -0
- data/lib/generators/rodauth/migration/remember.erb +6 -0
- data/lib/generators/rodauth/migration/reset_password.erb +7 -0
- data/lib/generators/rodauth/migration/single_session.erb +5 -0
- data/lib/generators/rodauth/migration/sms_codes.erb +8 -0
- data/lib/generators/rodauth/migration/verify_account.erb +7 -0
- data/lib/generators/rodauth/migration/verify_login_change.erb +7 -0
- data/lib/generators/rodauth/migration/webauthn.erb +12 -0
- data/lib/generators/rodauth/migration_generator.rb +32 -0
- data/lib/generators/rodauth/migration_helpers.rb +69 -0
- data/lib/generators/rodauth/templates/app/controllers/rodauth_controller.rb +2 -1
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +18 -20
- data/lib/generators/rodauth/templates/config/initializers/sequel.rb +1 -5
- data/lib/generators/rodauth/templates/db/migrate/create_rodauth.rb +2 -176
- data/lib/rodauth/rails.rb +33 -4
- data/lib/rodauth/rails/app.rb +6 -2
- data/lib/rodauth/rails/app/flash.rb +1 -1
- data/lib/rodauth/rails/app/middleware.rb +26 -0
- data/lib/rodauth/rails/feature.rb +100 -30
- data/lib/rodauth/rails/railtie.rb +6 -0
- data/lib/rodauth/rails/tasks.rake +28 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +1 -1
- metadata +26 -5
- data/lib/rodauth/features/rails.rb +0 -1
@@ -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
|
-
|
5
|
-
DB = Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection)
|
6
|
-
<% else -%>
|
7
|
-
DB = Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection)
|
8
|
-
<% end -%>
|
4
|
+
DB = Sequel.connect("<%= sequel_uri_scheme %>://", extensions: :activerecord_connection)
|
@@ -1,179 +1,5 @@
|
|
1
|
-
class
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def change
|
3
|
-
|
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
|
data/lib/rodauth/rails.rb
CHANGED
@@ -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
|
|
data/lib/rodauth/rails/app.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
require "roda"
|
2
|
+
require "rodauth"
|
3
|
+
require "rodauth/rails/feature"
|
2
4
|
|
3
5
|
module Rodauth
|
4
6
|
module Rails
|
5
7
|
# The superclass for creating a Rodauth middleware.
|
6
8
|
class App < Roda
|
7
|
-
|
9
|
+
require "rodauth/rails/app/middleware"
|
10
|
+
plugin Middleware
|
11
|
+
|
8
12
|
plugin :hooks
|
9
13
|
plugin :render, layout: false
|
10
14
|
|
@@ -25,7 +29,7 @@ module Rodauth
|
|
25
29
|
set_deadline_values? true
|
26
30
|
|
27
31
|
# use HMACs for additional security
|
28
|
-
hmac_secret { ::Rails.
|
32
|
+
hmac_secret { Rodauth::Rails.secret_key_base }
|
29
33
|
|
30
34
|
# evaluate user configuration
|
31
35
|
instance_exec(&block)
|
@@ -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,73 @@ 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
|
+
elsif result
|
72
|
+
result[1].merge!(rails_controller_instance.response.headers)
|
73
|
+
throw :halt, result
|
74
|
+
else
|
75
|
+
result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Runs any #(before|around|after)_action controller callbacks.
|
80
|
+
def rails_controller_callbacks
|
81
|
+
# don't verify CSRF token as part of callbacks, Rodauth will do that
|
82
|
+
rails_controller_instance.allow_forgery_protection = false
|
83
|
+
|
84
|
+
rails_controller_instance.run_callbacks(:process_action) do
|
85
|
+
# turn the setting back to default so that form tags generate CSRF tags
|
86
|
+
rails_controller_instance.allow_forgery_protection = rails_controller.allow_forgery_protection
|
87
|
+
|
88
|
+
yield
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Runs any registered #rescue_from controller handlers.
|
93
|
+
def rails_controller_rescue
|
94
|
+
yield
|
95
|
+
rescue Exception => exception
|
96
|
+
rails_controller_instance.rescue_with_handler(exception) || raise
|
97
|
+
|
98
|
+
unless rails_controller_instance.performed?
|
99
|
+
raise Rodauth::Rails::Error, "rescue_from handler didn't write any response"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns Roda response from controller response if set.
|
104
|
+
def rails_controller_response
|
105
|
+
controller_response = rails_controller_instance.response
|
106
|
+
|
107
|
+
response.status = controller_response.status
|
108
|
+
response.headers.merge! controller_response.headers
|
109
|
+
response.write controller_response.body
|
110
|
+
|
111
|
+
request.halt
|
112
|
+
end
|
113
|
+
|
53
114
|
# Create emails with ActionMailer which uses configured delivery method.
|
54
115
|
def create_email_to(to, subject, body)
|
55
116
|
Mailer.create_email(to: to, from: email_from, subject: "#{email_subject_prefix}#{subject}", body: body)
|
@@ -64,11 +125,14 @@ module Rodauth
|
|
64
125
|
def rails_render(*args)
|
65
126
|
return if only_json?
|
66
127
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
128
|
+
rails_controller_instance.render_to_string(*args)
|
129
|
+
rescue ActionView::MissingTemplate
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# Calls the controller to verify the authenticity token.
|
134
|
+
def rails_check_csrf!
|
135
|
+
rails_controller_instance.send(:verify_authenticity_token)
|
72
136
|
end
|
73
137
|
|
74
138
|
# Hidden tag with Rails CSRF token inserted into Rodauth templates.
|
@@ -86,30 +150,36 @@ module Rodauth
|
|
86
150
|
rails_controller_instance.send(:form_authenticity_token)
|
87
151
|
end
|
88
152
|
|
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
153
|
# Instances of the configured controller with current request's env hash.
|
95
|
-
def
|
96
|
-
|
97
|
-
|
154
|
+
def _rails_controller_instance
|
155
|
+
controller = rails_controller.new
|
156
|
+
rails_request = ActionDispatch::Request.new(scope.env)
|
98
157
|
|
99
|
-
|
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
|
158
|
+
prepare_rails_controller(controller, rails_request)
|
106
159
|
|
107
|
-
|
160
|
+
controller
|
108
161
|
end
|
109
162
|
|
110
|
-
|
111
|
-
|
112
|
-
|
163
|
+
if ActionPack.version >= Gem::Version.new("5.0")
|
164
|
+
# Controller class to use for view rendering, CSRF protection, and
|
165
|
+
# running any registered action callbacks and rescue_from handlers.
|
166
|
+
def rails_controller
|
167
|
+
only_json? ? ActionController::API : ActionController::Base
|
168
|
+
end
|
169
|
+
|
170
|
+
def prepare_rails_controller(controller, rails_request)
|
171
|
+
controller.set_request! rails_request
|
172
|
+
controller.set_response! rails_controller.make_response!(rails_request)
|
173
|
+
end
|
174
|
+
else
|
175
|
+
def rails_controller
|
176
|
+
ActionController::Base
|
177
|
+
end
|
178
|
+
|
179
|
+
def prepare_rails_controller(controller, rails_request)
|
180
|
+
controller.send(:set_response!, rails_request)
|
181
|
+
controller.instance_variable_set(:@_request, rails_request)
|
182
|
+
end
|
113
183
|
end
|
114
184
|
|
115
185
|
# ActionMailer subclass for correct email delivering.
|