add_auth 0.2.2 → 0.3.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 +22 -0
- data/README.md +38 -14
- data/ROADMAP.md +87 -38
- data/SECURITY.md +6 -0
- data/app/controllers/add_auth/accounts_controller.rb +138 -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/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 +14 -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 +251 -0
- data/lib/add_auth/core/account_policy.rb +50 -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 +5 -0
- data/lib/add_auth/rails/doctor.rb +70 -3
- 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 +170 -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 +96 -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 +52 -0
- data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +49 -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/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/install/templates/initializer.rb +4 -0
- 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/templates/create_add_auth_security_events.rb.tt +3 -1
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +7 -3
- 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 +85 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Rails
|
|
5
|
+
module ProviderLibraries
|
|
6
|
+
# Mint the form token with the actual maintained OmniAuth validator. A
|
|
7
|
+
# Rails adopter may already configure its session key; using a separate
|
|
8
|
+
# default Rack validator would generate a token that cannot be accepted.
|
|
9
|
+
module RequestProtection
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def validator
|
|
13
|
+
return unless defined?(::OmniAuth::AuthenticityTokenProtection)
|
|
14
|
+
require "rack/protection"
|
|
15
|
+
phase = ::OmniAuth.config.request_validation_phase
|
|
16
|
+
candidate = (phase.is_a?(Class) && phase <= ::Rack::Protection::AuthenticityToken) ? phase.new : phase
|
|
17
|
+
candidate if candidate.is_a?(::Rack::Protection::AuthenticityToken) || rails_validator?(candidate)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def token(session:, rails_token: nil)
|
|
21
|
+
configured = validator
|
|
22
|
+
raise AddAuth::Error, "configure a supported OmniAuth request validator" unless configured
|
|
23
|
+
if rails_validator?(configured)
|
|
24
|
+
raise AddAuth::Error, "Rails request token is unavailable" unless rails_token.respond_to?(:call)
|
|
25
|
+
return rails_token.call
|
|
26
|
+
end
|
|
27
|
+
configured.mask_authenticity_token(session)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parameter
|
|
31
|
+
configured = validator
|
|
32
|
+
raise AddAuth::Error, "configure a supported OmniAuth request validator" unless configured
|
|
33
|
+
rails_validator?(configured) ? ::ActionController::Base.request_forgery_protection_token : configured.options.fetch(:authenticity_param)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rails_validator?(candidate)
|
|
37
|
+
defined?(::OmniAuth::RailsCsrfProtection::TokenVerifier) && candidate.is_a?(::OmniAuth::RailsCsrfProtection::TokenVerifier)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :rails_validator?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -14,10 +14,147 @@ module AddAuth
|
|
|
14
14
|
|
|
15
15
|
def config = AddAuth.configuration
|
|
16
16
|
|
|
17
|
+
def account_policy
|
|
18
|
+
Core::AccountPolicy.new(enabled: config.lifecycle.enabled, eligible: config.lifecycle.eligible)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def eligible(user)
|
|
22
|
+
config.eligible.call(user) == true && account_policy.allowed?(user)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def authority
|
|
26
|
+
require "add_auth/rails/stores/authority"
|
|
27
|
+
tokens = []
|
|
28
|
+
tokens << ::AddAuthSignInToken if defined?(::AddAuthSignInToken) && ::AddAuthSignInToken.table_exists?
|
|
29
|
+
tokens << ::AddAuthAccountToken if defined?(::AddAuthAccountToken) && ::AddAuthAccountToken.table_exists?
|
|
30
|
+
ceremonies = []
|
|
31
|
+
ceremonies << ::AddAuthCeremony if defined?(::AddAuthCeremony) && ::AddAuthCeremony.table_exists?
|
|
32
|
+
ceremonies << ::AddAuthExternalTransaction if defined?(::AddAuthExternalTransaction) && ::AddAuthExternalTransaction.table_exists?
|
|
33
|
+
ceremonies << ::AddAuthMobileHandoff if defined?(::AddAuthMobileHandoff) && ::AddAuthMobileHandoff.table_exists?
|
|
34
|
+
invalidator = if defined?(::AddAuthExternalIdentity) && ::AddAuthExternalIdentity.table_exists?
|
|
35
|
+
->(user_id:, at:) { Core::ExternalIdentities.invalidate_credentials_in_transaction(store: external_identity_store, user_id: user_id, at: at) }
|
|
36
|
+
end
|
|
37
|
+
Stores::Authority.new(user_model: ::User, session_model: ::Session, token_models: tokens, ceremony_models: ceremonies, external_invalidator: invalidator)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def external_identity_store
|
|
41
|
+
require "add_auth/rails/stores/external_identities"
|
|
42
|
+
Stores::ExternalIdentities.new(user_model: ::User, identity_model: ::AddAuthExternalIdentity, transaction_model: ::AddAuthExternalTransaction)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def external_provider(id)
|
|
46
|
+
config.external_identities.provider(id) if config.external_identities.enabled
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def external_reauthentication? = config.external_identities.enabled && config.external_identities.providers.any?(&:reauthentication?)
|
|
50
|
+
|
|
51
|
+
def external_identities
|
|
52
|
+
options = config.external_identities
|
|
53
|
+
raise AddAuth::Error, "enable external identities and register a provider first" unless options.enabled && options.configurations.any?
|
|
54
|
+
Core::ExternalIdentities.new(store: external_identity_store, configurations: options.configurations,
|
|
55
|
+
sessions: sessions, access_policy: access_policy, policy: step_up_policy, eligible: method(:eligible),
|
|
56
|
+
digest: config.sign_in_token_digest, revoke_authority: authority.method(:revoke),
|
|
57
|
+
remaining_method: remaining_factors,
|
|
58
|
+
enrollment_eligible: ->(user) { account_policy.allowed?(user, purpose: :confirm) }, enabled: true, mobile_enabled: config.mobile.enabled)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def remaining_factors
|
|
62
|
+
require "add_auth/core/remaining_factors"
|
|
63
|
+
require "add_auth/core/passwords/credential"
|
|
64
|
+
credential = Core::Passwords::Credential.new(legacy_verifier: config.legacy_password_verifier)
|
|
65
|
+
Core::RemainingFactors.new(access_policy: access_policy,
|
|
66
|
+
passkey_count: ->(user) { (defined?(::AddAuthCredential) && ::AddAuthCredential.table_exists?) ? ::AddAuthCredential.where(user_id: user.id, revoked_at: nil).count : 0 },
|
|
67
|
+
password_available: ->(user) { credential.available?(user: user, current: config.current_password_support) })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def provider_enrollment
|
|
71
|
+
require "add_auth/rails/provider_enrollment"
|
|
72
|
+
ProviderEnrollment.new(model: ::AddAuthExternalTransaction, digest: config.sign_in_token_digest, key: key("external-enrollment"))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def accounts
|
|
76
|
+
raise AddAuth::Error, "enable account lifecycle first" unless config.lifecycle.enabled
|
|
77
|
+
require "add_auth/rails/stores/account_tokens"
|
|
78
|
+
Core::AccountLifecycle.new(store: Stores::AccountTokens.new(user_model: ::User, token_model: ::AddAuthAccountToken,
|
|
79
|
+
session_model: ::Session, address_model: ::AddAuthAddressClaim, authority: authority, provision: config.lifecycle.provision, delete_account: config.lifecycle.delete_account),
|
|
80
|
+
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: config.trusted_recovery_address,
|
|
82
|
+
lifetime: config.lifecycle.proof_lifetime, notify: ->(**event) { security_events.issue(**event) if config.notifications.enabled },
|
|
83
|
+
sessions: sessions, step_up_policy: step_up_policy, profile_attributes: config.lifecycle.profile_attributes, deletion_allowed: config.lifecycle.deletion_allowed,
|
|
84
|
+
external_identities: -> { external_identities })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def enqueue_account_request(identifier:, purpose:)
|
|
88
|
+
job = ::AddAuth::AccountRequestJob.perform_later(encrypt_intake(identifier: identifier, purpose: purpose))
|
|
89
|
+
raise AddAuth::Error, "account request queue unavailable" unless job
|
|
90
|
+
job
|
|
91
|
+
rescue
|
|
92
|
+
raise AddAuth::Error, "account request queue unavailable", cause: nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def account_proof_url(token, purpose:)
|
|
96
|
+
raise AddAuth::Error, "unsupported account proof" unless Core::AccountLifecycle::PURPOSES.include?(purpose)
|
|
97
|
+
authentication_url("/account/proofs/#{purpose}", token: token)
|
|
98
|
+
end
|
|
99
|
+
|
|
17
100
|
def sessions
|
|
18
101
|
Core::Sessions.new(store: Stores::Sessions.new(user_model: ::User, session_model: ::Session),
|
|
19
|
-
digest: config.session_token_digest, eligible:
|
|
20
|
-
idle_timeout: config.session.idle_timeout, legacy_bridge_until: config.session.legacy_bridge_until, access_policy: access_policy
|
|
102
|
+
digest: config.session_token_digest, eligible: method(:eligible), lifetime: config.session.lifetime,
|
|
103
|
+
idle_timeout: config.session.idle_timeout, legacy_bridge_until: config.session.legacy_bridge_until, access_policy: access_policy,
|
|
104
|
+
password_lifecycle: password_lifecycle,
|
|
105
|
+
mobile_profile: mobile_profile,
|
|
106
|
+
verified_denial: ->(user) { (config.eligible.call(user) == true) ? account_policy.denial(user) : :disabled },
|
|
107
|
+
remembered_profile: config.lifecycle.enabled ? {lifetime: config.lifecycle.remember_lifetime, idle_timeout: config.lifecycle.remember_idle_timeout} : nil,
|
|
108
|
+
on_sign_in: method(:record_sign_in))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mobile_profile
|
|
112
|
+
options = config.mobile
|
|
113
|
+
return unless options.enabled
|
|
114
|
+
raise AddAuth::Error, "enable session_upgrade before mobile sessions" unless config.session.enabled
|
|
115
|
+
Core::MobileProfile.new(lifetime: options.lifetime, idle_timeout: options.idle_timeout, clients: options.clients, callbacks: options.callbacks)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mobile_authentication
|
|
119
|
+
require "add_auth/core/mobile_authentication"
|
|
120
|
+
Core::MobileAuthentication.new(sessions: sessions, profile: mobile_profile,
|
|
121
|
+
intake: intake, verify_password: method(:authenticate_password))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def mobile_handoffs
|
|
125
|
+
require "add_auth/core/mobile_handoffs"
|
|
126
|
+
require "add_auth/rails/stores/mobile_handoffs"
|
|
127
|
+
Core::MobileHandoffs.new(store: Stores::MobileHandoffs.new(user_model: ::User, handoff_model: ::AddAuthMobileHandoff, session_model: ::Session),
|
|
128
|
+
profile: mobile_profile, sessions: sessions, access_policy: access_policy, digest: config.sign_in_token_digest)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def native_apple
|
|
132
|
+
require "add_auth/core/native_authentication"
|
|
133
|
+
require "add_auth/rails/provider_libraries/apple_native"
|
|
134
|
+
require "add_auth/rails/provider_libraries/apple_jwks"
|
|
135
|
+
raise AddAuth::Error, "install and require ruby-jwt for native Apple authentication" unless defined?(::JWT)
|
|
136
|
+
raise AddAuth::Error, "configure native Apple providers as an explicit client map" unless config.mobile.apple_providers.is_a?(Hash)
|
|
137
|
+
providers = config.mobile.apple_providers.transform_values { |id| config.external_identities.native_provider(id) }
|
|
138
|
+
Core::NativeAuthentication.new(external_identities: external_identities, profile: mobile_profile,
|
|
139
|
+
providers: providers, intake: intake, accounts: (method(:accounts) if config.lifecycle.enabled), verify: ->(**arguments) {
|
|
140
|
+
ProviderLibraries::AppleNative::CallbackResult.capture(**arguments, jwks: ProviderLibraries::AppleJwks.new(cache: ::Rails.cache))
|
|
141
|
+
})
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def record_sign_in(user:, session:, at:)
|
|
145
|
+
require "add_auth/rails/stores/commit_dispatch"
|
|
146
|
+
Stores::CommitDispatch.event(transaction: ::User.current_transaction, event: "session_created.add_auth",
|
|
147
|
+
payload: {user_id: user.id, session_id: session.id, method: session.authenticated_with, occurred_at: at})
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def password_lifecycle
|
|
151
|
+
return unless config.lifecycle.enabled || ::User.column_names.include?("add_auth_password_scheme")
|
|
152
|
+
require "add_auth/core/passwords/lifecycle"
|
|
153
|
+
require "add_auth/rails/stores/account_credentials"
|
|
154
|
+
Core::Passwords::Lifecycle.new(store: Stores::AccountCredentials.new(user_model: ::User, authority: authority),
|
|
155
|
+
policy: method(:eligible), enabled: config.lifecycle.enabled,
|
|
156
|
+
maximum_attempts: config.lifecycle.maximum_attempts, unlock_in: config.lifecycle.unlock_in,
|
|
157
|
+
issue_unlock: ->(user) { accounts.issue_unlock_in_transaction(user: user) })
|
|
21
158
|
end
|
|
22
159
|
|
|
23
160
|
def security_events
|
|
@@ -31,7 +168,9 @@ module AddAuth
|
|
|
31
168
|
Core::AccessPolicy.new(credentials: ->(id) {
|
|
32
169
|
::AddAuthCredential.find_by(external_id: id) if defined?(::AddAuthCredential) && ::AddAuthCredential.table_exists?
|
|
33
170
|
}, passkeys_enabled: config.passkeys.enabled, email_enabled: Core::Intake.email_available?(config),
|
|
34
|
-
trusted_recovery_address: config.trusted_recovery_address, password_enabled: config.passwords_enabled
|
|
171
|
+
trusted_recovery_address: config.trusted_recovery_address, password_enabled: config.passwords_enabled,
|
|
172
|
+
external_enabled: config.external_identities.enabled,
|
|
173
|
+
external_current: ->(**arguments) { external_identities.credential_current?(**arguments) })
|
|
35
174
|
end
|
|
36
175
|
|
|
37
176
|
def email(purpose: :sign_in)
|
|
@@ -41,7 +180,7 @@ module AddAuth
|
|
|
41
180
|
raise AddAuth::Error, "enable session_upgrade and email_link first" unless Core::Intake.email_available?(config)
|
|
42
181
|
Core::Strategies::EmailLink.new(store: Stores::EmailTokens.new(user_model: ::User,
|
|
43
182
|
token_model: ::AddAuthSignInToken, session_model: ::Session), digest: config.sign_in_token_digest,
|
|
44
|
-
delivery_cipher: DeliveryCipher.new(key: key("delivery")), eligible:
|
|
183
|
+
delivery_cipher: DeliveryCipher.new(key: key("delivery")), eligible: method(:eligible),
|
|
45
184
|
normalize_identifier: method(:normalize), identifier_for: ->(user) { user.email_address },
|
|
46
185
|
token_lifetime: {"reauthentication" => 300, "recovery" => 1200}.fetch(purpose.to_s, config.email_link.token_lifetime),
|
|
47
186
|
same_browser: (purpose.to_s == "recovery") ? false : config.email_link.same_browser, purpose: purpose,
|
|
@@ -82,7 +221,23 @@ module AddAuth
|
|
|
82
221
|
else
|
|
83
222
|
{}
|
|
84
223
|
end
|
|
85
|
-
|
|
224
|
+
if config.lifecycle.enabled
|
|
225
|
+
defaults = defaults.merge(
|
|
226
|
+
change_email: {methods: [:password, :email_link, :passkey], return_to: "/account/email", label: "change your email address"},
|
|
227
|
+
change_password: {methods: [:password, :email_link, :passkey], return_to: "/account/password", label: "change your password"},
|
|
228
|
+
delete_account: {methods: [:password, :email_link, :passkey], return_to: "/account/delete", label: "delete your account"}
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
if config.external_identities.enabled
|
|
232
|
+
defaults = defaults.merge(
|
|
233
|
+
link_external_identity: {methods: [:password, :email_link, :passkey], return_to: "/account/external-identities", label: "link a sign-in provider"},
|
|
234
|
+
unlink_external_identity: {methods: [:password, :email_link, :passkey], return_to: "/account/external-identities", label: "remove a sign-in provider"}
|
|
235
|
+
)
|
|
236
|
+
defaults = defaults.transform_values do |rule|
|
|
237
|
+
(!external_reauthentication? || rule[:require_passkey] || rule[:reauthentication] == false) ? rule : rule.merge(methods: (rule[:methods] + [:external_identity]).uniq)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
{sign_out_everywhere: {methods: [:password, :email_link, :passkey, *(:external_identity if external_reauthentication?)], return_to: "/sessions/revoke-all", label: "sign out everywhere"}}.merge(defaults).merge(config.step_up.purposes)
|
|
86
241
|
end
|
|
87
242
|
|
|
88
243
|
def passkeys
|
|
@@ -91,7 +246,7 @@ module AddAuth
|
|
|
91
246
|
Core::Strategies::Passkey.new(store: Stores::Passkeys.new(user_model: ::User, session_model: ::Session,
|
|
92
247
|
credential_model: ::AddAuthCredential, ceremony_model: ::AddAuthCeremony, token_model: ::AddAuthSignInToken), sessions: sessions,
|
|
93
248
|
policy: step_up_policy, access_policy: access_policy, digest: config.sign_in_token_digest,
|
|
94
|
-
eligible:
|
|
249
|
+
eligible: method(:eligible), rp_id: config.passkeys.rp_id, origins: config.passkeys.origins, name: config.passkeys.name,
|
|
95
250
|
limiter: method(:limit), anonymous_limit: config.passkeys.anonymous_limit,
|
|
96
251
|
notify: ->(**event) { security_events.issue(**event) }, allow_localhost: !::Rails.env.production?, support_url: config.support_url,
|
|
97
252
|
on_failure: ->(reason) { ::ActiveSupport::Notifications.instrument("passkey_failure.add_auth", reason: reason) })
|
|
@@ -103,6 +258,7 @@ module AddAuth
|
|
|
103
258
|
password_version: ->(user) { config.sign_in_token_digest.digest("password:#{user.password_digest}") },
|
|
104
259
|
credential_current: ->(user:, evidence:) {
|
|
105
260
|
evidence.method == :password ||
|
|
261
|
+
(evidence.method == :external_identity && access_policy.external_credential_current?(user: user, id: evidence.credential_id, version: evidence.credential_version)) ||
|
|
106
262
|
(evidence.method == :passkey && access_policy.credential_current?(user: user, id: evidence.credential_id) &&
|
|
107
263
|
::AddAuthCredential.find_by(external_id: evidence.credential_id)&.public_key == evidence.credential_version) ||
|
|
108
264
|
(evidence.method == :email_link && Core::Intake.email_available?(config) &&
|
|
@@ -204,14 +360,20 @@ module AddAuth
|
|
|
204
360
|
end
|
|
205
361
|
|
|
206
362
|
def sign_in_url(token, purpose: "sign_in")
|
|
363
|
+
path = {"sign_in" => "/sign-in/link", "reauthentication" => "/reauthenticate/link", "recovery" => "/recover/link"}.fetch(purpose.to_s)
|
|
364
|
+
authentication_url(path, token: token)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def authentication_url(path, **query)
|
|
207
368
|
uri = URI.parse(config.base_url.to_s)
|
|
208
369
|
valid = uri.host && !uri.userinfo && !uri.query && !uri.fragment && ["", "/"].include?(uri.path) &&
|
|
209
370
|
(uri.scheme == "https" || (!::Rails.env.production? && uri.scheme == "http"))
|
|
210
371
|
unless valid
|
|
211
372
|
raise AddAuth::Error, "configure base_url as a fixed HTTPS origin (HTTP allowed only outside production)"
|
|
212
373
|
end
|
|
213
|
-
|
|
214
|
-
|
|
374
|
+
"#{uri.to_s.delete_suffix("/")}#{path}?#{URI.encode_www_form(query)}"
|
|
375
|
+
rescue URI::InvalidURIError
|
|
376
|
+
raise AddAuth::Error, "configure a fixed HTTPS origin", cause: nil
|
|
215
377
|
end
|
|
216
378
|
end
|
|
217
379
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Rails
|
|
5
|
+
module Stores
|
|
6
|
+
class AccountCredentials
|
|
7
|
+
def initialize(user_model:, authority:)
|
|
8
|
+
@users, @authority = user_model, authority
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def update_account(user:, **attributes)
|
|
12
|
+
raise AddAuth::Error, "credential changes require an account transaction" unless @users.connection.transaction_open?
|
|
13
|
+
user.update_columns(attributes)
|
|
14
|
+
user.reload
|
|
15
|
+
rescue ActiveRecord::RecordNotUnique
|
|
16
|
+
raise Core::AccountLifecycle::Conflict
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def replace_password(user:, password:)
|
|
20
|
+
raise AddAuth::Error, "credential changes require an account transaction" unless @users.connection.transaction_open?
|
|
21
|
+
user.password = password
|
|
22
|
+
unless user.valid?
|
|
23
|
+
user.reload
|
|
24
|
+
raise Core::AccountLifecycle::InvalidPassword
|
|
25
|
+
end
|
|
26
|
+
persist_password(user: user, digest: user.password_digest)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rehash_password(user:, password:)
|
|
30
|
+
fresh = @users.new(password: password)
|
|
31
|
+
persist_password(user: user, digest: fresh.password_digest)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def revoke_authority(user:, at:) = @authority.revoke(user_id: user.id, at: at)
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def persist_password(user:, digest:)
|
|
39
|
+
update_account(user: user, password_digest: digest, add_auth_password_scheme: "rails")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -20,13 +20,7 @@ module AddAuth
|
|
|
20
20
|
transaction.after_commit { completed = true }
|
|
21
21
|
# Opposing account switches acquire their locks in the same order.
|
|
22
22
|
accounts = [id, additional_id].compact.uniq.sort_by(&:to_s).map do |account_id|
|
|
23
|
-
|
|
24
|
-
pk = @users.connection.quote_column_name(@users.primary_key)
|
|
25
|
-
@users.where(@users.primary_key => account_id).update_all("#{pk} = #{pk}")
|
|
26
|
-
@users.find_by(@users.primary_key => account_id)
|
|
27
|
-
else
|
|
28
|
-
@users.lock.find_by(@users.primary_key => account_id)
|
|
29
|
-
end
|
|
23
|
+
current_in_transaction(id: account_id)
|
|
30
24
|
end
|
|
31
25
|
result = yield accounts.compact.find { |account| account.id == id }
|
|
32
26
|
end
|
|
@@ -34,6 +28,21 @@ module AddAuth
|
|
|
34
28
|
raise AddAuth::Error, "authentication transaction rolled back" unless completed
|
|
35
29
|
result
|
|
36
30
|
end
|
|
31
|
+
|
|
32
|
+
# For model save callbacks, whose Active Record transaction already owns
|
|
33
|
+
# the write. All actual row-lock acquisition lives in this adapter.
|
|
34
|
+
def current_in_transaction(id:)
|
|
35
|
+
raise AddAuth::Error, "account row locking requires an owning transaction" unless @users.connection.transaction_open?
|
|
36
|
+
@users.uncached do
|
|
37
|
+
if @users.connection.adapter_name == "SQLite"
|
|
38
|
+
pk = @users.connection.quote_column_name(@users.primary_key)
|
|
39
|
+
@users.unscoped.where(@users.primary_key => id).update_all("#{pk} = #{pk}")
|
|
40
|
+
@users.unscoped.find_by(@users.primary_key => id)
|
|
41
|
+
else
|
|
42
|
+
@users.unscoped.lock.find_by(@users.primary_key => id)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
37
46
|
end
|
|
38
47
|
end
|
|
39
48
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "add_auth/rails/stores/email_tokens"
|
|
4
|
+
require "add_auth/rails/stores/commit_dispatch"
|
|
5
|
+
require "add_auth/rails/stores/account_credentials"
|
|
6
|
+
|
|
7
|
+
module AddAuth
|
|
8
|
+
module Rails
|
|
9
|
+
module Stores
|
|
10
|
+
class AccountTokens < EmailTokens
|
|
11
|
+
def initialize(user_model:, token_model:, session_model:, address_model:, authority:, provision:, delete_account: ->(user) { user.destroy! })
|
|
12
|
+
super(user_model: user_model, token_model: token_model, session_model: session_model)
|
|
13
|
+
raise ArgumentError, "address claims must share the account connection pool" unless address_model.connection_pool.equal?(user_model.connection_pool)
|
|
14
|
+
@addresses, @authority, @provision = address_model, authority, provision
|
|
15
|
+
@delete_account = delete_account
|
|
16
|
+
@account_credentials = AccountCredentials.new(user_model: user_model, authority: authority)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_account(email:, password:, profile: {})
|
|
20
|
+
raise AddAuth::Error, "registration must own its transaction" if @users.connection.transaction_open?
|
|
21
|
+
completed = false
|
|
22
|
+
@users.transaction do |transaction|
|
|
23
|
+
transaction.after_commit { completed = true }
|
|
24
|
+
user = @users.new(profile)
|
|
25
|
+
user.email_address = email
|
|
26
|
+
user.password = password
|
|
27
|
+
user.add_auth_authority = "add_auth"
|
|
28
|
+
user.save!
|
|
29
|
+
yield user
|
|
30
|
+
end
|
|
31
|
+
raise AddAuth::Error, "registration transaction rolled back" unless completed
|
|
32
|
+
:created
|
|
33
|
+
rescue ActiveRecord::RecordNotUnique, Core::AccountLifecycle::Conflict
|
|
34
|
+
:duplicate
|
|
35
|
+
rescue ActiveRecord::RecordInvalid => error
|
|
36
|
+
(error.record.errors.attribute_names - [:email_address]).empty? ? :duplicate : :invalid
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def replace_pending(**attributes)
|
|
40
|
+
raise AddAuth::Error, "account proof must join its owning transaction" unless @users.current_transaction.open?
|
|
41
|
+
record = super
|
|
42
|
+
CommitDispatch.enqueue(transaction: @users.current_transaction, job: ::AddAuth::AccountDeliveryJob,
|
|
43
|
+
id: record.id, failure_event: "account_enqueue_failed.add_auth", payload: {issuance_id: record.id})
|
|
44
|
+
record
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def claim_address(user:, digest:, address:, state:)
|
|
48
|
+
raise Core::AccountLifecycle::Conflict if @users.where(email_address: address).where.not(id: user.id).exists?
|
|
49
|
+
@addresses.where(user_id: user.id, state: state).where.not(digest: digest).delete_all
|
|
50
|
+
existing = @addresses.find_by(digest: digest)
|
|
51
|
+
if existing
|
|
52
|
+
raise Core::AccountLifecycle::Conflict unless existing.user_id == user.id && existing.state == state
|
|
53
|
+
else
|
|
54
|
+
@addresses.create!(user_id: user.id, digest: digest, state: state)
|
|
55
|
+
end
|
|
56
|
+
rescue ActiveRecord::RecordNotUnique
|
|
57
|
+
raise Core::AccountLifecycle::Conflict
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def promote_address(user:, digest:)
|
|
61
|
+
pending = @addresses.find_by!(user_id: user.id, digest: digest, state: "pending")
|
|
62
|
+
@addresses.where(user_id: user.id, state: "current").delete_all
|
|
63
|
+
pending.update!(state: "current")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inspect_token(digest:)
|
|
67
|
+
@tokens.uncached do
|
|
68
|
+
record = @tokens.find_by(digest: digest)
|
|
69
|
+
[record && @users.find_by(id: record.user_id), record]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def update_account(user:, **attributes)
|
|
74
|
+
# Core owns notices and invalidation for this transition. Avoid firing
|
|
75
|
+
# a second independent model callback for the same committed change.
|
|
76
|
+
@account_credentials.update_account(user: user, **attributes)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def replace_password(user:, password:)
|
|
80
|
+
@account_credentials.replace_password(user: user, password: password)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def revoke_authority(user:, at:) = @authority.revoke(user_id: user.id, at: at)
|
|
84
|
+
def provision(user:) = @provision.call(user)
|
|
85
|
+
|
|
86
|
+
def delete_account(user:)
|
|
87
|
+
raise AddAuth::Error, "account deletion requires its owning transaction" unless @users.connection.transaction_open?
|
|
88
|
+
@delete_account.call(user)
|
|
89
|
+
raise Core::AccountLifecycle::DeletionRejected if @users.unscoped.exists?(id: user.id)
|
|
90
|
+
rescue ActiveRecord::RecordNotDestroyed, ActiveRecord::InvalidForeignKey
|
|
91
|
+
raise Core::AccountLifecycle::DeletionRejected, cause: nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Rails
|
|
5
|
+
module Stores
|
|
6
|
+
# One persistence implementation for retiring outstanding account authority.
|
|
7
|
+
# Core chooses the event, account and time; callers own the account lock.
|
|
8
|
+
class Authority
|
|
9
|
+
def initialize(user_model:, session_model:, token_models: [], ceremony_models: [], credential_models: [], external_invalidator: nil)
|
|
10
|
+
@external_invalidator = external_invalidator
|
|
11
|
+
@users, @sessions, @tokens, @ceremonies, @credentials = user_model, session_model, token_models, ceremony_models, credential_models
|
|
12
|
+
unless [session_model, *token_models, *ceremony_models, *credential_models].all? { |model| model.connection_pool.equal?(user_model.connection_pool) }
|
|
13
|
+
raise ArgumentError, "authentication models must share one connection pool"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def revoke(user_id:, at:)
|
|
18
|
+
raise AddAuth::Error, "authority revocation requires an account transaction" unless @users.connection.transaction_open?
|
|
19
|
+
@external_invalidator&.call(user_id: user_id, at: at)
|
|
20
|
+
@sessions.where(user_id: user_id, revoked_at: nil).update_all(revoked_at: at)
|
|
21
|
+
@tokens.each { |model| model.where(user_id: user_id, consumed_at: nil, revoked_at: nil).update_all(revoked_at: at, delivery_payload: nil) }
|
|
22
|
+
@ceremonies.each { |model| model.where(user_id: user_id, consumed_at: nil).update_all(consumed_at: at) }
|
|
23
|
+
@credentials.each { |model| model.where(user_id: user_id, revoked_at: nil).update_all(revoked_at: at) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
module Rails
|
|
5
|
+
module Stores
|
|
6
|
+
module CommitDispatch
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def enqueue(transaction:, job:, id:, failure_event:, payload:)
|
|
10
|
+
raise AddAuth::Error, "outbox dispatch must join its owning transaction" unless transaction.open?
|
|
11
|
+
transaction.after_commit do
|
|
12
|
+
raise AddAuth::Error unless job.perform_later(id)
|
|
13
|
+
rescue
|
|
14
|
+
notify(event: failure_event, payload: payload)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def event(transaction:, event:, payload:)
|
|
19
|
+
raise AddAuth::Error, "commit events must join their owning transaction" unless transaction.open?
|
|
20
|
+
transaction.after_commit { notify(event: event, payload: payload) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def notify(event:, payload:)
|
|
24
|
+
ActiveSupport::Notifications.instrument(event, payload)
|
|
25
|
+
rescue
|
|
26
|
+
# Optional observers cannot report a committed operation as failed.
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "add_auth/rails/stores/account_lock"
|
|
5
|
+
|
|
6
|
+
module AddAuth
|
|
7
|
+
module Rails
|
|
8
|
+
module Stores
|
|
9
|
+
class ExternalIdentities
|
|
10
|
+
def initialize(user_model:, identity_model:, transaction_model:)
|
|
11
|
+
@users, @identities, @transactions = user_model, identity_model, transaction_model
|
|
12
|
+
unless [identity_model, transaction_model].all? { |model| model.connection_pool.equal?(@users.connection_pool) }
|
|
13
|
+
raise ArgumentError, "authentication models must share one connection pool"
|
|
14
|
+
end
|
|
15
|
+
@lock = AccountLock.new(@users)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with_user(id:, &block) = @lock.call(id: id, &block)
|
|
19
|
+
|
|
20
|
+
def with_identity(namespace:, replacing: nil)
|
|
21
|
+
@identities.uncached do
|
|
22
|
+
locator = binding(namespace: namespace)
|
|
23
|
+
@lock.call(id: locator&.user_id, additional_id: replacing&.user_id) do |user|
|
|
24
|
+
yield user, user && binding(namespace: namespace)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The G3 capability establishes fresh-account provenance. The adapter
|
|
30
|
+
# additionally enforces the exact user model/pool and owning transaction
|
|
31
|
+
# before a pending transaction can be consumed.
|
|
32
|
+
def registration_account(user:)
|
|
33
|
+
return unless @users.connection.transaction_open? && user.is_a?(@users) && user.persisted? && !user.destroyed?
|
|
34
|
+
@users.uncached { @users.find_by(id: user.id) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def binding(namespace:) = @identities.find_by(namespace: namespace)
|
|
38
|
+
def binding_for_user(user_id:, id:) = @identities.find_by(user_id: user_id, id: id)
|
|
39
|
+
def bindings(user_id:) = @identities.where(user_id: user_id, revoked_at: nil).to_a
|
|
40
|
+
|
|
41
|
+
def bind(user:, evidence:, at:)
|
|
42
|
+
require_transaction!
|
|
43
|
+
# A savepoint contains unique-index failures on PostgreSQL. Account
|
|
44
|
+
# locks serialize same-owner callbacks; the unique namespace index
|
|
45
|
+
# arbitrates different owners without choosing one by email.
|
|
46
|
+
@identities.transaction(requires_new: true) do
|
|
47
|
+
identity = binding(namespace: evidence.namespace)
|
|
48
|
+
if identity
|
|
49
|
+
return unless identity.user_id == user.id
|
|
50
|
+
return identity unless identity.revoked_at
|
|
51
|
+
identity.update!(revoked_at: nil, linked_at: at, credential_version: SecureRandom.hex(16), provenance: evidence.provenance)
|
|
52
|
+
identity
|
|
53
|
+
else
|
|
54
|
+
@identities.create!(user_id: user.id, namespace: evidence.namespace, provider_id: evidence.provider_id,
|
|
55
|
+
issuer: evidence.issuer, audience: evidence.audience, subject: evidence.subject,
|
|
56
|
+
provenance: evidence.provenance, linked_at: at, credential_version: SecureRandom.hex(16))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
rescue ActiveRecord::RecordNotUnique
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def revoke(identity:, at:)
|
|
64
|
+
require_transaction!
|
|
65
|
+
identity.update!(revoked_at: at, credential_version: SecureRandom.hex(16))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def invalidate_credentials(user_id:, at:, credential_version:)
|
|
69
|
+
require_transaction!
|
|
70
|
+
# Never move the cutoff backwards if a delayed invalidation arrives.
|
|
71
|
+
@identities.where(user_id: user_id, revoked_at: nil).where("invalidated_at IS NULL OR invalidated_at <= ?", at)
|
|
72
|
+
.update_all(invalidated_at: at, credential_version: credential_version, updated_at: at)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def create_transaction(**attributes) = @transactions.create!(**attributes)
|
|
76
|
+
def transaction_by_digest(digest:) = @transactions.uncached { @transactions.find_by(digest: digest) }
|
|
77
|
+
|
|
78
|
+
def consume_transaction(digest:, at:)
|
|
79
|
+
@transactions.where(digest: digest, consumed_at: nil).where("issued_at <= ? AND expires_at > ?", at, at)
|
|
80
|
+
.update_all(consumed_at: at) == 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def require_transaction!
|
|
86
|
+
raise AddAuth::Error, "identity mutation requires an account transaction" unless @users.connection.transaction_open?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -12,7 +12,7 @@ module AddAuth
|
|
|
12
12
|
scope = @model.where("expires_at <= ?", before)
|
|
13
13
|
if @kind == :session
|
|
14
14
|
scope = scope.or(@model.where("revoked_at <= ?", before))
|
|
15
|
-
elsif @kind
|
|
15
|
+
elsif !Core::Maintenance::EXPIRING.key?(@kind)
|
|
16
16
|
scope = without_live_lease(scope, now)
|
|
17
17
|
end
|
|
18
18
|
bounded(scope, limit).delete_all
|