standard_id 0.1.5 → 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 +529 -20
- data/app/controllers/concerns/standard_id/inertia_rendering.rb +49 -0
- data/app/controllers/concerns/standard_id/inertia_support.rb +31 -0
- 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 +50 -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 +12 -21
- data/app/controllers/standard_id/web/signup_controller.rb +11 -8
- 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 +19 -8
- data/lib/standard_config/config.rb +13 -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 +13 -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 +20 -6
- data/lib/standard_id/social_providers/response_builder.rb +0 -18
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Definitions
|
|
4
|
+
AUTHENTICATION_ATTEMPT_STARTED = "authentication.attempt.started"
|
|
5
|
+
AUTHENTICATION_SUCCEEDED = "authentication.attempt.succeeded"
|
|
6
|
+
AUTHENTICATION_FAILED = "authentication.attempt.failed"
|
|
7
|
+
PASSWORD_VALIDATED = "authentication.password.validated"
|
|
8
|
+
PASSWORD_VALIDATION_FAILED = "authentication.password.failed"
|
|
9
|
+
OTP_VALIDATED = "authentication.otp.validated"
|
|
10
|
+
OTP_VALIDATION_FAILED = "authentication.otp.failed"
|
|
11
|
+
|
|
12
|
+
SESSION_CREATING = "session.creating"
|
|
13
|
+
SESSION_CREATED = "session.created"
|
|
14
|
+
SESSION_VALIDATING = "session.validating"
|
|
15
|
+
SESSION_VALIDATED = "session.validated"
|
|
16
|
+
SESSION_EXPIRED = "session.expired"
|
|
17
|
+
SESSION_REVOKED = "session.revoked"
|
|
18
|
+
SESSION_REFRESHED = "session.refreshed"
|
|
19
|
+
|
|
20
|
+
ACCOUNT_CREATING = "account.creating"
|
|
21
|
+
ACCOUNT_CREATED = "account.created"
|
|
22
|
+
ACCOUNT_VERIFIED = "account.verified"
|
|
23
|
+
ACCOUNT_STATUS_CHANGED = "account.status_changed"
|
|
24
|
+
ACCOUNT_ACTIVATED = "account.activated"
|
|
25
|
+
ACCOUNT_DEACTIVATED = "account.deactivated"
|
|
26
|
+
ACCOUNT_LOCKED = "account.locked"
|
|
27
|
+
ACCOUNT_UNLOCKED = "account.unlocked"
|
|
28
|
+
|
|
29
|
+
IDENTIFIER_CREATED = "identifier.created"
|
|
30
|
+
IDENTIFIER_VERIFICATION_STARTED = "identifier.verification.started"
|
|
31
|
+
IDENTIFIER_VERIFICATION_SUCCEEDED = "identifier.verification.succeeded"
|
|
32
|
+
IDENTIFIER_VERIFICATION_FAILED = "identifier.verification.failed"
|
|
33
|
+
IDENTIFIER_LINKED = "identifier.linked"
|
|
34
|
+
|
|
35
|
+
OAUTH_AUTHORIZATION_REQUESTED = "oauth.authorization.requested"
|
|
36
|
+
OAUTH_AUTHORIZATION_GRANTED = "oauth.authorization.granted"
|
|
37
|
+
OAUTH_AUTHORIZATION_DENIED = "oauth.authorization.denied"
|
|
38
|
+
OAUTH_TOKEN_ISSUING = "oauth.token.issuing"
|
|
39
|
+
OAUTH_TOKEN_ISSUED = "oauth.token.issued"
|
|
40
|
+
OAUTH_TOKEN_REFRESHED = "oauth.token.refreshed"
|
|
41
|
+
OAUTH_CODE_CONSUMED = "oauth.code.consumed"
|
|
42
|
+
|
|
43
|
+
PASSWORDLESS_CODE_REQUESTED = "passwordless.code.requested"
|
|
44
|
+
PASSWORDLESS_CODE_GENERATED = "passwordless.code.generated"
|
|
45
|
+
PASSWORDLESS_CODE_SENT = "passwordless.code.sent"
|
|
46
|
+
PASSWORDLESS_CODE_VERIFIED = "passwordless.code.verified"
|
|
47
|
+
PASSWORDLESS_CODE_FAILED = "passwordless.code.failed"
|
|
48
|
+
PASSWORDLESS_ACCOUNT_CREATED = "passwordless.account.created"
|
|
49
|
+
|
|
50
|
+
SOCIAL_AUTH_STARTED = "social.auth.started"
|
|
51
|
+
SOCIAL_CALLBACK_RECEIVED = "social.auth.callback_received"
|
|
52
|
+
SOCIAL_USER_INFO_FETCHED = "social.user_info.fetched"
|
|
53
|
+
SOCIAL_ACCOUNT_CREATED = "social.account.created"
|
|
54
|
+
SOCIAL_ACCOUNT_LINKED = "social.account.linked"
|
|
55
|
+
SOCIAL_AUTH_COMPLETED = "social.auth.completed"
|
|
56
|
+
|
|
57
|
+
CREDENTIAL_PASSWORD_CREATED = "credential.password.created"
|
|
58
|
+
CREDENTIAL_PASSWORD_RESET_INITIATED = "credential.password.reset_initiated"
|
|
59
|
+
CREDENTIAL_PASSWORD_RESET_COMPLETED = "credential.password.reset_completed"
|
|
60
|
+
CREDENTIAL_PASSWORD_CHANGED = "credential.password.changed"
|
|
61
|
+
CREDENTIAL_CLIENT_SECRET_CREATED = "credential.client_secret.created"
|
|
62
|
+
CREDENTIAL_CLIENT_SECRET_ROTATED = "credential.client_secret.rotated"
|
|
63
|
+
CREDENTIAL_CLIENT_SECRET_REVOKED = "credential.client_secret.revoked"
|
|
64
|
+
|
|
65
|
+
AUTHENTICATION_EVENTS = [
|
|
66
|
+
AUTHENTICATION_ATTEMPT_STARTED,
|
|
67
|
+
AUTHENTICATION_SUCCEEDED,
|
|
68
|
+
AUTHENTICATION_FAILED,
|
|
69
|
+
PASSWORD_VALIDATED,
|
|
70
|
+
PASSWORD_VALIDATION_FAILED,
|
|
71
|
+
OTP_VALIDATED,
|
|
72
|
+
OTP_VALIDATION_FAILED
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
75
|
+
SESSION_EVENTS = [
|
|
76
|
+
SESSION_CREATING,
|
|
77
|
+
SESSION_CREATED,
|
|
78
|
+
SESSION_VALIDATING,
|
|
79
|
+
SESSION_VALIDATED,
|
|
80
|
+
SESSION_EXPIRED,
|
|
81
|
+
SESSION_REVOKED,
|
|
82
|
+
SESSION_REFRESHED
|
|
83
|
+
].freeze
|
|
84
|
+
|
|
85
|
+
ACCOUNT_EVENTS = [
|
|
86
|
+
ACCOUNT_CREATING,
|
|
87
|
+
ACCOUNT_CREATED,
|
|
88
|
+
ACCOUNT_VERIFIED,
|
|
89
|
+
ACCOUNT_STATUS_CHANGED,
|
|
90
|
+
ACCOUNT_ACTIVATED,
|
|
91
|
+
ACCOUNT_DEACTIVATED,
|
|
92
|
+
ACCOUNT_LOCKED,
|
|
93
|
+
ACCOUNT_UNLOCKED
|
|
94
|
+
].freeze
|
|
95
|
+
|
|
96
|
+
IDENTIFIER_EVENTS = [
|
|
97
|
+
IDENTIFIER_CREATED,
|
|
98
|
+
IDENTIFIER_VERIFICATION_STARTED,
|
|
99
|
+
IDENTIFIER_VERIFICATION_SUCCEEDED,
|
|
100
|
+
IDENTIFIER_VERIFICATION_FAILED,
|
|
101
|
+
IDENTIFIER_LINKED
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
OAUTH_EVENTS = [
|
|
105
|
+
OAUTH_AUTHORIZATION_REQUESTED,
|
|
106
|
+
OAUTH_AUTHORIZATION_GRANTED,
|
|
107
|
+
OAUTH_AUTHORIZATION_DENIED,
|
|
108
|
+
OAUTH_TOKEN_ISSUING,
|
|
109
|
+
OAUTH_TOKEN_ISSUED,
|
|
110
|
+
OAUTH_TOKEN_REFRESHED,
|
|
111
|
+
OAUTH_CODE_CONSUMED
|
|
112
|
+
].freeze
|
|
113
|
+
|
|
114
|
+
PASSWORDLESS_EVENTS = [
|
|
115
|
+
PASSWORDLESS_CODE_REQUESTED,
|
|
116
|
+
PASSWORDLESS_CODE_GENERATED,
|
|
117
|
+
PASSWORDLESS_CODE_SENT,
|
|
118
|
+
PASSWORDLESS_CODE_VERIFIED,
|
|
119
|
+
PASSWORDLESS_CODE_FAILED,
|
|
120
|
+
PASSWORDLESS_ACCOUNT_CREATED
|
|
121
|
+
].freeze
|
|
122
|
+
|
|
123
|
+
SOCIAL_EVENTS = [
|
|
124
|
+
SOCIAL_AUTH_STARTED,
|
|
125
|
+
SOCIAL_CALLBACK_RECEIVED,
|
|
126
|
+
SOCIAL_USER_INFO_FETCHED,
|
|
127
|
+
SOCIAL_ACCOUNT_CREATED,
|
|
128
|
+
SOCIAL_ACCOUNT_LINKED,
|
|
129
|
+
SOCIAL_AUTH_COMPLETED
|
|
130
|
+
].freeze
|
|
131
|
+
|
|
132
|
+
CREDENTIAL_EVENTS = [
|
|
133
|
+
CREDENTIAL_PASSWORD_CREATED,
|
|
134
|
+
CREDENTIAL_PASSWORD_RESET_INITIATED,
|
|
135
|
+
CREDENTIAL_PASSWORD_RESET_COMPLETED,
|
|
136
|
+
CREDENTIAL_PASSWORD_CHANGED,
|
|
137
|
+
CREDENTIAL_CLIENT_SECRET_CREATED,
|
|
138
|
+
CREDENTIAL_CLIENT_SECRET_ROTATED,
|
|
139
|
+
CREDENTIAL_CLIENT_SECRET_REVOKED
|
|
140
|
+
].freeze
|
|
141
|
+
|
|
142
|
+
ALL_EVENTS = (
|
|
143
|
+
AUTHENTICATION_EVENTS +
|
|
144
|
+
SESSION_EVENTS +
|
|
145
|
+
ACCOUNT_EVENTS +
|
|
146
|
+
IDENTIFIER_EVENTS +
|
|
147
|
+
OAUTH_EVENTS +
|
|
148
|
+
PASSWORDLESS_EVENTS +
|
|
149
|
+
SOCIAL_EVENTS +
|
|
150
|
+
CREDENTIAL_EVENTS
|
|
151
|
+
).freeze
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Include definitions at module level for convenience
|
|
155
|
+
include Definitions
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
# Event object that wraps ActiveSupport::Notifications event data
|
|
4
|
+
#
|
|
5
|
+
# Provides a clean interface for accessing event information in subscribers.
|
|
6
|
+
#
|
|
7
|
+
# @attr_reader [String] name The full namespaced event name
|
|
8
|
+
# @attr_reader [Hash] payload The event payload data
|
|
9
|
+
# @attr_reader [Time] started_at When the event started
|
|
10
|
+
# @attr_reader [Time] finished_at When the event finished
|
|
11
|
+
# @attr_reader [String] transaction_id Unique identifier for this event instance
|
|
12
|
+
#
|
|
13
|
+
class Event
|
|
14
|
+
attr_reader :name, :payload, :started_at, :finished_at, :transaction_id
|
|
15
|
+
|
|
16
|
+
# @param name [String] The full namespaced event name
|
|
17
|
+
# @param payload [Hash] The event payload
|
|
18
|
+
# @param started_at [Time] When the event started
|
|
19
|
+
# @param finished_at [Time] When the event finished
|
|
20
|
+
# @param transaction_id [String] Unique identifier for the event
|
|
21
|
+
def initialize(name:, payload:, started_at: nil, finished_at: nil, transaction_id: nil)
|
|
22
|
+
@name = name
|
|
23
|
+
@payload = payload.with_indifferent_access
|
|
24
|
+
@started_at = started_at
|
|
25
|
+
@finished_at = finished_at
|
|
26
|
+
@transaction_id = transaction_id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get the event name without the namespace prefix
|
|
30
|
+
#
|
|
31
|
+
# @return [String] The short event name
|
|
32
|
+
# @example
|
|
33
|
+
# event.short_name # => "authentication.attempt.succeeded"
|
|
34
|
+
#
|
|
35
|
+
def short_name
|
|
36
|
+
name.to_s.delete_prefix("#{Events::NAMESPACE}.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the event type from the payload
|
|
40
|
+
#
|
|
41
|
+
# @return [String, nil] The event type
|
|
42
|
+
#
|
|
43
|
+
def event_type
|
|
44
|
+
payload[:event_type]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get the unique event ID
|
|
48
|
+
#
|
|
49
|
+
# @return [String, nil] The event UUID
|
|
50
|
+
#
|
|
51
|
+
def event_id
|
|
52
|
+
payload[:event_id]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the event timestamp
|
|
56
|
+
#
|
|
57
|
+
# @return [String, nil] ISO8601 formatted timestamp
|
|
58
|
+
#
|
|
59
|
+
def timestamp
|
|
60
|
+
payload[:timestamp]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Calculate the duration of the event in milliseconds
|
|
64
|
+
#
|
|
65
|
+
# @return [Float, nil] Duration in milliseconds, or nil if timing not available
|
|
66
|
+
#
|
|
67
|
+
def duration_ms
|
|
68
|
+
return nil unless started_at && finished_at
|
|
69
|
+
(finished_at - started_at) * 1000
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Convenience method to access payload values
|
|
73
|
+
#
|
|
74
|
+
# @param key [Symbol, String] The payload key
|
|
75
|
+
# @return [Object] The value from the payload
|
|
76
|
+
#
|
|
77
|
+
def [](key)
|
|
78
|
+
payload[key]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if the payload contains a key
|
|
82
|
+
#
|
|
83
|
+
# @param key [Symbol, String] The payload key
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
#
|
|
86
|
+
def key?(key)
|
|
87
|
+
payload.key?(key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert event to a hash representation
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] The event as a hash
|
|
93
|
+
#
|
|
94
|
+
def to_h
|
|
95
|
+
{
|
|
96
|
+
name: name,
|
|
97
|
+
short_name: short_name,
|
|
98
|
+
transaction_id: transaction_id,
|
|
99
|
+
started_at: started_at&.iso8601,
|
|
100
|
+
finished_at: finished_at&.iso8601,
|
|
101
|
+
duration_ms: duration_ms,
|
|
102
|
+
payload: payload.to_h
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convert event to JSON
|
|
107
|
+
#
|
|
108
|
+
# @return [String] JSON representation
|
|
109
|
+
#
|
|
110
|
+
def to_json(*args)
|
|
111
|
+
to_h.to_json(*args)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# String representation for debugging
|
|
115
|
+
#
|
|
116
|
+
# @return [String]
|
|
117
|
+
#
|
|
118
|
+
def inspect
|
|
119
|
+
"#<#{self.class.name} name=#{name} event_id=#{event_id} duration_ms=#{duration_ms}>"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Subscribers
|
|
4
|
+
class AccountLockingSubscriber < Base
|
|
5
|
+
subscribe_to StandardId::Events::ACCOUNT_LOCKED
|
|
6
|
+
|
|
7
|
+
def call(event)
|
|
8
|
+
account = event[:account]
|
|
9
|
+
active_sessions = account.sessions.active
|
|
10
|
+
active_sessions.find_each do |session|
|
|
11
|
+
session.revoke!(reason: "account_locked")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Subscribers
|
|
4
|
+
class AccountStatusSubscriber < Base
|
|
5
|
+
subscribe_to StandardId::Events::ACCOUNT_DEACTIVATED
|
|
6
|
+
|
|
7
|
+
def call(event)
|
|
8
|
+
account = event[:account]
|
|
9
|
+
active_sessions = account.sessions.active
|
|
10
|
+
active_sessions.find_each do |session|
|
|
11
|
+
session.revoke!(reason: "account_deactivated")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Subscribers
|
|
4
|
+
# Base class for event subscribers
|
|
5
|
+
#
|
|
6
|
+
# @example Single event subscription
|
|
7
|
+
# class AuditSubscriber < StandardId::Events::Subscribers::Base
|
|
8
|
+
# subscribe_to StandardId::Events::AUTHENTICATION_SUCCEEDED
|
|
9
|
+
#
|
|
10
|
+
# def call(event)
|
|
11
|
+
# AuditLog.create!(
|
|
12
|
+
# event_type: event.short_name,
|
|
13
|
+
# account_id: event[:account]&.id,
|
|
14
|
+
# ip_address: event[:ip_address],
|
|
15
|
+
# metadata: event.payload
|
|
16
|
+
# )
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Multiple event subscription
|
|
21
|
+
# class SecurityAlertSubscriber < StandardId::Events::Subscribers::Base
|
|
22
|
+
# subscribe_to StandardId::Events::AUTHENTICATION_FAILED,
|
|
23
|
+
# StandardId::Events::SESSION_REVOKED,
|
|
24
|
+
# StandardId::Events::ACCOUNT_LOCKED
|
|
25
|
+
#
|
|
26
|
+
# def call(event)
|
|
27
|
+
# SecurityMailer.alert(event.to_h).deliver_later
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @example Pattern subscription
|
|
32
|
+
# class MetricsSubscriber < StandardId::Events::Subscribers::Base
|
|
33
|
+
# subscribe_to_pattern(/authentication/)
|
|
34
|
+
#
|
|
35
|
+
# def call(event)
|
|
36
|
+
# StatsD.increment("standard_id.#{event.short_name}")
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
class Base
|
|
41
|
+
class << self
|
|
42
|
+
# Subscribe to specific event(s)
|
|
43
|
+
#
|
|
44
|
+
# @param event_names [Array<String>] Event name constants
|
|
45
|
+
# @return [void]
|
|
46
|
+
#
|
|
47
|
+
def subscribe_to(*event_names)
|
|
48
|
+
@subscribed_events = event_names.flatten
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Subscribe to events matching a pattern
|
|
52
|
+
#
|
|
53
|
+
# @param pattern [Regexp] Pattern to match event names
|
|
54
|
+
# @return [void]
|
|
55
|
+
#
|
|
56
|
+
def subscribe_to_pattern(pattern)
|
|
57
|
+
@subscription_pattern = pattern
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get the subscribed events
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<String>]
|
|
63
|
+
#
|
|
64
|
+
def subscribed_events
|
|
65
|
+
@subscribed_events || []
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get the subscription pattern
|
|
69
|
+
#
|
|
70
|
+
# @return [Regexp, nil]
|
|
71
|
+
#
|
|
72
|
+
def subscription_pattern
|
|
73
|
+
@subscription_pattern
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Register this subscriber with the event system
|
|
77
|
+
#
|
|
78
|
+
# @return [Array<Object>] The subscription handles
|
|
79
|
+
#
|
|
80
|
+
def attach
|
|
81
|
+
instance = new
|
|
82
|
+
@subscriptions ||= []
|
|
83
|
+
|
|
84
|
+
if subscription_pattern
|
|
85
|
+
@subscriptions << Events.subscribe(subscription_pattern) do |event|
|
|
86
|
+
instance.handle(event)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
subscribed_events.each do |event_name|
|
|
91
|
+
@subscriptions << Events.subscribe(event_name) do |event|
|
|
92
|
+
instance.handle(event)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@subscriptions
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Unregister this subscriber from the event system
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
#
|
|
103
|
+
def detach
|
|
104
|
+
return unless @subscriptions
|
|
105
|
+
|
|
106
|
+
@subscriptions.each do |subscription|
|
|
107
|
+
Events.unsubscribe(subscription)
|
|
108
|
+
end
|
|
109
|
+
@subscriptions = []
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if the subscriber is currently attached
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
#
|
|
116
|
+
def attached?
|
|
117
|
+
@subscriptions&.any?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle an event
|
|
122
|
+
#
|
|
123
|
+
# Override this method to add custom handling logic like
|
|
124
|
+
# async processing or error handling. By default, it calls
|
|
125
|
+
# the `call` method.
|
|
126
|
+
#
|
|
127
|
+
# @param event [StandardId::Events::Event] The event to handle
|
|
128
|
+
# @return [void]
|
|
129
|
+
#
|
|
130
|
+
def handle(event)
|
|
131
|
+
call(event)
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
handle_error(e, event)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Process the event
|
|
137
|
+
#
|
|
138
|
+
# Subclasses must implement this method.
|
|
139
|
+
#
|
|
140
|
+
# @param event [StandardId::Events::Event] The event to process
|
|
141
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
142
|
+
#
|
|
143
|
+
def call(event)
|
|
144
|
+
raise NotImplementedError, "#{self.class.name} must implement #call"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Handle errors during event processing
|
|
148
|
+
#
|
|
149
|
+
# Override this method to customize error handling.
|
|
150
|
+
# By default, it logs the error and re-raises.
|
|
151
|
+
#
|
|
152
|
+
# @param error [StandardError] The error that occurred
|
|
153
|
+
# @param event [StandardId::Events::Event] The event being processed
|
|
154
|
+
# @raise [StandardError] Re-raises the error by default
|
|
155
|
+
#
|
|
156
|
+
def handle_error(error, event)
|
|
157
|
+
StandardId.logger.error(
|
|
158
|
+
"[StandardId::Events] Error in #{self.class.name} handling #{event.short_name}: #{error.message}"
|
|
159
|
+
)
|
|
160
|
+
raise error
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Subscribers
|
|
4
|
+
class LoggingSubscriber < Base
|
|
5
|
+
subscribe_to_pattern(/\Astandard_id\./)
|
|
6
|
+
|
|
7
|
+
LOG_LEVELS = {
|
|
8
|
+
"authentication.attempt.started" => :debug,
|
|
9
|
+
"authentication.attempt.succeeded" => :info,
|
|
10
|
+
"authentication.attempt.failed" => :warn,
|
|
11
|
+
"authentication.password.validated" => :debug,
|
|
12
|
+
"authentication.password.failed" => :warn,
|
|
13
|
+
"authentication.otp.validated" => :debug,
|
|
14
|
+
"authentication.otp.failed" => :warn,
|
|
15
|
+
"session.creating" => :debug,
|
|
16
|
+
"session.created" => :info,
|
|
17
|
+
"session.validating" => :debug,
|
|
18
|
+
"session.validated" => :debug,
|
|
19
|
+
"session.expired" => :info,
|
|
20
|
+
"session.revoked" => :info,
|
|
21
|
+
"session.refreshed" => :debug,
|
|
22
|
+
"account.creating" => :debug,
|
|
23
|
+
"account.created" => :info,
|
|
24
|
+
"account.verified" => :info,
|
|
25
|
+
"account.status_changed" => :warn,
|
|
26
|
+
"account.activated" => :info,
|
|
27
|
+
"account.deactivated" => :warn,
|
|
28
|
+
"account.locked" => :warn,
|
|
29
|
+
"account.unlocked" => :info,
|
|
30
|
+
"identifier.created" => :debug,
|
|
31
|
+
"identifier.verification.started" => :debug,
|
|
32
|
+
"identifier.verification.succeeded" => :info,
|
|
33
|
+
"identifier.verification.failed" => :warn,
|
|
34
|
+
"identifier.linked" => :info,
|
|
35
|
+
"oauth.authorization.requested" => :debug,
|
|
36
|
+
"oauth.authorization.granted" => :info,
|
|
37
|
+
"oauth.authorization.denied" => :info,
|
|
38
|
+
"oauth.token.issuing" => :debug,
|
|
39
|
+
"oauth.token.issued" => :info,
|
|
40
|
+
"oauth.token.refreshed" => :debug,
|
|
41
|
+
"oauth.code.consumed" => :debug,
|
|
42
|
+
"passwordless.code.requested" => :debug,
|
|
43
|
+
"passwordless.code.generated" => :debug,
|
|
44
|
+
"passwordless.code.sent" => :info,
|
|
45
|
+
"passwordless.code.verified" => :info,
|
|
46
|
+
"passwordless.code.failed" => :warn,
|
|
47
|
+
"passwordless.account.created" => :info,
|
|
48
|
+
"social.auth.started" => :debug,
|
|
49
|
+
"social.auth.callback_received" => :debug,
|
|
50
|
+
"social.user_info.fetched" => :debug,
|
|
51
|
+
"social.account.created" => :info,
|
|
52
|
+
"social.account.linked" => :info,
|
|
53
|
+
"social.auth.completed" => :info,
|
|
54
|
+
"credential.password.created" => :info,
|
|
55
|
+
"credential.password.reset_initiated" => :info,
|
|
56
|
+
"credential.password.reset_completed" => :info,
|
|
57
|
+
"credential.password.changed" => :info,
|
|
58
|
+
"credential.client_secret.created" => :info,
|
|
59
|
+
"credential.client_secret.rotated" => :warn
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
DEFAULT_LOG_LEVEL = :debug
|
|
63
|
+
|
|
64
|
+
def call(event)
|
|
65
|
+
return unless logging_enabled?
|
|
66
|
+
|
|
67
|
+
log_level = LOG_LEVELS.fetch(event.short_name, DEFAULT_LOG_LEVEL)
|
|
68
|
+
payload = build_payload(event, log_level)
|
|
69
|
+
|
|
70
|
+
case log_level
|
|
71
|
+
when :debug then StandardId.logger.debug(payload)
|
|
72
|
+
when :info then StandardId.logger.info(payload)
|
|
73
|
+
when :warn then StandardId.logger.warn(payload)
|
|
74
|
+
when :error then StandardId.logger.error(payload)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_error(error, event)
|
|
79
|
+
StandardId.logger.error({
|
|
80
|
+
subject: "standard_id.logging_subscriber.error",
|
|
81
|
+
event_type: event.short_name,
|
|
82
|
+
error: error.message
|
|
83
|
+
})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def logging_enabled?
|
|
89
|
+
config = StandardId.config
|
|
90
|
+
return true unless config.respond_to?(:events)
|
|
91
|
+
|
|
92
|
+
config.events.enable_logging
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_payload(event, log_level)
|
|
96
|
+
payload = {
|
|
97
|
+
subject: "standard_id.#{event.short_name}",
|
|
98
|
+
severity: log_level.to_s
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
payload[:duration] = event.duration_ms.round(2) if event.duration_ms
|
|
102
|
+
|
|
103
|
+
if (account = event[:account])
|
|
104
|
+
payload[:account_id] = account.respond_to?(:id) ? account.id : account
|
|
105
|
+
elsif event[:account_id]
|
|
106
|
+
payload[:account_id] = event[:account_id]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
payload[:login] = event[:account_lookup] || event[:login] || event[:username] if event[:account_lookup] || event[:login] || event[:username]
|
|
110
|
+
payload[:auth_method] = event[:auth_method] if event[:auth_method]
|
|
111
|
+
payload[:grant_type] = event[:grant_type] if event[:grant_type]
|
|
112
|
+
payload[:provider] = event[:provider] if event[:provider]
|
|
113
|
+
payload[:session_type] = event[:session_type] if event[:session_type]
|
|
114
|
+
payload[:ip_address] = event[:ip_address] if event[:ip_address]
|
|
115
|
+
payload[:error_code] = event[:error_code] if event[:error_code]
|
|
116
|
+
|
|
117
|
+
payload
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|