add_auth 0.2.2 → 0.4.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 +36 -0
- data/README.md +64 -17
- data/ROADMAP.md +98 -43
- data/SECURITY.md +8 -0
- data/app/controllers/add_auth/accounts_controller.rb +146 -0
- data/app/controllers/add_auth/assets_controller.rb +2 -1
- data/app/controllers/add_auth/external_identities_controller.rb +56 -0
- data/app/controllers/add_auth/mobile_sessions_controller.rb +120 -0
- data/app/controllers/add_auth/provider_sign_ins_controller.rb +248 -0
- data/app/controllers/add_auth/reauthentications_controller.rb +5 -0
- data/app/helpers/add_auth/sign_ins_helper.rb +2 -1
- data/app/jobs/add_auth/account_delivery_job.rb +14 -0
- data/app/jobs/add_auth/account_request_job.rb +17 -0
- data/app/mailers/add_auth/account_mailer.rb +12 -0
- data/app/views/add_auth/account_mailer/link.text.erb +7 -0
- data/app/views/add_auth/accounts/_check_email.html.erb +5 -0
- data/app/views/add_auth/accounts/_delete.html.erb +6 -0
- data/app/views/add_auth/accounts/_email.html.erb +12 -0
- data/app/views/add_auth/accounts/_invalid_proof.html.erb +5 -0
- data/app/views/add_auth/accounts/_password.html.erb +12 -0
- data/app/views/add_auth/accounts/_proof.html.erb +16 -0
- data/app/views/add_auth/accounts/_register.html.erb +17 -0
- data/app/views/add_auth/accounts/_request.html.erb +17 -0
- data/app/views/add_auth/accounts/_unavailable.html.erb +3 -0
- data/app/views/add_auth/external_identities/_index.html.erb +22 -0
- data/app/views/add_auth/external_identities/_unavailable.html.erb +3 -0
- data/app/views/add_auth/passkeys/_controls.html.erb +3 -1
- data/app/views/add_auth/provider_sign_ins/_enrollment.html.erb +12 -0
- data/app/views/add_auth/provider_sign_ins/_failure.html.erb +3 -0
- data/app/views/add_auth/provider_sign_ins/_prepare.html.erb +7 -0
- data/app/views/add_auth/reauthentications/_form.html.erb +9 -0
- data/app/views/add_auth/sessions/_list.html.erb +1 -1
- data/app/views/add_auth/sessions/index.html.erb +7 -0
- data/app/views/add_auth/sign_ins/_confirmation.html.erb +1 -0
- data/app/views/add_auth/sign_ins/_form.html.erb +17 -0
- data/app/views/add_auth/sign_ins/_remember.html.erb +6 -0
- data/app/views/layouts/add_auth/authentication.html.erb +4 -3
- data/exe/add_auth-preflight +26 -0
- data/lib/add_auth/configuration.rb +78 -4
- data/lib/add_auth/core/access_policy.rb +11 -1
- data/lib/add_auth/core/account_lifecycle.rb +295 -0
- data/lib/add_auth/core/account_policy.rb +68 -0
- data/lib/add_auth/core/external_identities/evidence.rb +98 -0
- data/lib/add_auth/core/external_identities.rb +278 -0
- data/lib/add_auth/core/intake.rb +6 -1
- data/lib/add_auth/core/maintenance.rb +5 -3
- data/lib/add_auth/core/migration/account_adoption.rb +79 -0
- data/lib/add_auth/core/migration/readiness.rb +41 -0
- data/lib/add_auth/core/mobile_authentication.rb +35 -0
- data/lib/add_auth/core/mobile_handoffs.rb +83 -0
- data/lib/add_auth/core/mobile_profile.rb +46 -0
- data/lib/add_auth/core/mobile_response.rb +12 -0
- data/lib/add_auth/core/native_authentication.rb +77 -0
- data/lib/add_auth/core/passwords/bcrypt_support.rb +28 -0
- data/lib/add_auth/core/passwords/credential.rb +49 -0
- data/lib/add_auth/core/passwords/legacy_bcrypt.rb +43 -0
- data/lib/add_auth/core/passwords/lifecycle.rb +60 -0
- data/lib/add_auth/core/remaining_factors.rb +24 -0
- data/lib/add_auth/core/sessions.rb +102 -17
- data/lib/add_auth/core/step_up.rb +13 -1
- data/lib/add_auth/migration/static_inventory.rb +116 -0
- data/lib/add_auth/rails/account_password_entry.rb +24 -0
- data/lib/add_auth/rails/authentication.rb +1 -1
- data/lib/add_auth/rails/authentication_pages.rb +6 -1
- data/lib/add_auth/rails/doctor.rb +80 -6
- data/lib/add_auth/rails/ejection.rb +1 -1
- data/lib/add_auth/rails/engine.rb +6 -1
- data/lib/add_auth/rails/migration/account_store.rb +41 -0
- data/lib/add_auth/rails/migration/effective_inventory.rb +162 -0
- data/lib/add_auth/rails/migration/key_type.rb +23 -0
- data/lib/add_auth/rails/migration/source_bridge.rb +53 -0
- data/lib/add_auth/rails/password_adoption.rb +37 -0
- data/lib/add_auth/rails/provider_enrollment.rb +61 -0
- data/lib/add_auth/rails/provider_libraries/apple_form_post_correlation.rb +127 -0
- data/lib/add_auth/rails/provider_libraries/apple_jwks.rb +68 -0
- data/lib/add_auth/rails/provider_libraries/apple_native.rb +55 -0
- data/lib/add_auth/rails/provider_libraries/callback_route.rb +16 -0
- data/lib/add_auth/rails/provider_libraries/omniauth.rb +136 -0
- data/lib/add_auth/rails/provider_libraries/omniauth_correlation.rb +107 -0
- data/lib/add_auth/rails/provider_libraries/openid_connect.rb +62 -0
- data/lib/add_auth/rails/provider_libraries/request_protection.rb +43 -0
- data/lib/add_auth/rails/runtime.rb +179 -8
- data/lib/add_auth/rails/stores/account_credentials.rb +44 -0
- data/lib/add_auth/rails/stores/account_lock.rb +16 -7
- data/lib/add_auth/rails/stores/account_tokens.rb +104 -0
- data/lib/add_auth/rails/stores/authority.rb +28 -0
- data/lib/add_auth/rails/stores/commit_dispatch.rb +32 -0
- data/lib/add_auth/rails/stores/external_identities.rb +91 -0
- data/lib/add_auth/rails/stores/maintenance.rb +1 -1
- data/lib/add_auth/rails/stores/mobile_handoffs.rb +53 -0
- data/lib/add_auth/rails/stores/security_events.rb +3 -6
- data/lib/add_auth/rails/user_lifecycle.rb +8 -10
- data/lib/add_auth/result.rb +1 -0
- data/lib/add_auth/version.rb +1 -1
- data/lib/add_auth.rb +4 -0
- data/lib/generators/add_auth/accounts/accounts_generator.rb +56 -0
- data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +49 -0
- data/lib/generators/add_auth/accounts/templates/add_add_auth_provisioning.rb.tt +9 -0
- data/lib/generators/add_auth/accounts/templates/add_auth_account_token.rb +6 -0
- data/lib/generators/add_auth/accounts/templates/add_auth_address_claim.rb +6 -0
- data/lib/generators/add_auth/devise_accounts/devise_accounts_generator.rb +23 -0
- data/lib/generators/add_auth/devise_accounts/templates/prepare_add_auth_devise_accounts.rb.tt +17 -0
- data/lib/generators/add_auth/email_link/email_link_generator.rb +4 -1
- data/lib/generators/add_auth/email_link/templates/add_auth_challenge.js +1 -0
- data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +3 -1
- data/lib/generators/add_auth/external_identities/external_identities_generator.rb +52 -0
- data/lib/generators/add_auth/external_identities/templates/add_add_auth_external_enrollment.rb.tt +5 -0
- data/lib/generators/add_auth/external_identities/templates/add_add_auth_external_identity_validity.rb.tt +5 -0
- data/lib/generators/add_auth/external_identities/templates/add_auth_external_identity.rb +4 -0
- data/lib/generators/add_auth/external_identities/templates/add_auth_external_transaction.rb +6 -0
- data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt +50 -0
- data/lib/generators/add_auth/external_identities/templates/providers.rb +51 -0
- data/lib/generators/add_auth/feature_configuration.rb +21 -0
- data/lib/generators/add_auth/install/templates/initializer.rb +154 -46
- data/lib/generators/add_auth/javascript/templates/passkey.js +8 -5
- data/lib/generators/add_auth/mobile_sessions/mobile_sessions_generator.rb +48 -0
- data/lib/generators/add_auth/mobile_sessions/templates/add_add_auth_mobile_sessions.rb.tt +9 -0
- data/lib/generators/add_auth/mobile_sessions/templates/add_auth_mobile_handoff.rb +5 -0
- data/lib/generators/add_auth/mobile_sessions/templates/create_add_auth_mobile_handoffs.rb.tt +27 -0
- data/lib/generators/add_auth/mobile_sessions/templates/mobile.rb +8 -0
- data/lib/generators/add_auth/notifications/notifications_generator.rb +3 -4
- data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +3 -1
- data/lib/generators/add_auth/passkeys/passkeys_generator.rb +3 -21
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +7 -3
- data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +3 -1
- data/lib/generators/add_auth/step_up/step_up_generator.rb +5 -5
- data/lib/generators/add_auth/step_up/templates/add_add_auth_reauthentication.rb.tt +3 -1
- data/lib/tasks/add_auth.rake +18 -1
- metadata +101 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
require "add_auth/rails/migration/key_type"
|
|
2
|
+
|
|
1
3
|
class CreateAddAuthSignInTokens < ActiveRecord::Migration[8.0]
|
|
2
4
|
def change
|
|
3
5
|
create_table :add_auth_sign_in_tokens do |t|
|
|
4
|
-
t.references :user, null: false, foreign_key: true
|
|
6
|
+
t.references :user, type: AddAuth::Rails::Migration::KeyType.for(connection, :users), null: false, foreign_key: true
|
|
5
7
|
t.string :digest, limit: 64, null: false
|
|
6
8
|
t.string :identifier_digest, limit: 64, null: false
|
|
7
9
|
t.string :purpose, null: false, default: "sign_in"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module AddAuth
|
|
7
|
+
module Generators
|
|
8
|
+
# Optional persistence and routes; provider strategies remain disabled until configured.
|
|
9
|
+
class ExternalIdentitiesGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
14
|
+
|
|
15
|
+
def persistence
|
|
16
|
+
unless Dir[File.join(destination_root, "db/migrate/*_create_add_auth_external_identities.rb")].any?
|
|
17
|
+
migration_template "create_add_auth_external_identities.rb.tt", "db/migrate/create_add_auth_external_identities.rb"
|
|
18
|
+
end
|
|
19
|
+
unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_external_identity_validity.rb")].any?
|
|
20
|
+
migration_template "add_add_auth_external_identity_validity.rb.tt", "db/migrate/add_add_auth_external_identity_validity.rb"
|
|
21
|
+
end
|
|
22
|
+
unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_external_enrollment.rb")].any?
|
|
23
|
+
migration_template "add_add_auth_external_enrollment.rb.tt", "db/migrate/add_add_auth_external_enrollment.rb"
|
|
24
|
+
end
|
|
25
|
+
%w[external_identity external_transaction].each do |name|
|
|
26
|
+
copy_file "add_auth_#{name}.rb", "app/models/add_auth_#{name}.rb", skip: true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def routes
|
|
31
|
+
source = File.read(File.join(destination_root, "config/routes.rb"))
|
|
32
|
+
return if source.include?("# AddAuth external provider callbacks")
|
|
33
|
+
|
|
34
|
+
route <<~ROUTES
|
|
35
|
+
# AddAuth external provider callbacks
|
|
36
|
+
get "account/sign-up/providers/:provider", to: "add_auth/provider_sign_ins#enrollment", defaults: {flow: "enroll"}
|
|
37
|
+
post "account/sign-up/providers/:provider", to: "add_auth/provider_sign_ins#prepare", defaults: {flow: "enroll"}
|
|
38
|
+
post "sign-in/providers/:provider", to: "add_auth/provider_sign_ins#prepare", defaults: {flow: "sign_in"}
|
|
39
|
+
post "account/external-identities/providers/:provider", to: "add_auth/provider_sign_ins#prepare", defaults: {flow: "link"}
|
|
40
|
+
get "account/external-identities", to: "add_auth/external_identities#index"
|
|
41
|
+
delete "account/external-identities/:id", to: "add_auth/external_identities#destroy"
|
|
42
|
+
post "reauthenticate/providers/:provider", to: "add_auth/provider_sign_ins#prepare", defaults: {flow: "reauthenticate"}
|
|
43
|
+
match "auth/:provider/callback", to: "add_auth/provider_sign_ins#callback", via: [:get, :post], constraints: AddAuth::Rails::ProviderLibraries::CallbackRoute
|
|
44
|
+
ROUTES
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def provider_configuration
|
|
48
|
+
copy_file "providers.rb", "config/initializers/add_auth_providers.rb", skip: true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
class AddAuthExternalTransaction < ApplicationRecord
|
|
2
|
+
self.filter_attributes += [:enrollment_payload, :digest, :browser_digest, :session_digest]
|
|
3
|
+
belongs_to :user, optional: true
|
|
4
|
+
belongs_to :session, optional: true
|
|
5
|
+
def inspect = "#<AddAuthExternalTransaction [FILTERED]>"
|
|
6
|
+
end
|
data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
user_type = reference_type(:users)
|
|
4
|
+
session_type = reference_type(:sessions)
|
|
5
|
+
create_table :add_auth_external_identities do |t|
|
|
6
|
+
t.references :user, type: user_type, null: false, foreign_key: {on_delete: :cascade}
|
|
7
|
+
# SHA256 of the exact JSON [configuration, issuer, audience, subject].
|
|
8
|
+
# Indexing this key preserves case-sensitive subjects on either store.
|
|
9
|
+
t.string :namespace, limit: 64, null: false
|
|
10
|
+
t.string :provider_id, limit: 2048, null: false
|
|
11
|
+
t.string :issuer, limit: 2048, null: false
|
|
12
|
+
t.string :audience, limit: 2048, null: false
|
|
13
|
+
t.string :subject, limit: 2048, null: false
|
|
14
|
+
t.string :provenance, limit: 2048, null: false
|
|
15
|
+
t.string :credential_version, null: false
|
|
16
|
+
t.datetime :linked_at, null: false
|
|
17
|
+
t.datetime :revoked_at
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
add_index :add_auth_external_identities, :namespace, unique: true
|
|
21
|
+
create_table :add_auth_external_transactions do |t|
|
|
22
|
+
t.string :digest, null: false
|
|
23
|
+
t.string :browser_digest, null: false
|
|
24
|
+
t.string :provider_id, limit: 2048, null: false
|
|
25
|
+
t.string :issuer, limit: 2048, null: false
|
|
26
|
+
t.string :audience, limit: 2048, null: false
|
|
27
|
+
t.string :purpose, null: false
|
|
28
|
+
t.references :user, type: user_type, foreign_key: {on_delete: :cascade}
|
|
29
|
+
t.references :session, type: session_type, foreign_key: {on_delete: :cascade}
|
|
30
|
+
t.string :session_digest
|
|
31
|
+
t.integer :policy_version
|
|
32
|
+
t.datetime :issued_at, null: false
|
|
33
|
+
t.datetime :expires_at, null: false
|
|
34
|
+
t.datetime :consumed_at
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
add_index :add_auth_external_transactions, :digest, unique: true
|
|
38
|
+
add_index :add_auth_external_transactions, :expires_at
|
|
39
|
+
add_column :sessions, :authentication_external_id, :string
|
|
40
|
+
add_column :sessions, :authentication_external_version, :string
|
|
41
|
+
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
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# External provider support is opt-in. Add only the provider gems you use to
|
|
4
|
+
# the host application's Gemfile, then configure their OmniAuth middleware
|
|
5
|
+
# using their normal configuration. Reuse existing registrations rather than
|
|
6
|
+
# adding a second OmniAuth::Builder for the same provider. Keep the host's CSRF
|
|
7
|
+
# validator, scopes, request methods and failure handler. AddAuth changes none
|
|
8
|
+
# of them. Keep issuer/audience fixed; never derive them from a request, Host
|
|
9
|
+
# header, profile attribute, or callback parameter.
|
|
10
|
+
#
|
|
11
|
+
# AddAuth's correlation middleware goes before OmniAuth::Builder. It retains
|
|
12
|
+
# opaque Core transaction data only. OmniAuth (or the selected OIDC library)
|
|
13
|
+
# owns code exchange, state, nonce, JWKS/signature, issuer, audience and expiry.
|
|
14
|
+
# Requests without an AddAuth transaction continue through the existing stack.
|
|
15
|
+
# Generated callback routes select only enabled, explicitly registered providers.
|
|
16
|
+
# Review routing precedence before adopting a provider with an existing callback.
|
|
17
|
+
# Custom provider paths/callbacks require an explicit host integration; do not
|
|
18
|
+
# rewrite an existing library configuration to fit this conventional-path example.
|
|
19
|
+
#
|
|
20
|
+
# require "omniauth"
|
|
21
|
+
# require "omniauth-google-oauth2"
|
|
22
|
+
# require "add_auth/rails/provider_libraries/omniauth"
|
|
23
|
+
# require "add_auth/rails/provider_libraries/omniauth_correlation"
|
|
24
|
+
# Rails.application.config.middleware.use AddAuth::Rails::ProviderLibraries::OmniAuthCorrelation,
|
|
25
|
+
# providers: ["google_oauth2"]
|
|
26
|
+
# Rails.application.config.middleware.use OmniAuth::Builder do
|
|
27
|
+
# provider :google_oauth2, ENV.fetch("GOOGLE_CLIENT_ID"), ENV.fetch("GOOGLE_CLIENT_SECRET")
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# verifier = AddAuth::Rails::ProviderLibraries::OmniAuth::Verifier.new(
|
|
31
|
+
# provider: "google_oauth2", provenance: "omniauth-google-oauth2",
|
|
32
|
+
# mapping: {issuer: %i[extra id_info iss], audience: %i[extra id_info aud], subject: %i[extra id_info sub],
|
|
33
|
+
# authenticated_at: %i[extra id_info auth_time]}
|
|
34
|
+
# )
|
|
35
|
+
# AddAuth.configure do |config|
|
|
36
|
+
# config.external_identities.register(
|
|
37
|
+
# id: "google", label: "Google", middleware_name: "google_oauth2",
|
|
38
|
+
# configuration: AddAuth::Core::ExternalIdentities::Configuration.new(
|
|
39
|
+
# id: "google", issuer: "https://accounts.google.com", audience: ENV.fetch("GOOGLE_CLIENT_ID"), verifier: verifier
|
|
40
|
+
# )
|
|
41
|
+
# )
|
|
42
|
+
# config.external_identities.enabled = true
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# Apple form_post needs its own callback-only cookie wrapper before
|
|
46
|
+
# OmniAuth::Builder. Do not weaken your normal Rails session cookie:
|
|
47
|
+
#
|
|
48
|
+
# require "omniauth-apple"
|
|
49
|
+
# require "add_auth/rails/provider_libraries/apple_form_post_correlation"
|
|
50
|
+
# Rails.application.config.middleware.use AddAuth::Rails::ProviderLibraries::AppleFormPostCorrelation,
|
|
51
|
+
# key: Rails.application.key_generator.generate_key("add_auth.apple-form-post.v1", 32)
|
|
@@ -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
|
|
@@ -1,48 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# AddAuth settings. Defaults are active; alternatives are commented out.
|
|
4
|
+
# Run the relevant generator and migrations before enabling a feature.
|
|
5
|
+
# Install alone enables no features. Existing initializers are never replaced.
|
|
2
6
|
AddAuth.configure do |config|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
# config.passwords_enabled =
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
# config.
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
# config.
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
# config.
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# config.
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
7
|
+
# Sign-in and rendering. HTML forms work in all browser modes.
|
|
8
|
+
config.passwords_enabled = true
|
|
9
|
+
# config.passwords_enabled = false # Use only other explicitly enabled methods.
|
|
10
|
+
config.turbo_enabled = true
|
|
11
|
+
# config.turbo_enabled = false # Ordinary JS navigation, with Turbo absent.
|
|
12
|
+
|
|
13
|
+
# Host business eligibility is checked alongside Core account policy.
|
|
14
|
+
config.eligible = ->(_user) { true }
|
|
15
|
+
# config.eligible = ->(user) { user.access_state == "active" }
|
|
16
|
+
|
|
17
|
+
# Browser sessions: bin/rails generate add_auth:session_upgrade
|
|
18
|
+
config.session.enabled = false
|
|
19
|
+
config.session.lifetime = 43_200 # 12 hours, absolute expiry.
|
|
20
|
+
config.session.idle_timeout = 1800 # 30 minutes; no greater than lifetime.
|
|
21
|
+
config.session.legacy_bridge_until = nil # Reject old Rails session-ID cookies.
|
|
22
|
+
# config.session.legacy_bridge_until = Time.iso8601("2030-01-01T00:00:00Z")
|
|
23
|
+
# Choose a short, fixed cutover deadline for a reviewed legacy-cookie bridge.
|
|
24
|
+
|
|
25
|
+
# Account lifecycle: bin/rails generate add_auth:accounts
|
|
26
|
+
# Password-only host: bin/rails generate add_auth:accounts --no-email-link
|
|
27
|
+
config.lifecycle.enabled = false
|
|
28
|
+
# config.lifecycle.enabled = true # After policy review and migrations.
|
|
29
|
+
config.lifecycle.confirmation_required = true
|
|
30
|
+
# config.lifecycle.confirmation_required = false
|
|
31
|
+
# false: password signup provisions and signs in immediately, without mail.
|
|
32
|
+
# confirmed_at stays nil. Requires the add_auth_provisioned_at migration.
|
|
33
|
+
config.lifecycle.reset_unconfirmed = false
|
|
34
|
+
# config.lifecycle.reset_unconfirmed = true # Requires optional confirmation.
|
|
35
|
+
# true: the stored mailbox holder may reset an unconfirmed password account
|
|
36
|
+
# after consuming an exact-address, single-use reset proof. No automatic login
|
|
37
|
+
# or confirmation. This does not trust the address for any other recovery.
|
|
38
|
+
# false: confirm the address before password reset, or use host support.
|
|
39
|
+
# Email changes always need fresh reauthentication and proof of the new address.
|
|
40
|
+
config.lifecycle.password_policy = ->(password) { password.length >= 12 && password.bytesize <= 72 }
|
|
41
|
+
config.lifecycle.eligible = ->(_user) { true } # Additional lifecycle eligibility.
|
|
42
|
+
config.lifecycle.profile_attributes = ->(_profile) { {} } # Accept no profile fields.
|
|
43
|
+
# config.lifecycle.profile_attributes = ->(profile) { profile.slice(:name, :consent) }
|
|
44
|
+
# Also allowlist those fields in the host registration controller. Never roles.
|
|
45
|
+
config.lifecycle.provision = ->(_user) {} # Local writes in the account transaction.
|
|
46
|
+
# config.lifecycle.provision = ->(user) { user.create_personal_workspace! }
|
|
47
|
+
# Raise on failure. Use the same DB pool; remote work needs a host outbox.
|
|
48
|
+
config.lifecycle.maximum_attempts = 20 # Failed-password threshold: 1..100.
|
|
49
|
+
config.lifecycle.unlock_in = 3600 # Timed lock: 60..86400 seconds.
|
|
50
|
+
config.lifecycle.proof_lifetime = 3600 # Account links: 60..86400 seconds.
|
|
51
|
+
config.lifecycle.remember_lifetime = 1_209_600 # 14 days, absolute expiry.
|
|
52
|
+
config.lifecycle.remember_idle_timeout = 604_800 # 7 days; <= remember_lifetime.
|
|
53
|
+
config.lifecycle.deletion_allowed = ->(_user) { true } # Host authorization/retention.
|
|
54
|
+
# config.lifecycle.deletion_allowed = ->(user) { user.invoices.none? }
|
|
55
|
+
config.lifecycle.delete_account = ->(user) { user.destroy! } # Same-DB local deletion.
|
|
56
|
+
|
|
57
|
+
# Email-link sign-in: bin/rails generate add_auth:email_link
|
|
58
|
+
# Account confirmation/reset mail does not require email-link sign-in.
|
|
59
|
+
config.email_link.enabled = false
|
|
60
|
+
config.email_link.token_lifetime = 1200 # 20 minutes.
|
|
61
|
+
config.email_link.same_browser = false
|
|
62
|
+
# config.email_link.same_browser = true # Require the requesting browser.
|
|
63
|
+
|
|
64
|
+
# Fresh proof for sensitive actions: bin/rails generate add_auth:step_up
|
|
65
|
+
# Use --no-email-link for password-only reauthentication.
|
|
66
|
+
config.step_up.enabled = false
|
|
67
|
+
config.step_up.fresh_for = 600 # Ordinary proof: 10 minutes.
|
|
68
|
+
config.step_up.strong_for = 300 # Strong proof: 5 minutes.
|
|
69
|
+
config.step_up.purposes = {} # Built-in feature purposes are supplied by Core.
|
|
70
|
+
# config.step_up.purposes = {
|
|
71
|
+
# export_data: {methods: [:password], return_to: "/exports/new", label: "export your data"}
|
|
72
|
+
# }
|
|
73
|
+
# Recheck elevation and host authorization inside the actual mutation.
|
|
74
|
+
|
|
75
|
+
# Passkeys: bin/rails generate add_auth:passkeys
|
|
76
|
+
config.passkeys.enabled = false
|
|
77
|
+
config.passkeys.rp_id = nil # Required: stable deployment RP ID.
|
|
78
|
+
# config.passkeys.rp_id = "example.com"
|
|
79
|
+
config.passkeys.origins = [] # Required: exact trusted origins, never request Host.
|
|
80
|
+
# config.passkeys.origins = ["https://app.example.com"]
|
|
81
|
+
config.passkeys.name = "Your account"
|
|
82
|
+
config.passkeys.anonymous_limit = 1000 # Shared ceremony budget per five minutes.
|
|
83
|
+
config.trusted_recovery_address = ->(_user) {} # No trusted recovery by default.
|
|
84
|
+
# config.trusted_recovery_address = ->(user) { user.email_address if user.confirmed_at }
|
|
85
|
+
# Lifecycle additionally rejects unconfirmed addresses as trusted recovery.
|
|
86
|
+
config.support_url = nil # Required before strict-policy activation.
|
|
87
|
+
# config.support_url = "/support" # Document your host-owned recovery process.
|
|
88
|
+
|
|
89
|
+
# Security notices: bin/rails generate add_auth:notifications
|
|
90
|
+
config.notifications.enabled = false
|
|
91
|
+
# Mail/queue settings also serve account proofs and optional email sign-in.
|
|
92
|
+
config.base_url = nil # Required for links: fixed trusted HTTPS origin.
|
|
93
|
+
# config.base_url = "https://app.example.com"
|
|
94
|
+
config.mail_from = nil # Required for mail; configure SMTP in Rails as usual.
|
|
95
|
+
# config.mail_from = "Your app <sign-in@example.com>"
|
|
96
|
+
# Configure a durable Active Job adapter and run add_auth:deliver_pending each minute.
|
|
97
|
+
config.rate_limit_store = nil # Defaults to Rails.cache; shared atomic counters required.
|
|
98
|
+
# config.rate_limit_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV.fetch("AUTH_REDIS_URL"))
|
|
99
|
+
# Solid Cache can serve application caching, but cannot serve these counters.
|
|
100
|
+
config.maintenance.batch_size = 100 # Maximum rows per operation/model: 1..1000.
|
|
101
|
+
config.maintenance.session_retention = nil # Keep history until explicitly selected.
|
|
102
|
+
config.maintenance.email_retention = nil
|
|
103
|
+
config.maintenance.notification_retention = nil
|
|
104
|
+
config.maintenance.account_retention = nil
|
|
105
|
+
# config.maintenance.session_retention = 604_800 # 7 days, seconds.
|
|
106
|
+
# config.maintenance.email_retention = 604_800
|
|
107
|
+
# config.maintenance.notification_retention = 2_592_000 # 30 days.
|
|
108
|
+
# config.maintenance.account_retention = 604_800
|
|
109
|
+
|
|
110
|
+
# Captcha: bin/rails generate add_auth:challenge (provider-specific setup).
|
|
111
|
+
config.challenge = AddAuth::Core::Challenge::Null.new
|
|
112
|
+
config.challenge_on = [] # Available: sign_in, email_link, reauthenticate,
|
|
113
|
+
# passkey_enrollment, register, account_request, provider.
|
|
114
|
+
# config.challenge_on = [:sign_in, :register, :account_request]
|
|
115
|
+
config.challenge_when_unavailable = :closed
|
|
116
|
+
# config.challenge_when_unavailable = :open # Explicit outage bypass; emits an event.
|
|
117
|
+
# Replace challenge with a configured Turnstile or Recaptcha adapter. Keep
|
|
118
|
+
# secrets in Rails credentials/environment, and configure expected hostnames.
|
|
119
|
+
|
|
120
|
+
# Optional providers: bin/rails generate add_auth:external_identities
|
|
121
|
+
config.external_identities.enabled = false
|
|
122
|
+
# Register only reviewed Core provider configurations through
|
|
123
|
+
# config.external_identities.register(id:, label:, middleware_name:, configuration:,
|
|
124
|
+
# apple_form_post: false, reauthentication: false)
|
|
125
|
+
# or config.external_identities.register_native(configuration:).
|
|
126
|
+
# The generated add_auth_providers.rb explains provider-library middleware,
|
|
127
|
+
# verifier and credential setup. It remains commented out until reviewed.
|
|
128
|
+
|
|
129
|
+
# Native sessions: bin/rails generate add_auth:mobile_sessions
|
|
130
|
+
config.mobile.enabled = false
|
|
131
|
+
config.mobile.lifetime = nil # Required when enabled; choose finite seconds.
|
|
132
|
+
config.mobile.idle_timeout = nil # Required; <= lifetime.
|
|
133
|
+
config.mobile.clients = [] # Explicit first-party client identifiers.
|
|
134
|
+
config.mobile.callbacks = {} # Reviewed callbacks per client, when using handoffs.
|
|
135
|
+
config.mobile.apple_providers = {} # Native Apple provider IDs per client.
|
|
136
|
+
# config.mobile.lifetime = 2_592_000 # Example: 30 days.
|
|
137
|
+
# config.mobile.idle_timeout = 1_209_600 # Example: 14 days.
|
|
138
|
+
# config.mobile.clients = %w[android ios]
|
|
139
|
+
|
|
140
|
+
# Presentation: eject with bin/rails generate add_auth:views.
|
|
141
|
+
config.stylesheet = "/add_auth.css"
|
|
142
|
+
# config.stylesheet = nil # Supply your own styling.
|
|
143
|
+
config.css_classes = {}
|
|
144
|
+
# config.css_classes = {input: "form-control", button: "btn btn-primary"}
|
|
145
|
+
|
|
146
|
+
# Password migration and cryptography: retain Rails' verifier by default.
|
|
147
|
+
config.legacy_password_verifier = nil
|
|
148
|
+
config.current_password_support = AddAuth::Core::Passwords::BcryptSupport.new
|
|
149
|
+
# A custom verifier must also declare current_password_support.available?(digest:).
|
|
150
|
+
# Rails configures digest_secret from its key generator automatically. Preserve it.
|
|
151
|
+
# Outside Rails only:
|
|
152
|
+
# config.digest_secret = -> { ENV.fetch("ADD_AUTH_DIGEST_SECRET") }
|
|
153
|
+
# Optional adapter overrides must implement digest(token) and matches?(digest, token):
|
|
154
|
+
# config.session_token_digest = MySessionDigest.new
|
|
155
|
+
# config.sign_in_token_digest = MyProofDigest.new
|
|
48
156
|
end
|
|
@@ -4,9 +4,9 @@ import { options, credential } from "/add_auth/codec.js"
|
|
|
4
4
|
|
|
5
5
|
export class PasskeyController extends Controller {
|
|
6
6
|
static targets = ["form", "controls", "unavailable", "status", "button"]
|
|
7
|
-
static values = { optionsUrl: String, finishUrl: String, mode: String, purpose: String, csrf: String, conditional: Boolean }
|
|
7
|
+
static values = { optionsUrl: String, finishUrl: String, mode: String, purpose: String, csrf: String, conditional: Boolean, turbo: { type: Boolean, default: true } }
|
|
8
8
|
connect() {
|
|
9
|
-
this.generation = 0
|
|
9
|
+
this.generation = (this.generation || 0) + 1
|
|
10
10
|
this.active = true
|
|
11
11
|
if (!window.PublicKeyCredential || !navigator.credentials || !window.isSecureContext) return
|
|
12
12
|
this.controlsTarget.hidden = false
|
|
@@ -25,13 +25,16 @@ export class PasskeyController extends Controller {
|
|
|
25
25
|
otherSubmission(event) { if (event.target !== this.formTarget) this.stop() }
|
|
26
26
|
disconnect() { this.active = false; this.stop() }
|
|
27
27
|
beforeCache() { this.active = false; this.stop(); this.controlsTarget.hidden = true; this.unavailableTarget.hidden = false; this.message("") }
|
|
28
|
+
restore(event) { if (event.persisted) this.connect() }
|
|
28
29
|
async run(conditional) {
|
|
29
30
|
this.stop()
|
|
30
31
|
const generation = this.generation
|
|
31
32
|
this.abort = new AbortController()
|
|
32
33
|
if (!conditional) { this.buttonTarget.disabled = true; this.message("Follow your browser’s passkey prompt.") }
|
|
33
34
|
try {
|
|
34
|
-
const
|
|
35
|
+
const form = new FormData(this.formTarget)
|
|
36
|
+
const challenge = form.get("challenge_token")
|
|
37
|
+
const remember = form.getAll("remember").at(-1)
|
|
35
38
|
const start = await this.post(this.optionsUrlValue, { purpose: this.purposeValue || null, challenge_token: challenge }, this.abort.signal)
|
|
36
39
|
this.formTarget.dispatchEvent(new CustomEvent("add_auth:proof-used"))
|
|
37
40
|
if (!this.current(generation)) return
|
|
@@ -42,7 +45,7 @@ export class PasskeyController extends Controller {
|
|
|
42
45
|
if (conditional) request.mediation = "conditional"
|
|
43
46
|
const proof = await navigator.credentials[create ? "create" : "get"](request)
|
|
44
47
|
if (!this.current(generation)) return
|
|
45
|
-
const result = await this.post(this.finishUrlValue, { transaction: this.transaction, credential: credential(proof) }, this.abort.signal)
|
|
48
|
+
const result = await this.post(this.finishUrlValue, { transaction: this.transaction, credential: credential(proof), remember }, this.abort.signal)
|
|
46
49
|
if (!this.current(generation)) return
|
|
47
50
|
this.transaction = null
|
|
48
51
|
this.navigate(result.redirect)
|
|
@@ -76,6 +79,6 @@ export class PasskeyController extends Controller {
|
|
|
76
79
|
}
|
|
77
80
|
current(generation) { return this.active && this.element.isConnected && this.generation === generation }
|
|
78
81
|
message(text) { this.statusTarget.textContent = text; this.statusTarget.hidden = !text }
|
|
79
|
-
navigate(path) { if (path) window.Turbo ? window.Turbo.visit(path) : window.location.assign(path) }
|
|
82
|
+
navigate(path) { if (path) (this.turboValue && window.Turbo) ? window.Turbo.visit(path) : window.location.assign(path) }
|
|
80
83
|
}
|
|
81
84
|
application.register("add-auth-passkey", PasskeyController)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module AddAuth
|
|
7
|
+
module Generators
|
|
8
|
+
class MobileSessionsGenerator < ::Rails::Generators::Base
|
|
9
|
+
include ::Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
13
|
+
|
|
14
|
+
def persistence
|
|
15
|
+
unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_mobile_sessions.rb")].any?
|
|
16
|
+
migration_template "add_add_auth_mobile_sessions.rb.tt", "db/migrate/add_add_auth_mobile_sessions.rb"
|
|
17
|
+
end
|
|
18
|
+
unless Dir[File.join(destination_root, "db/migrate/*_create_add_auth_mobile_handoffs.rb")].any?
|
|
19
|
+
migration_template "create_add_auth_mobile_handoffs.rb.tt", "db/migrate/create_add_auth_mobile_handoffs.rb"
|
|
20
|
+
end
|
|
21
|
+
copy_file "add_auth_mobile_handoff.rb", "app/models/add_auth_mobile_handoff.rb", skip: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def routes
|
|
25
|
+
source = File.read(File.join(destination_root, "config/routes.rb"))
|
|
26
|
+
return if source.include?("# AddAuth mobile sessions")
|
|
27
|
+
route <<~ROUTES
|
|
28
|
+
# AddAuth mobile sessions
|
|
29
|
+
post "mobile/session", to: "add_auth/mobile_sessions#create"
|
|
30
|
+
get "mobile/session", to: "add_auth/mobile_sessions#show"
|
|
31
|
+
delete "mobile/session", to: "add_auth/mobile_sessions#destroy"
|
|
32
|
+
get "mobile/sessions", to: "add_auth/mobile_sessions#index"
|
|
33
|
+
delete "mobile/sessions/:id", to: "add_auth/mobile_sessions#revoke"
|
|
34
|
+
post "mobile/sessions/revoke-all", to: "add_auth/mobile_sessions#revoke_all"
|
|
35
|
+
get "mobile/providers/:provider", to: "add_auth/provider_sign_ins#prepare", defaults: {flow: "mobile"}
|
|
36
|
+
post "mobile/handoff", to: "add_auth/mobile_sessions#exchange"
|
|
37
|
+
post "mobile/apple/challenge", to: "add_auth/mobile_sessions#apple_challenge"
|
|
38
|
+
post "mobile/apple/session", to: "add_auth/mobile_sessions#apple"
|
|
39
|
+
post "mobile/apple/enrollment", to: "add_auth/mobile_sessions#apple_enroll"
|
|
40
|
+
ROUTES
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configuration
|
|
44
|
+
copy_file "mobile.rb", "config/initializers/add_auth_mobile.rb", skip: true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddAddAuthMobileSessions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :sessions, :transport, :string, null: false, default: "browser"
|
|
4
|
+
add_column :sessions, :client_id, :string
|
|
5
|
+
add_column :sessions, :mobile_idle_timeout, :integer
|
|
6
|
+
add_check_constraint :sessions, "transport IN ('browser', 'mobile')", name: "add_auth_session_transport"
|
|
7
|
+
add_check_constraint :sessions, "transport != 'mobile' OR (client_id IS NOT NULL AND mobile_idle_timeout IS NOT NULL AND mobile_idle_timeout BETWEEN 60 AND 7776000 AND token_digest IS NOT NULL AND authenticated_at IS NOT NULL AND expires_at IS NOT NULL AND last_seen_at IS NOT NULL)", name: "add_auth_mobile_profile"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class CreateAddAuthMobileHandoffs < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
user_key = connection.columns(:users).find { |column| column.name == connection.primary_key(:users) }
|
|
4
|
+
raise "Unsupported authentication primary key" unless user_key && [:integer, :uuid].include?(user_key.type)
|
|
5
|
+
user_type = (user_key.type == :integer && user_key.limit == 8) ? :bigint : user_key.type
|
|
6
|
+
create_table :add_auth_mobile_handoffs do |t|
|
|
7
|
+
t.references :user, type: user_type, foreign_key: {on_delete: :cascade}
|
|
8
|
+
t.string :external_digest, null: false
|
|
9
|
+
t.string :digest
|
|
10
|
+
t.string :client_id, limit: 64, null: false
|
|
11
|
+
t.string :callback, limit: 2048, null: false
|
|
12
|
+
t.string :state, limit: 128, null: false
|
|
13
|
+
t.string :challenge_digest, null: false
|
|
14
|
+
t.string :credential_id
|
|
15
|
+
t.string :credential_version
|
|
16
|
+
t.integer :policy_version
|
|
17
|
+
t.datetime :authenticated_at
|
|
18
|
+
t.datetime :issued_at, null: false
|
|
19
|
+
t.datetime :expires_at, null: false
|
|
20
|
+
t.datetime :consumed_at
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
add_index :add_auth_mobile_handoffs, :external_digest, unique: true
|
|
24
|
+
add_index :add_auth_mobile_handoffs, :digest, unique: true
|
|
25
|
+
add_index :add_auth_mobile_handoffs, :expires_at
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Mobile sessions are optional and remain disabled after generating persistence.
|
|
2
|
+
# Review the host/client timeout policy before enabling. These are example values.
|
|
3
|
+
# AddAuth.configure do |config|
|
|
4
|
+
# config.mobile.lifetime = 30 * 86_400
|
|
5
|
+
# config.mobile.idle_timeout = 14 * 86_400
|
|
6
|
+
# config.mobile.clients = %w[android ios]
|
|
7
|
+
# config.mobile.enabled = true
|
|
8
|
+
# end
|
|
@@ -1,12 +1,14 @@
|
|
|
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 NotificationsGenerator < ::Rails::Generators::Base
|
|
9
10
|
include ::Rails::Generators::Migration
|
|
11
|
+
include FeatureConfiguration
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
12
14
|
def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
@@ -21,10 +23,7 @@ module AddAuth
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def configuration
|
|
24
|
-
|
|
25
|
-
unless File.read(File.join(destination_root, path)).include?("config.notifications.enabled = true")
|
|
26
|
-
append_to_file path, "\nAddAuth.configure do |config|\n config.notifications.enabled = true\nend\n"
|
|
27
|
-
end
|
|
26
|
+
enable_feature(:notifications)
|
|
28
27
|
say "Migrate, configure mail_from and a durable queue, and schedule add_auth:deliver_pending every minute."
|
|
29
28
|
end
|
|
30
29
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
require "add_auth/rails/migration/key_type"
|
|
2
|
+
|
|
1
3
|
class CreateAddAuthSecurityEvents < ActiveRecord::Migration[8.0]
|
|
2
4
|
def change
|
|
3
5
|
create_table :add_auth_security_events do |t|
|
|
4
|
-
t.references :user, foreign_key: {on_delete: :nullify}
|
|
6
|
+
t.references :user, type: AddAuth::Rails::Migration::KeyType.for(connection, :users), foreign_key: {on_delete: :nullify}
|
|
5
7
|
t.string :kind, null: false
|
|
6
8
|
t.string :digest, null: false
|
|
7
9
|
t.text :delivery_payload
|