add_auth 0.3.0 → 0.5.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 +27 -0
- data/README.md +61 -9
- data/ROADMAP.md +43 -31
- data/SECURITY.md +7 -1
- data/app/controllers/add_auth/accounts_controller.rb +10 -2
- data/app/controllers/add_auth/reauthentications_controller.rb +5 -0
- data/app/controllers/add_auth/sessions_controller.rb +1 -1
- data/app/views/add_auth/accounts/_register.html.erb +1 -1
- data/app/views/add_auth/accounts/_request.html.erb +3 -0
- data/lib/add_auth/configuration.rb +2 -2
- data/lib/add_auth/core/account_lifecycle.rb +51 -7
- data/lib/add_auth/core/account_policy.rb +21 -3
- data/lib/add_auth/core/session_cursor.rb +38 -0
- data/lib/add_auth/core/session_key.rb +32 -0
- data/lib/add_auth/core/sessions.rb +19 -8
- data/lib/add_auth/rails/authentication_pages.rb +1 -1
- data/lib/add_auth/rails/doctor.rb +10 -3
- data/lib/add_auth/rails/migration/key_type.rb +3 -3
- data/lib/add_auth/rails/runtime.rb +13 -4
- data/lib/add_auth/rails/stores/account_tokens.rb +10 -2
- data/lib/add_auth/rails/stores/sessions.rb +10 -2
- data/lib/add_auth/version.rb +1 -1
- data/lib/generators/add_auth/accounts/accounts_generator.rb +7 -1
- data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +2 -2
- data/lib/generators/add_auth/accounts/templates/add_add_auth_provisioning.rb.tt +9 -0
- data/lib/generators/add_auth/email_link/email_link_generator.rb +4 -1
- data/lib/generators/add_auth/email_tokens/email_tokens_generator.rb +2 -0
- data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +1 -1
- data/lib/generators/add_auth/external_identities/external_identities_generator.rb +2 -0
- data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt +6 -12
- data/lib/generators/add_auth/feature_configuration.rb +21 -0
- data/lib/generators/add_auth/install/templates/initializer.rb +154 -50
- data/lib/generators/add_auth/mobile_sessions/mobile_sessions_generator.rb +2 -0
- data/lib/generators/add_auth/mobile_sessions/templates/create_add_auth_mobile_handoffs.rb.tt +4 -4
- data/lib/generators/add_auth/notifications/notifications_generator.rb +5 -4
- data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +1 -1
- data/lib/generators/add_auth/passkeys/passkeys_generator.rb +5 -21
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +2 -2
- data/lib/generators/add_auth/primary_key.rb +19 -0
- data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +6 -1
- data/lib/generators/add_auth/session_upgrade/templates/add_add_auth_session_cursor_index.rb.tt +5 -0
- data/lib/generators/add_auth/step_up/step_up_generator.rb +5 -5
- metadata +21 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Core
|
|
5
|
+
class SessionKey
|
|
6
|
+
UUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
7
|
+
MAX_INTEGER = (2**63) - 1
|
|
8
|
+
|
|
9
|
+
def initialize(type:)
|
|
10
|
+
raise ArgumentError, "unsupported session key type" unless %i[integer bigint uuid].include?(type)
|
|
11
|
+
@uuid = type == :uuid
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse(value)
|
|
15
|
+
if @uuid
|
|
16
|
+
value.downcase if value.is_a?(String) && value.ascii_only? && UUID.match?(value)
|
|
17
|
+
elsif value.is_a?(Integer)
|
|
18
|
+
value if value.between?(1, MAX_INTEGER)
|
|
19
|
+
elsif value.is_a?(String) && value.ascii_only? && value.match?(/\A[1-9]\d{0,18}\z/)
|
|
20
|
+
integer = value.to_i
|
|
21
|
+
integer if integer <= MAX_INTEGER
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Stock Rails signed cookies serialize integer IDs as integers, UUIDs as
|
|
26
|
+
# strings. Numeric strings must not broaden the legacy integer bridge.
|
|
27
|
+
def legacy(value)
|
|
28
|
+
parse(value) if @uuid || value.is_a?(Integer)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "add_auth/core/mobile_profile"
|
|
5
|
+
require "add_auth/core/session_key"
|
|
6
|
+
require "add_auth/core/session_cursor"
|
|
5
7
|
|
|
6
8
|
module AddAuth
|
|
7
9
|
module Core
|
|
@@ -37,6 +39,8 @@ module AddAuth
|
|
|
37
39
|
raise ArgumentError, "legacy_bridge_until must be an absolute Time or nil"
|
|
38
40
|
end
|
|
39
41
|
@store, @digest, @eligible, @clock = store, digest, eligible, clock
|
|
42
|
+
@key = SessionKey.new(type: store.respond_to?(:id_type) ? store.id_type : :integer)
|
|
43
|
+
@cursor = SessionCursor.new(key: @key)
|
|
40
44
|
@access_policy = access_policy
|
|
41
45
|
@password_lifecycle = password_lifecycle
|
|
42
46
|
@on_sign_in = on_sign_in
|
|
@@ -191,13 +195,14 @@ module AddAuth
|
|
|
191
195
|
def list_page(user:, current_session_id: nil, before: nil)
|
|
192
196
|
empty = Page.new(entries: [])
|
|
193
197
|
return empty unless user && @eligible.call(user) == true
|
|
194
|
-
|
|
198
|
+
boundary = @cursor.decode(before) unless before.nil?
|
|
199
|
+
return empty unless before.nil? || boundary
|
|
195
200
|
|
|
196
201
|
now = @clock.now
|
|
197
|
-
candidates = @store.list_for_user(user_id: user.id, before:
|
|
202
|
+
candidates = @store.list_for_user(user_id: user.id, before: boundary,
|
|
198
203
|
excluding: current_session_id, limit: PAGE_SIZE + 1, now: now,
|
|
199
204
|
active_after: now - [@idle, @remembered&.fetch(:idle_timeout) || @idle, @mobile&.idle_timeout || @idle].max, legacy: !!(@deadline && now < @deadline))
|
|
200
|
-
cursor = candidates[PAGE_SIZE - 1]
|
|
205
|
+
cursor = @cursor.encode(candidates[PAGE_SIZE - 1]) if candidates.length > PAGE_SIZE
|
|
201
206
|
rows = candidates.first(PAGE_SIZE)
|
|
202
207
|
if before.nil? && current_session_id
|
|
203
208
|
current = @store.find_for_user(user_id: user.id, session_id: current_session_id)
|
|
@@ -217,16 +222,22 @@ module AddAuth
|
|
|
217
222
|
|
|
218
223
|
# Both the initiating bearer and target ownership are checked under lock.
|
|
219
224
|
def revoke_one(user:, session:, session_id:)
|
|
220
|
-
|
|
225
|
+
target_id = @key.parse(session_id)
|
|
226
|
+
return false unless target_id
|
|
221
227
|
|
|
222
228
|
with_live_session(user: user, session: session) do |account, _current, now|
|
|
223
|
-
target = @store.find_for_user_in_transaction(user_id: account.id, session_id:
|
|
229
|
+
target = @store.find_for_user_in_transaction(user_id: account.id, session_id: target_id)
|
|
224
230
|
next false unless target
|
|
225
231
|
@store.update(target, revoked_at: now) unless target.revoked_at
|
|
226
232
|
true
|
|
227
233
|
end || false
|
|
228
234
|
end
|
|
229
235
|
|
|
236
|
+
def current_session?(session:, session_id:)
|
|
237
|
+
id = @key.parse(session_id)
|
|
238
|
+
!!(id && session && id == session.id)
|
|
239
|
+
end
|
|
240
|
+
|
|
230
241
|
# Password proof is obtained from the locked, current account. Intake
|
|
231
242
|
# limits must be applied by the caller before reaching this expensive port.
|
|
232
243
|
def reauthenticate(user:, session:, purpose:, policy:)
|
|
@@ -315,10 +326,10 @@ module AddAuth
|
|
|
315
326
|
private
|
|
316
327
|
|
|
317
328
|
def cookie_lookup(value)
|
|
318
|
-
if value.is_a?(String) && PATTERN.match?(value)
|
|
329
|
+
if value.is_a?(String) && value.ascii_only? && PATTERN.match?(value)
|
|
319
330
|
{digest: @digest.digest(value)}
|
|
320
|
-
elsif
|
|
321
|
-
{id:
|
|
331
|
+
elsif @deadline && @clock.now < @deadline && (id = @key.legacy(value))
|
|
332
|
+
{id: id}
|
|
322
333
|
end
|
|
323
334
|
end
|
|
324
335
|
|
|
@@ -16,7 +16,7 @@ module AddAuth
|
|
|
16
16
|
add_auth_accept(grant)
|
|
17
17
|
redirect_to destination, status: :see_other
|
|
18
18
|
else
|
|
19
|
-
@error = "Email or password is incorrect. Try again
|
|
19
|
+
@error = "Email or password is incorrect. Try again."
|
|
20
20
|
page("form", status: :unprocessable_entity)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -120,6 +120,11 @@ module AddAuth
|
|
|
120
120
|
check("Install provider account management routes") { ::Rails.application.routes.recognize_path("/account/external-identities", method: :get) }
|
|
121
121
|
end
|
|
122
122
|
if config.lifecycle.enabled
|
|
123
|
+
check("Set explicit confirmation and unconfirmed-reset policies") { Runtime.account_policy }
|
|
124
|
+
if config.lifecycle.confirmation_required == false
|
|
125
|
+
columns(::User, %w[add_auth_provisioned_at], "optional confirmation provisioning")
|
|
126
|
+
check("Enable password authentication for optional signup confirmation") { config.passwords_enabled }
|
|
127
|
+
end
|
|
123
128
|
if defined?(::PasswordsController)
|
|
124
129
|
check("Retire stock password reset entry points through the shared account flow") { ::PasswordsController < AccountPasswordEntry }
|
|
125
130
|
end
|
|
@@ -179,10 +184,12 @@ module AddAuth
|
|
|
179
184
|
check("Enable security notification delivery errors") { ::AddAuth::SecurityMailer.raise_delivery_errors }
|
|
180
185
|
end
|
|
181
186
|
if config.step_up.enabled
|
|
182
|
-
check("Enable session
|
|
187
|
+
check("Enable session prerequisites for step_up") { config.session.enabled }
|
|
183
188
|
columns(::Session, %w[elevation_version elevation_expires_at], "reauthentication")
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
if config.email_link.enabled
|
|
190
|
+
columns(defined?(::AddAuthSignInToken) && ::AddAuthSignInToken,
|
|
191
|
+
%w[browser_digest session_id session_digest authentication_purpose], "reauthentication")
|
|
192
|
+
end
|
|
186
193
|
check("Declare valid step-up purposes and safe return destinations") { Runtime.step_up_purposes.any? && Runtime.step_up_policy }
|
|
187
194
|
%w[/reauthenticate /reauthenticate/link].each do |path|
|
|
188
195
|
check("Install route #{path}") { ::Rails.application.routes.recognize_path(path, method: :get) }
|
|
@@ -7,15 +7,15 @@ module AddAuth
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
# Resolve when the migration runs, after the host's account table exists.
|
|
10
|
-
#
|
|
10
|
+
# Keep the session keyword for migrations generated by earlier releases.
|
|
11
11
|
def for(connection, table, session: false)
|
|
12
12
|
key = connection.primary_key(table)
|
|
13
13
|
column = connection.columns(table).find { |entry| entry.name == key }
|
|
14
14
|
raise ArgumentError, "AddAuth requires a single integer or UUID primary key on #{table}" unless column
|
|
15
15
|
return :bigint if column.type == :integer && column.limit == 8
|
|
16
16
|
return :integer if column.type == :integer
|
|
17
|
-
return :uuid if column.type == :uuid
|
|
18
|
-
raise ArgumentError,
|
|
17
|
+
return :uuid if column.type == :uuid
|
|
18
|
+
raise ArgumentError, "AddAuth requires an integer or UUID primary key on #{table}"
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -15,11 +15,17 @@ module AddAuth
|
|
|
15
15
|
def config = AddAuth.configuration
|
|
16
16
|
|
|
17
17
|
def account_policy
|
|
18
|
-
Core::AccountPolicy.new(enabled: config.lifecycle.enabled,
|
|
18
|
+
Core::AccountPolicy.new(enabled: config.lifecycle.enabled,
|
|
19
|
+
eligible: ->(user) { config.eligible.call(user) == true && config.lifecycle.eligible.call(user) == true },
|
|
20
|
+
confirmation_required: config.lifecycle.confirmation_required, reset_unconfirmed: config.lifecycle.reset_unconfirmed)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def eligible(user)
|
|
22
|
-
|
|
24
|
+
account_policy.allowed?(user)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def trusted_recovery_address(user)
|
|
28
|
+
config.trusted_recovery_address.call(user) if account_policy.trusted_address?(user)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
def authority
|
|
@@ -74,11 +80,14 @@ module AddAuth
|
|
|
74
80
|
|
|
75
81
|
def accounts
|
|
76
82
|
raise AddAuth::Error, "enable account lifecycle first" unless config.lifecycle.enabled
|
|
83
|
+
if !account_policy.confirmation_required? && !::User.column_names.include?("add_auth_provisioned_at")
|
|
84
|
+
raise AddAuth::Error, "rerun add_auth:accounts and migrate before enabling optional confirmation"
|
|
85
|
+
end
|
|
77
86
|
require "add_auth/rails/stores/account_tokens"
|
|
78
87
|
Core::AccountLifecycle.new(store: Stores::AccountTokens.new(user_model: ::User, token_model: ::AddAuthAccountToken,
|
|
79
88
|
session_model: ::Session, address_model: ::AddAuthAddressClaim, authority: authority, provision: config.lifecycle.provision, delete_account: config.lifecycle.delete_account),
|
|
80
89
|
digest: config.sign_in_token_digest, delivery_cipher: DeliveryCipher.new(key: key("account-proofs")),
|
|
81
|
-
policy: account_policy, password_policy: config.lifecycle.password_policy, trusted_address:
|
|
90
|
+
policy: account_policy, password_policy: config.lifecycle.password_policy, trusted_address: method(:trusted_recovery_address),
|
|
82
91
|
lifetime: config.lifecycle.proof_lifetime, notify: ->(**event) { security_events.issue(**event) if config.notifications.enabled },
|
|
83
92
|
sessions: sessions, step_up_policy: step_up_policy, profile_attributes: config.lifecycle.profile_attributes, deletion_allowed: config.lifecycle.deletion_allowed,
|
|
84
93
|
external_identities: -> { external_identities })
|
|
@@ -168,7 +177,7 @@ module AddAuth
|
|
|
168
177
|
Core::AccessPolicy.new(credentials: ->(id) {
|
|
169
178
|
::AddAuthCredential.find_by(external_id: id) if defined?(::AddAuthCredential) && ::AddAuthCredential.table_exists?
|
|
170
179
|
}, passkeys_enabled: config.passkeys.enabled, email_enabled: Core::Intake.email_available?(config),
|
|
171
|
-
trusted_recovery_address:
|
|
180
|
+
trusted_recovery_address: method(:trusted_recovery_address), password_enabled: config.passwords_enabled,
|
|
172
181
|
external_enabled: config.external_identities.enabled,
|
|
173
182
|
external_current: ->(**arguments) { external_identities.credential_current?(**arguments) })
|
|
174
183
|
end
|
|
@@ -16,16 +16,19 @@ module AddAuth
|
|
|
16
16
|
@account_credentials = AccountCredentials.new(user_model: user_model, authority: authority)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def create_account(email:, password:, profile: {})
|
|
19
|
+
def create_account(email:, password:, profile: {}, replacing: nil)
|
|
20
20
|
raise AddAuth::Error, "registration must own its transaction" if @users.connection.transaction_open?
|
|
21
21
|
completed = false
|
|
22
|
+
saved = false
|
|
22
23
|
@users.transaction do |transaction|
|
|
23
24
|
transaction.after_commit { completed = true }
|
|
25
|
+
AccountLock.new(@users).current_in_transaction(id: replacing.user_id) if replacing
|
|
24
26
|
user = @users.new(profile)
|
|
25
27
|
user.email_address = email
|
|
26
28
|
user.password = password
|
|
27
29
|
user.add_auth_authority = "add_auth"
|
|
28
30
|
user.save!
|
|
31
|
+
saved = true
|
|
29
32
|
yield user
|
|
30
33
|
end
|
|
31
34
|
raise AddAuth::Error, "registration transaction rolled back" unless completed
|
|
@@ -33,6 +36,7 @@ module AddAuth
|
|
|
33
36
|
rescue ActiveRecord::RecordNotUnique, Core::AccountLifecycle::Conflict
|
|
34
37
|
:duplicate
|
|
35
38
|
rescue ActiveRecord::RecordInvalid => error
|
|
39
|
+
raise if saved
|
|
36
40
|
(error.record.errors.attribute_names - [:email_address]).empty? ? :duplicate : :invalid
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -81,7 +85,11 @@ module AddAuth
|
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
def revoke_authority(user:, at:) = @authority.revoke(user_id: user.id, at: at)
|
|
84
|
-
|
|
88
|
+
|
|
89
|
+
def provision(user:)
|
|
90
|
+
@provision.call(user)
|
|
91
|
+
user.reload
|
|
92
|
+
end
|
|
85
93
|
|
|
86
94
|
def delete_account(user:)
|
|
87
95
|
raise AddAuth::Error, "account deletion requires its owning transaction" unless @users.connection.transaction_open?
|
|
@@ -6,8 +6,11 @@ module AddAuth
|
|
|
6
6
|
module Rails
|
|
7
7
|
module Stores
|
|
8
8
|
class Sessions
|
|
9
|
+
attr_reader :id_type
|
|
10
|
+
|
|
9
11
|
def initialize(user_model:, session_model:)
|
|
10
12
|
@users, @sessions = user_model, session_model
|
|
13
|
+
@id_type = @sessions.columns_hash.fetch(@sessions.primary_key).type
|
|
11
14
|
raise ArgumentError, "authentication models must share one connection pool" unless @users.connection_pool.equal?(@sessions.connection_pool)
|
|
12
15
|
@lock = AccountLock.new(@users)
|
|
13
16
|
end
|
|
@@ -38,11 +41,16 @@ module AddAuth
|
|
|
38
41
|
# Query cutoffs come from Core; Core still authorizes every returned row.
|
|
39
42
|
def list_for_user(user_id:, before:, excluding:, limit:, now:, active_after:, legacy:)
|
|
40
43
|
scope = @sessions.where(user_id: user_id, revoked_at: nil)
|
|
41
|
-
|
|
44
|
+
if before&.key?(:legacy_id)
|
|
45
|
+
scope = scope.where("id < ?", before.fetch(:legacy_id))
|
|
46
|
+
elsif before
|
|
47
|
+
scope = scope.where("created_at < ? OR (created_at = ? AND id < ?)",
|
|
48
|
+
before.fetch(:created_at), before.fetch(:created_at), before.fetch(:id))
|
|
49
|
+
end
|
|
42
50
|
scope = scope.where.not(id: excluding) if excluding
|
|
43
51
|
scope = scope.where("(expires_at IS NULL OR expires_at > ?) AND (last_seen_at IS NULL OR last_seen_at > ?)", now, active_after)
|
|
44
52
|
scope = scope.where.not(expires_at: nil).where.not(last_seen_at: nil) unless legacy
|
|
45
|
-
scope.order(id: :desc).limit(limit).to_a
|
|
53
|
+
scope.order(created_at: :desc, id: :desc).limit(limit).to_a
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
def find_for_user(user_id:, session_id:) = @sessions.find_by(user_id: user_id, id: session_id)
|
data/lib/add_auth/version.rb
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "generators/add_auth/primary_key"
|
|
4
5
|
require "rails/generators/active_record"
|
|
5
6
|
|
|
6
7
|
module AddAuth
|
|
7
8
|
module Generators
|
|
8
9
|
class AccountsGenerator < ::Rails::Generators::Base
|
|
9
10
|
include ::Rails::Generators::Migration
|
|
11
|
+
include PrimaryKey
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
class_option :email_link, type: :boolean, default: true, desc: "Enable email sign-in as a step-up dependency"
|
|
12
15
|
desc "Prepare optional account lifecycle schema and pages; review existing account confirmation and policy before enabling"
|
|
13
16
|
|
|
14
17
|
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
15
18
|
|
|
16
19
|
def prerequisites
|
|
17
20
|
invoke "add_auth:notifications"
|
|
18
|
-
invoke "add_auth:step_up"
|
|
21
|
+
invoke "add_auth:step_up", [], email_link: options[:email_link]
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def persistence
|
|
22
25
|
unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_accounts.rb")].any?
|
|
23
26
|
migration_template "add_add_auth_accounts.rb.tt", "db/migrate/add_add_auth_accounts.rb"
|
|
24
27
|
end
|
|
28
|
+
unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_provisioning.rb")].any?
|
|
29
|
+
migration_template "add_add_auth_provisioning.rb.tt", "db/migrate/add_add_auth_provisioning.rb"
|
|
30
|
+
end
|
|
25
31
|
%w[add_auth_account_token add_auth_address_claim].each do |name|
|
|
26
32
|
template "#{name}.rb", "app/models/#{name}.rb" unless File.exist?(File.join(destination_root, "app/models/#{name}.rb"))
|
|
27
33
|
end
|
|
@@ -14,7 +14,7 @@ class AddAddAuthAccounts < ActiveRecord::Migration[8.0]
|
|
|
14
14
|
add_column :users, :add_auth_authority, :string, default: "add_auth", null: false unless column_exists?(:users, :add_auth_authority)
|
|
15
15
|
add_column :sessions, :remembered, :boolean, default: false_default, null: false unless column_exists?(:sessions, :remembered)
|
|
16
16
|
add_column :sessions, :idle_timeout, :integer unless column_exists?(:sessions, :idle_timeout)
|
|
17
|
-
create_table :add_auth_address_claims do |t|
|
|
17
|
+
create_table :add_auth_address_claims, id: <%= authentication_primary_key_type %> do |t|
|
|
18
18
|
t.references :user, type: user_key_type, null: false, foreign_key: {on_delete: :cascade}
|
|
19
19
|
t.string :digest, null: false
|
|
20
20
|
t.string :state, null: false
|
|
@@ -22,7 +22,7 @@ class AddAddAuthAccounts < ActiveRecord::Migration[8.0]
|
|
|
22
22
|
end
|
|
23
23
|
add_index :add_auth_address_claims, :digest, unique: true
|
|
24
24
|
add_index :add_auth_address_claims, [:user_id, :state], unique: true
|
|
25
|
-
create_table :add_auth_account_tokens do |t|
|
|
25
|
+
create_table :add_auth_account_tokens, id: <%= authentication_primary_key_type %> do |t|
|
|
26
26
|
t.references :user, type: user_key_type, null: false, foreign_key: {on_delete: :cascade}
|
|
27
27
|
t.string :digest, null: false
|
|
28
28
|
t.string :purpose, null: false
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddAddAuthProvisioning < ActiveRecord::Migration[8.0]
|
|
2
|
+
def up
|
|
3
|
+
add_column :users, :add_auth_provisioned_at, :datetime unless column_exists?(:users, :add_auth_provisioned_at)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def down
|
|
7
|
+
raise ActiveRecord::IrreversibleMigration, "Retain provisioning receipts to prevent repeating account setup"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "generators/add_auth/feature_configuration"
|
|
4
5
|
require "rails/generators/active_record"
|
|
5
6
|
|
|
6
7
|
module AddAuth
|
|
7
8
|
module Generators
|
|
8
9
|
class EmailLinkGenerator < ::Rails::Generators::Base
|
|
9
10
|
include ::Rails::Generators::Migration
|
|
11
|
+
include FeatureConfiguration
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
class_option :enable, type: :boolean, default: true, desc: "Enable email sign-in after preparing its storage"
|
|
12
15
|
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
13
16
|
|
|
14
17
|
def dependencies
|
|
@@ -48,7 +51,7 @@ module AddAuth
|
|
|
48
51
|
unless File.read(File.join(destination_root, "config/routes.rb")).include?("add_auth/challenge.js")
|
|
49
52
|
route 'get "add_auth/challenge.js", to: "add_auth/assets#challenge"'
|
|
50
53
|
end
|
|
51
|
-
|
|
54
|
+
enable_feature(:email_link) if options[:enable]
|
|
52
55
|
say "Review and migrate, configure base_url/mail_from, a durable queue, shared rate-limit cache and a recurring add_auth:deliver_pending sweep."
|
|
53
56
|
end
|
|
54
57
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "generators/add_auth/primary_key"
|
|
4
5
|
require "rails/generators/active_record"
|
|
5
6
|
|
|
6
7
|
module AddAuth
|
|
@@ -8,6 +9,7 @@ module AddAuth
|
|
|
8
9
|
# Internal persistence foundation only. Does not install sign-in routes.
|
|
9
10
|
class EmailTokensGenerator < ::Rails::Generators::Base
|
|
10
11
|
include ::Rails::Generators::Migration
|
|
12
|
+
include PrimaryKey
|
|
11
13
|
|
|
12
14
|
source_root File.expand_path("templates", __dir__)
|
|
13
15
|
|
|
@@ -2,7 +2,7 @@ require "add_auth/rails/migration/key_type"
|
|
|
2
2
|
|
|
3
3
|
class CreateAddAuthSignInTokens < ActiveRecord::Migration[8.0]
|
|
4
4
|
def change
|
|
5
|
-
create_table :add_auth_sign_in_tokens do |t|
|
|
5
|
+
create_table :add_auth_sign_in_tokens, id: <%= authentication_primary_key_type %> do |t|
|
|
6
6
|
t.references :user, type: AddAuth::Rails::Migration::KeyType.for(connection, :users), null: false, foreign_key: true
|
|
7
7
|
t.string :digest, limit: 64, null: false
|
|
8
8
|
t.string :identifier_digest, limit: 64, null: false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "generators/add_auth/primary_key"
|
|
4
5
|
require "rails/generators/active_record"
|
|
5
6
|
|
|
6
7
|
module AddAuth
|
|
@@ -8,6 +9,7 @@ module AddAuth
|
|
|
8
9
|
# Optional persistence and routes; provider strategies remain disabled until configured.
|
|
9
10
|
class ExternalIdentitiesGenerator < ::Rails::Generators::Base
|
|
10
11
|
include ::Rails::Generators::Migration
|
|
12
|
+
include PrimaryKey
|
|
11
13
|
|
|
12
14
|
source_root File.expand_path("templates", __dir__)
|
|
13
15
|
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
require "add_auth/rails/migration/key_type"
|
|
2
|
+
|
|
1
3
|
class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
|
|
2
4
|
def change
|
|
3
|
-
user_type =
|
|
4
|
-
session_type =
|
|
5
|
-
create_table :add_auth_external_identities do |t|
|
|
5
|
+
user_type = AddAuth::Rails::Migration::KeyType.for(connection, :users)
|
|
6
|
+
session_type = AddAuth::Rails::Migration::KeyType.for(connection, :sessions, session: true)
|
|
7
|
+
create_table :add_auth_external_identities, id: <%= authentication_primary_key_type %> do |t|
|
|
6
8
|
t.references :user, type: user_type, null: false, foreign_key: {on_delete: :cascade}
|
|
7
9
|
# SHA256 of the exact JSON [configuration, issuer, audience, subject].
|
|
8
10
|
# Indexing this key preserves case-sensitive subjects on either store.
|
|
@@ -18,7 +20,7 @@ class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
|
|
|
18
20
|
t.timestamps
|
|
19
21
|
end
|
|
20
22
|
add_index :add_auth_external_identities, :namespace, unique: true
|
|
21
|
-
create_table :add_auth_external_transactions do |t|
|
|
23
|
+
create_table :add_auth_external_transactions, id: <%= authentication_primary_key_type %> do |t|
|
|
22
24
|
t.string :digest, null: false
|
|
23
25
|
t.string :browser_digest, null: false
|
|
24
26
|
t.string :provider_id, limit: 2048, null: false
|
|
@@ -39,12 +41,4 @@ class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
|
|
|
39
41
|
add_column :sessions, :authentication_external_id, :string
|
|
40
42
|
add_column :sessions, :authentication_external_version, :string
|
|
41
43
|
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def reference_type(table)
|
|
46
|
-
column = connection.columns(table).find { |item| item.name == connection.primary_key(table) }
|
|
47
|
-
raise "Unsupported authentication primary key" unless column && [:integer, :uuid].include?(column.type)
|
|
48
|
-
(column.type == :integer && column.limit == 8) ? :bigint : column.type
|
|
49
|
-
end
|
|
50
44
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Generators
|
|
5
|
+
module FeatureConfiguration
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def enable_feature(name)
|
|
9
|
+
path = "config/initializers/add_auth.rb"
|
|
10
|
+
source = File.read(File.join(destination_root, path))
|
|
11
|
+
setting = "config.#{name}.enabled"
|
|
12
|
+
pattern = /^(\s*)(?:# #{Regexp.escape(setting)} = true|#{Regexp.escape(setting)} = false)$/
|
|
13
|
+
if source.match?(pattern)
|
|
14
|
+
gsub_file path, pattern, "\\1#{setting} = true"
|
|
15
|
+
elsif !source.match?(/^\s*#{Regexp.escape(setting)}\s*=/)
|
|
16
|
+
append_to_file path, "\nAddAuth.configure do |config|\n #{setting} = true\nend\n"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|