standard_id 0.1.6 → 0.1.7
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/README.md +368 -22
- data/app/controllers/concerns/standard_id/set_current_request_details.rb +19 -0
- data/app/controllers/concerns/standard_id/social_authentication.rb +86 -37
- data/app/controllers/concerns/standard_id/web_authentication.rb +29 -1
- data/app/controllers/standard_id/api/base_controller.rb +1 -0
- data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +7 -18
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +33 -37
- data/app/controllers/standard_id/web/base_controller.rb +1 -0
- data/app/controllers/standard_id/web/login_controller.rb +6 -19
- data/app/controllers/standard_id/web/signup_controller.rb +3 -6
- data/app/forms/standard_id/web/signup_form.rb +32 -1
- data/app/models/standard_id/browser_session.rb +8 -0
- data/app/models/standard_id/client_secret_credential.rb +11 -0
- data/app/models/standard_id/device_session.rb +4 -0
- data/app/models/standard_id/identifier.rb +28 -0
- data/app/models/standard_id/service_session.rb +1 -1
- data/app/models/standard_id/session.rb +16 -2
- data/app/views/standard_id/web/auth/callback/providers/{apple_mobile.html.erb → mobile_callback.html.erb} +1 -1
- data/config/routes/api.rb +1 -2
- data/config/routes/web.rb +4 -3
- data/lib/generators/standard_id/install/templates/standard_id.rb +11 -8
- data/lib/standard_config/config.rb +3 -12
- data/lib/standard_config/config_provider.rb +6 -6
- data/lib/standard_config/schema.rb +2 -2
- data/lib/standard_id/account_locking.rb +86 -0
- data/lib/standard_id/account_status.rb +45 -0
- data/lib/standard_id/api/authentication_guard.rb +40 -1
- data/lib/standard_id/api/token_manager.rb +1 -1
- data/lib/standard_id/config/schema.rb +11 -9
- data/lib/standard_id/current_attributes.rb +9 -0
- data/lib/standard_id/engine.rb +9 -0
- data/lib/standard_id/errors.rb +12 -0
- data/lib/standard_id/events/definitions.rb +157 -0
- data/lib/standard_id/events/event.rb +123 -0
- data/lib/standard_id/events/subscribers/account_locking_subscriber.rb +17 -0
- data/lib/standard_id/events/subscribers/account_status_subscriber.rb +17 -0
- data/lib/standard_id/events/subscribers/base.rb +165 -0
- data/lib/standard_id/events/subscribers/logging_subscriber.rb +122 -0
- data/lib/standard_id/events.rb +137 -0
- data/lib/standard_id/oauth/authorization_code_flow.rb +10 -0
- data/lib/standard_id/oauth/client_credentials_flow.rb +31 -0
- data/lib/standard_id/oauth/password_flow.rb +36 -4
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +38 -2
- data/lib/standard_id/oauth/subflows/social_login_grant.rb +11 -22
- data/lib/standard_id/oauth/token_grant_flow.rb +22 -1
- data/lib/standard_id/passwordless/base_strategy.rb +32 -0
- data/lib/standard_id/provider_registry.rb +73 -0
- data/lib/standard_id/{social_providers → providers}/apple.rb +46 -7
- data/lib/standard_id/providers/base.rb +242 -0
- data/lib/standard_id/{social_providers → providers}/google.rb +26 -7
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id/web/authentication_guard.rb +29 -0
- data/lib/standard_id/web/session_manager.rb +39 -1
- data/lib/standard_id/web/token_manager.rb +2 -2
- data/lib/standard_id.rb +13 -2
- metadata +18 -6
- data/lib/standard_id/social_providers/response_builder.rb +0 -18
|
@@ -6,37 +6,24 @@ module StandardId
|
|
|
6
6
|
|
|
7
7
|
skip_before_action :validate_content_type!
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
expect_and_permit!([], [:id_token, :code])
|
|
11
|
-
handle_social_callback("google")
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def apple
|
|
15
|
-
expect_and_permit!([], [:id_token, :code, :state, :flow])
|
|
16
|
-
handle_social_callback("apple")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def handle_social_callback(connection)
|
|
9
|
+
def callback
|
|
22
10
|
original_params = decode_state_params
|
|
23
|
-
|
|
24
|
-
provider_response = get_user_info_from_provider(connection, flow: flow)
|
|
11
|
+
provider_response = get_user_info_from_provider(flow: resolve_flow_for(provider.provider_name))
|
|
25
12
|
social_info = provider_response[:user_info]
|
|
26
13
|
provider_tokens = provider_response[:tokens]
|
|
27
|
-
account = find_or_create_account_from_social(social_info
|
|
14
|
+
account = find_or_create_account_from_social(social_info)
|
|
28
15
|
|
|
29
16
|
flow = StandardId::Oauth::SocialFlow.new(
|
|
30
17
|
params,
|
|
31
18
|
request,
|
|
32
19
|
account: account,
|
|
33
|
-
connection:
|
|
20
|
+
connection: provider.provider_name,
|
|
34
21
|
original_params: original_params
|
|
35
22
|
)
|
|
36
23
|
|
|
37
24
|
token_response = flow.execute
|
|
38
25
|
run_social_callback(
|
|
39
|
-
provider:
|
|
26
|
+
provider: provider.provider_name,
|
|
40
27
|
social_info: social_info,
|
|
41
28
|
provider_tokens: provider_tokens,
|
|
42
29
|
account: account,
|
|
@@ -44,6 +31,8 @@ module StandardId
|
|
|
44
31
|
render json: token_response, status: :ok
|
|
45
32
|
end
|
|
46
33
|
|
|
34
|
+
private
|
|
35
|
+
|
|
47
36
|
def decode_state_params
|
|
48
37
|
encoded_state = params[:state]
|
|
49
38
|
|
|
@@ -8,35 +8,10 @@ module StandardId
|
|
|
8
8
|
|
|
9
9
|
# Social callbacks must be accessible without an existing browser session
|
|
10
10
|
# because they create/sign-in the session upon successful callback.
|
|
11
|
-
skip_before_action :require_browser_session!, only: [:
|
|
12
|
-
skip_before_action :verify_authenticity_token, only: [:
|
|
11
|
+
skip_before_action :require_browser_session!, only: [:callback, :mobile_callback]
|
|
12
|
+
skip_before_action :verify_authenticity_token, only: [:callback, :mobile_callback], if: :skip_csrf_verification?
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
handle_social_callback("google")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def apple
|
|
19
|
-
handle_social_callback("apple")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def apple_mobile
|
|
23
|
-
state_data = decode_state_params
|
|
24
|
-
destination = state_data["redirect_uri"]
|
|
25
|
-
|
|
26
|
-
unless allow_other_host_redirect?(destination)
|
|
27
|
-
raise StandardId::InvalidRequestError, "Redirect URI is not allowed"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
relay_params = mobile_relay_params
|
|
31
|
-
@mobile_redirect_url = build_mobile_redirect(destination, relay_params)
|
|
32
|
-
render :apple_mobile, layout: false
|
|
33
|
-
rescue StandardId::InvalidRequestError => e
|
|
34
|
-
render plain: e.message, status: :unprocessable_entity
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def handle_social_callback(connection)
|
|
14
|
+
def callback
|
|
40
15
|
if params[:error].present?
|
|
41
16
|
handle_callback_error
|
|
42
17
|
return
|
|
@@ -46,22 +21,22 @@ module StandardId
|
|
|
46
21
|
|
|
47
22
|
begin
|
|
48
23
|
state_data = decode_state_params
|
|
49
|
-
redirect_uri =
|
|
50
|
-
provider_response = get_user_info_from_provider(
|
|
24
|
+
redirect_uri = callback_url_for
|
|
25
|
+
provider_response = get_user_info_from_provider(redirect_uri: redirect_uri)
|
|
51
26
|
social_info = provider_response[:user_info]
|
|
52
27
|
provider_tokens = provider_response[:tokens]
|
|
53
|
-
account = find_or_create_account_from_social(social_info
|
|
28
|
+
account = find_or_create_account_from_social(social_info)
|
|
54
29
|
session_manager.sign_in_account(account)
|
|
55
30
|
|
|
56
31
|
run_social_callback(
|
|
57
|
-
provider:
|
|
32
|
+
provider: provider.provider_name,
|
|
58
33
|
social_info: social_info,
|
|
59
34
|
provider_tokens: provider_tokens,
|
|
60
35
|
account: account,
|
|
61
36
|
)
|
|
62
37
|
|
|
63
38
|
destination = state_data["redirect_uri"]
|
|
64
|
-
redirect_options = { notice: "Successfully signed in with #{
|
|
39
|
+
redirect_options = { notice: "Successfully signed in with #{provider.provider_name.humanize}" }
|
|
65
40
|
redirect_options[:allow_other_host] = true if allow_other_host_redirect?(destination)
|
|
66
41
|
redirect_to destination, redirect_options
|
|
67
42
|
rescue StandardId::OAuthError => e
|
|
@@ -69,12 +44,33 @@ module StandardId
|
|
|
69
44
|
end
|
|
70
45
|
end
|
|
71
46
|
|
|
72
|
-
def
|
|
73
|
-
|
|
47
|
+
def mobile_callback
|
|
48
|
+
unless provider.supports_mobile_callback?
|
|
49
|
+
raise StandardId::InvalidRequestError, "Provider #{provider.provider_name} does not support mobile callback"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
state_data = decode_state_params
|
|
53
|
+
destination = state_data["redirect_uri"]
|
|
54
|
+
|
|
55
|
+
unless allow_other_host_redirect?(destination)
|
|
56
|
+
raise StandardId::InvalidRequestError, "Redirect URI is not allowed"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
relay_params = mobile_relay_params
|
|
60
|
+
@mobile_redirect_url = build_mobile_redirect(destination, relay_params)
|
|
61
|
+
render :mobile_callback, layout: false
|
|
62
|
+
rescue StandardId::InvalidRequestError => e
|
|
63
|
+
render plain: e.message, status: :unprocessable_entity
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def callback_url_for
|
|
69
|
+
"#{request.base_url}#{provider.callback_path}"
|
|
74
70
|
end
|
|
75
71
|
|
|
76
|
-
def
|
|
77
|
-
|
|
72
|
+
def skip_csrf_verification?
|
|
73
|
+
provider.skip_csrf?
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
def decode_state_params
|
|
@@ -37,28 +37,15 @@ module StandardId
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def social_login_url
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
google_authorization_url
|
|
43
|
-
when "apple"
|
|
44
|
-
apple_authorization_url
|
|
45
|
-
else
|
|
46
|
-
raise StandardId::InvalidRequestError, "Unsupported social connection: #{connection}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def google_authorization_url
|
|
51
|
-
StandardId::SocialProviders::Google.authorization_url(
|
|
52
|
-
state: encode_state,
|
|
53
|
-
redirect_uri: auth_callback_google_url
|
|
54
|
-
)
|
|
55
|
-
end
|
|
40
|
+
connection = params[:connection]
|
|
41
|
+
provider = StandardId::ProviderRegistry.get(connection)
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
StandardId::SocialProviders::Apple.authorization_url(
|
|
43
|
+
provider.authorization_url(
|
|
59
44
|
state: encode_state,
|
|
60
|
-
redirect_uri:
|
|
45
|
+
redirect_uri: "#{request.base_url}#{provider.callback_path}"
|
|
61
46
|
)
|
|
47
|
+
rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
|
|
48
|
+
raise StandardId::InvalidRequestError, e.message
|
|
62
49
|
end
|
|
63
50
|
|
|
64
51
|
def encode_state
|
|
@@ -61,12 +61,9 @@ module StandardId
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def callback_url
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
when "apple"
|
|
68
|
-
auth_callback_apple_url
|
|
69
|
-
end
|
|
64
|
+
connection = params[:connection]
|
|
65
|
+
provider = StandardId::ProviderRegistry.get(connection)
|
|
66
|
+
"#{request.base_url}#{provider.callback_path}"
|
|
70
67
|
end
|
|
71
68
|
|
|
72
69
|
def encode_state
|
|
@@ -16,15 +16,21 @@ module StandardId
|
|
|
16
16
|
def submit
|
|
17
17
|
return false unless valid?
|
|
18
18
|
|
|
19
|
+
emit_account_creating
|
|
20
|
+
|
|
19
21
|
ActiveRecord::Base.transaction do
|
|
20
22
|
@account = Account.create!(account_params)
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
password_credential = StandardId::PasswordCredential.create!(
|
|
22
25
|
password_credential_params.merge(
|
|
23
26
|
credential_attributes: {
|
|
24
27
|
identifier_attributes: email_identifier_params.merge(account: @account)
|
|
25
28
|
}
|
|
26
29
|
)
|
|
27
30
|
)
|
|
31
|
+
|
|
32
|
+
emit_account_created
|
|
33
|
+
emit_credential_created(password_credential)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
true
|
|
@@ -38,6 +44,31 @@ module StandardId
|
|
|
38
44
|
|
|
39
45
|
private
|
|
40
46
|
|
|
47
|
+
def emit_account_creating
|
|
48
|
+
StandardId::Events.publish(
|
|
49
|
+
StandardId::Events::ACCOUNT_CREATING,
|
|
50
|
+
account_params: account_params,
|
|
51
|
+
auth_method: "password"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def emit_account_created
|
|
56
|
+
StandardId::Events.publish(
|
|
57
|
+
StandardId::Events::ACCOUNT_CREATED,
|
|
58
|
+
account: @account,
|
|
59
|
+
auth_method: "password",
|
|
60
|
+
source: "signup"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emit_credential_created(password_credential)
|
|
65
|
+
StandardId::Events.publish(
|
|
66
|
+
StandardId::Events::CREDENTIAL_PASSWORD_CREATED,
|
|
67
|
+
credential: password_credential,
|
|
68
|
+
account: @account
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
41
72
|
def account_params
|
|
42
73
|
{ name: (email.to_s.split("@").first.presence || "User"), email: }
|
|
43
74
|
end
|
|
@@ -2,6 +2,14 @@ module StandardId
|
|
|
2
2
|
class BrowserSession < Session
|
|
3
3
|
validates :user_agent, presence: true
|
|
4
4
|
|
|
5
|
+
def self.expiry
|
|
6
|
+
StandardId.config.session.browser_session_lifetime.seconds.from_now
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.remember_me_expiry
|
|
10
|
+
StandardId.config.session.browser_session_remember_me_lifetime.seconds.from_now
|
|
11
|
+
end
|
|
12
|
+
|
|
5
13
|
def browser_info
|
|
6
14
|
return {} if user_agent.blank?
|
|
7
15
|
|
|
@@ -18,6 +18,7 @@ module StandardId
|
|
|
18
18
|
|
|
19
19
|
def revoke!
|
|
20
20
|
update!(active: false, revoked_at: Time.current)
|
|
21
|
+
emit_revoked_event
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def active?
|
|
@@ -57,6 +58,16 @@ module StandardId
|
|
|
57
58
|
self.client_secret ||= SecureRandom.hex(32)
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
def emit_revoked_event
|
|
62
|
+
StandardId::Events.publish(
|
|
63
|
+
StandardId::Events::CREDENTIAL_CLIENT_SECRET_REVOKED,
|
|
64
|
+
credential: self,
|
|
65
|
+
client_application: client_application,
|
|
66
|
+
client_id: client_id,
|
|
67
|
+
revoked_at: revoked_at
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
60
71
|
# Note: We intentionally do not enforce subset validation for per-secret overrides here.
|
|
61
72
|
# If needed later, we can introduce a configuration flag to enable enforcement.
|
|
62
73
|
end
|
|
@@ -11,6 +11,7 @@ module StandardId
|
|
|
11
11
|
validates :value, presence: true, uniqueness: { scope: [:account_id, :type] }
|
|
12
12
|
|
|
13
13
|
after_commit :mark_account_verified!, on: :update, if: :just_verified?
|
|
14
|
+
after_commit :emit_identifier_created_event, on: :create
|
|
14
15
|
|
|
15
16
|
def verified?
|
|
16
17
|
verified_at.present?
|
|
@@ -18,6 +19,7 @@ module StandardId
|
|
|
18
19
|
|
|
19
20
|
def verify!
|
|
20
21
|
update!(verified_at: Time.current)
|
|
22
|
+
emit_verification_succeeded
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def unverify!
|
|
@@ -37,6 +39,32 @@ module StandardId
|
|
|
37
39
|
return unless account.has_attribute?(:verified_at)
|
|
38
40
|
|
|
39
41
|
account.update!(verified: true, verified_at: Time.current)
|
|
42
|
+
emit_account_verified
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def emit_identifier_created_event
|
|
46
|
+
StandardId::Events.publish(
|
|
47
|
+
StandardId::Events::IDENTIFIER_CREATED,
|
|
48
|
+
identifier: self,
|
|
49
|
+
account: account
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def emit_verification_succeeded
|
|
54
|
+
StandardId::Events.publish(
|
|
55
|
+
StandardId::Events::IDENTIFIER_VERIFICATION_SUCCEEDED,
|
|
56
|
+
identifier: self,
|
|
57
|
+
account: account,
|
|
58
|
+
verified_at: verified_at
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def emit_account_verified
|
|
63
|
+
StandardId::Events.publish(
|
|
64
|
+
StandardId::Events::ACCOUNT_VERIFIED,
|
|
65
|
+
account: account,
|
|
66
|
+
verified_via: type.demodulize.underscore.gsub("_identifier", "")
|
|
67
|
+
)
|
|
40
68
|
end
|
|
41
69
|
end
|
|
42
70
|
end
|
|
@@ -19,7 +19,7 @@ module StandardId
|
|
|
19
19
|
attr_reader :token
|
|
20
20
|
|
|
21
21
|
before_validation :generate_token, :generate_token_digest, :generate_lookup_hash, on: :create
|
|
22
|
-
|
|
22
|
+
after_commit :emit_session_revoked_event, on: :update, if: :just_revoked?
|
|
23
23
|
|
|
24
24
|
def active?
|
|
25
25
|
!revoked? && !expired?
|
|
@@ -33,7 +33,8 @@ module StandardId
|
|
|
33
33
|
revoked_at.present?
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def revoke!
|
|
36
|
+
def revoke!(reason: nil)
|
|
37
|
+
@reason = reason
|
|
37
38
|
update!(revoked_at: Time.current)
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -50,5 +51,18 @@ module StandardId
|
|
|
50
51
|
def generate_lookup_hash
|
|
51
52
|
self.lookup_hash = Digest::SHA256.hexdigest("#{token}:#{Rails.configuration.secret_key_base}")
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
def just_revoked?
|
|
56
|
+
saved_change_to_revoked_at? && revoked?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def emit_session_revoked_event
|
|
60
|
+
StandardId::Events.publish(
|
|
61
|
+
StandardId::Events::SESSION_REVOKED,
|
|
62
|
+
session: self,
|
|
63
|
+
account:,
|
|
64
|
+
reason: @reason
|
|
65
|
+
)
|
|
66
|
+
end
|
|
53
67
|
end
|
|
54
68
|
end
|
data/config/routes/api.rb
CHANGED
|
@@ -16,8 +16,7 @@ StandardId::ApiEngine.routes.draw do
|
|
|
16
16
|
resource :token, only: [:create]
|
|
17
17
|
|
|
18
18
|
namespace :callback do
|
|
19
|
-
post :
|
|
20
|
-
post :apple, to: "providers#apple"
|
|
19
|
+
post ":provider", to: "providers#callback", as: :provider
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
end
|
data/config/routes/web.rb
CHANGED
|
@@ -7,10 +7,11 @@ StandardId::WebEngine.routes.draw do
|
|
|
7
7
|
|
|
8
8
|
# Social authentication callbacks (web flow)
|
|
9
9
|
namespace :auth do
|
|
10
|
+
post "callback_mobile/:provider", to: "callback/providers#mobile_callback", as: :callback_mobile
|
|
11
|
+
|
|
10
12
|
namespace :callback do
|
|
11
|
-
get :
|
|
12
|
-
post :
|
|
13
|
-
post :apple_mobile, to: "providers#apple_mobile"
|
|
13
|
+
get ":provider", to: "providers#callback", as: :provider
|
|
14
|
+
post ":provider", to: "providers#callback"
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -15,6 +15,13 @@ StandardId.configure do |c|
|
|
|
15
15
|
# c.use_inertia = true
|
|
16
16
|
# c.inertia_component_namespace = "auth" # Component path prefix (e.g., "auth/login/show")
|
|
17
17
|
|
|
18
|
+
# Session lifetimes (in seconds)
|
|
19
|
+
# c.session.browser_session_lifetime = 86400 # 24 hours (web sessions)
|
|
20
|
+
# c.session.browser_session_remember_me_lifetime = 2_592_000 # 30 days (remember me cookies)
|
|
21
|
+
# c.session.device_session_lifetime = 2_592_000 # 30 days (API device sessions)
|
|
22
|
+
# c.session.service_session_lifetime = 7_776_000 # 90 days (service-to-service sessions)
|
|
23
|
+
|
|
24
|
+
# Passwordless authentication delivery (DEPRECATED - use event subscriptions instead)
|
|
18
25
|
# c.passwordless_email_sender = ->(email, code) { PasswordlessMailer.with(code: code, to: email).deliver_later }
|
|
19
26
|
# c.passwordless_sms_sender = ->(phone, code) { SmsProvider.send_code(phone: phone, code: code) }
|
|
20
27
|
|
|
@@ -40,6 +47,10 @@ StandardId.configure do |c|
|
|
|
40
47
|
# }
|
|
41
48
|
# }
|
|
42
49
|
|
|
50
|
+
# Events
|
|
51
|
+
# Enable or disable logging emitted via the internal event system
|
|
52
|
+
# c.events.enable_logging = false
|
|
53
|
+
|
|
43
54
|
# Social login credentials (if enabled in your app)
|
|
44
55
|
# c.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
45
56
|
# c.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
@@ -55,14 +66,6 @@ StandardId.configure do |c|
|
|
|
55
66
|
# name: social_info[:name] || social_info[:given_name]
|
|
56
67
|
# }
|
|
57
68
|
# }
|
|
58
|
-
# c.social.social_callback = ->(social_info:, provider:, tokens:, account:) {
|
|
59
|
-
# Analytics.track_social_login(
|
|
60
|
-
# provider: provider,
|
|
61
|
-
# email: social_info[:email],
|
|
62
|
-
# tokens: tokens,
|
|
63
|
-
# account_id: account.id
|
|
64
|
-
# )
|
|
65
|
-
# }
|
|
66
69
|
|
|
67
70
|
# OIDC Logout allow list
|
|
68
71
|
# c.allowed_post_logout_redirect_uris = [
|
|
@@ -23,13 +23,10 @@ module StandardConfig
|
|
|
23
23
|
# If set, Authorization endpoints can redirect to this path with a redirect_uri param
|
|
24
24
|
attr_accessor :login_url
|
|
25
25
|
|
|
26
|
-
# Social login
|
|
27
|
-
attr_accessor :
|
|
28
|
-
attr_accessor :apple_client_id, :apple_client_secret, :apple_private_key, :apple_key_id, :apple_team_id
|
|
29
|
-
attr_accessor :social_account_attributes, :social_callback
|
|
26
|
+
# Social login hooks
|
|
27
|
+
attr_accessor :social_account_attributes
|
|
30
28
|
|
|
31
|
-
# Passwordless authentication callbacks
|
|
32
|
-
# These should be callable objects (procs/lambdas) that accept (recipient, code) parameters
|
|
29
|
+
# Passwordless authentication delivery callbacks (deprecated - use events instead)
|
|
33
30
|
attr_accessor :passwordless_email_sender, :passwordless_sms_sender
|
|
34
31
|
|
|
35
32
|
# Allowed post-logout redirect URIs for OIDC logout endpoint
|
|
@@ -56,15 +53,9 @@ module StandardConfig
|
|
|
56
53
|
@logger = nil
|
|
57
54
|
@issuer = nil
|
|
58
55
|
@login_url = nil
|
|
59
|
-
@google_client_id = nil
|
|
60
|
-
@google_client_secret = nil
|
|
61
|
-
@apple_client_id = nil
|
|
62
|
-
@apple_client_secret = nil
|
|
63
|
-
@apple_private_key = nil
|
|
64
56
|
@apple_key_id = nil
|
|
65
57
|
@apple_team_id = nil
|
|
66
58
|
@social_account_attributes = nil
|
|
67
|
-
@social_callback = nil
|
|
68
59
|
@passwordless_email_sender = nil
|
|
69
60
|
@passwordless_sms_sender = nil
|
|
70
61
|
@allowed_post_logout_redirect_uris = []
|
|
@@ -9,9 +9,9 @@ module StandardConfig
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def method_missing(method_name, *args)
|
|
12
|
-
if method_name.to_s.end_with?(
|
|
12
|
+
if method_name.to_s.end_with?("=")
|
|
13
13
|
# Setter - only works for static configs (OpenStruct objects)
|
|
14
|
-
field_name = method_name.to_s.chomp(
|
|
14
|
+
field_name = method_name.to_s.chomp("=").to_sym
|
|
15
15
|
validate_field!(field_name)
|
|
16
16
|
|
|
17
17
|
config_object = @resolver_proc.call
|
|
@@ -42,11 +42,11 @@ module StandardConfig
|
|
|
42
42
|
config_object = @resolver_proc.call
|
|
43
43
|
raw_value = if config_object.respond_to?(field_name)
|
|
44
44
|
config_object.send(field_name)
|
|
45
|
-
|
|
45
|
+
elsif config_object.respond_to?(:[])
|
|
46
46
|
config_object[field_name] || config_object[field_name.to_s]
|
|
47
|
-
|
|
47
|
+
else
|
|
48
48
|
nil
|
|
49
|
-
|
|
49
|
+
end
|
|
50
50
|
|
|
51
51
|
# Cast the value according to schema
|
|
52
52
|
field_def = @schema&.field_definition(@scope_name, field_name)
|
|
@@ -64,7 +64,7 @@ module StandardConfig
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
-
field_name = method_name.to_s.end_with?(
|
|
67
|
+
field_name = method_name.to_s.end_with?("=") ? method_name.to_s.chomp("=").to_sym : method_name.to_sym
|
|
68
68
|
@schema&.valid_field?(@scope_name, field_name) || super
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -55,8 +55,8 @@ module StandardConfig
|
|
|
55
55
|
when :boolean
|
|
56
56
|
case value
|
|
57
57
|
when true, false then value
|
|
58
|
-
when
|
|
59
|
-
when
|
|
58
|
+
when "true", "1", 1 then true
|
|
59
|
+
when "false", "0", 0 then false
|
|
60
60
|
else !!value
|
|
61
61
|
end
|
|
62
62
|
when :array
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module AccountLocking
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
belongs_to :locked_by, polymorphic: true, optional: true
|
|
7
|
+
belongs_to :unlocked_by, polymorphic: true, optional: true
|
|
8
|
+
|
|
9
|
+
scope :locked, -> { where(locked: true) }
|
|
10
|
+
scope :unlocked, -> { where(locked: false) }
|
|
11
|
+
|
|
12
|
+
after_commit :emit_account_locked_event, on: :update, if: :just_locked?
|
|
13
|
+
after_commit :emit_account_unlocked_event, on: :update, if: :just_unlocked?
|
|
14
|
+
|
|
15
|
+
# Subscribe to events to enforce lock status
|
|
16
|
+
# Lock check runs BEFORE status check (more restrictive first)
|
|
17
|
+
StandardId::Events.subscribe(
|
|
18
|
+
StandardId::Events::OAUTH_TOKEN_ISSUING,
|
|
19
|
+
StandardId::Events::SESSION_CREATING,
|
|
20
|
+
StandardId::Events::SESSION_VALIDATING
|
|
21
|
+
) do |event|
|
|
22
|
+
account = event[:account]
|
|
23
|
+
if account&.locked?
|
|
24
|
+
raise StandardId::AccountLockedError.new(account)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def locked?
|
|
30
|
+
locked == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unlocked?
|
|
34
|
+
!locked?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lock!(reason:, locked_by: nil)
|
|
38
|
+
return true if locked?
|
|
39
|
+
|
|
40
|
+
update!(
|
|
41
|
+
locked: true,
|
|
42
|
+
locked_at: Time.current,
|
|
43
|
+
lock_reason: reason,
|
|
44
|
+
locked_by: locked_by
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def unlock!(unlocked_by: nil)
|
|
49
|
+
return true if unlocked?
|
|
50
|
+
|
|
51
|
+
update!(
|
|
52
|
+
locked: false,
|
|
53
|
+
unlocked_at: Time.current,
|
|
54
|
+
unlocked_by: unlocked_by,
|
|
55
|
+
lock_reason: nil
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def just_locked?
|
|
62
|
+
locked_previously_changed? && locked?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def just_unlocked?
|
|
66
|
+
locked_previously_changed? && unlocked?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def emit_account_locked_event
|
|
70
|
+
StandardId::Events.publish(
|
|
71
|
+
StandardId::Events::ACCOUNT_LOCKED,
|
|
72
|
+
account: self,
|
|
73
|
+
reason: lock_reason,
|
|
74
|
+
locked_by:
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def emit_account_unlocked_event
|
|
79
|
+
StandardId::Events.publish(
|
|
80
|
+
StandardId::Events::ACCOUNT_UNLOCKED,
|
|
81
|
+
account: self,
|
|
82
|
+
unlocked_by:
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|