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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module AccountStatus
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
enum :status, { active: "active", inactive: "inactive" }, default: :active
|
|
7
|
+
|
|
8
|
+
after_commit :emit_account_status_changed_event, on: :update, if: :status_previously_changed?
|
|
9
|
+
|
|
10
|
+
StandardId::Events.subscribe(
|
|
11
|
+
StandardId::Events::OAUTH_TOKEN_ISSUING,
|
|
12
|
+
StandardId::Events::SESSION_CREATING,
|
|
13
|
+
StandardId::Events::SESSION_VALIDATING
|
|
14
|
+
) do |event|
|
|
15
|
+
account = event[:account]
|
|
16
|
+
if account&.inactive?
|
|
17
|
+
raise StandardId::AccountDeactivatedError, "Account is deactivated"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def activate!
|
|
23
|
+
return true if active?
|
|
24
|
+
|
|
25
|
+
update!(status: :active, activated_at: Time.current)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deactivate!
|
|
29
|
+
return true if inactive?
|
|
30
|
+
|
|
31
|
+
update!(status: :inactive, deactivated_at: Time.current)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def emit_account_status_changed_event
|
|
37
|
+
event = inactive? ? StandardId::Events::ACCOUNT_DEACTIVATED : StandardId::Events::ACCOUNT_ACTIVATED
|
|
38
|
+
StandardId::Events.publish(
|
|
39
|
+
event,
|
|
40
|
+
account: self,
|
|
41
|
+
previous_status: status_previously_was
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Api
|
|
3
3
|
class AuthenticationGuard
|
|
4
|
-
def require_session!(session_manager)
|
|
4
|
+
def require_session!(session_manager, request: nil)
|
|
5
5
|
api_session = session_manager.current_session
|
|
6
|
+
emit_session_validating(api_session, request)
|
|
6
7
|
|
|
7
8
|
if api_session.blank?
|
|
8
9
|
raise StandardId::NotAuthenticatedError, "Invalid or missing access token"
|
|
9
10
|
elsif api_session.respond_to?(:expired?) && api_session.expired?
|
|
11
|
+
emit_session_expired(api_session)
|
|
10
12
|
raise StandardId::ExpiredSessionError, "Session has expired"
|
|
11
13
|
elsif api_session.respond_to?(:revoked?) && api_session.revoked?
|
|
12
14
|
session_manager.clear_session!
|
|
13
15
|
raise StandardId::RevokedSessionError, "Session has been revoked"
|
|
14
16
|
end
|
|
15
17
|
|
|
18
|
+
emit_session_validated(api_session)
|
|
16
19
|
api_session
|
|
17
20
|
end
|
|
18
21
|
|
|
@@ -51,6 +54,42 @@ module StandardId
|
|
|
51
54
|
raise ArgumentError, "Scopes must be provided as a String, Symbol, or Array"
|
|
52
55
|
end
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
def emit_session_validating(api_session, request)
|
|
59
|
+
StandardId::Events.publish(
|
|
60
|
+
StandardId::Events::SESSION_VALIDATING,
|
|
61
|
+
session: api_session
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def emit_session_validated(api_session)
|
|
66
|
+
account = if api_session.respond_to?(:account)
|
|
67
|
+
api_session.account
|
|
68
|
+
elsif api_session.respond_to?(:account_id)
|
|
69
|
+
StandardId.account_class.find_by(id: api_session.account_id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
StandardId::Events.publish(
|
|
73
|
+
StandardId::Events::SESSION_VALIDATED,
|
|
74
|
+
session: api_session,
|
|
75
|
+
account: account
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def emit_session_expired(api_session)
|
|
80
|
+
account = if api_session.respond_to?(:account)
|
|
81
|
+
api_session.account
|
|
82
|
+
elsif api_session.respond_to?(:account_id)
|
|
83
|
+
StandardId.account_class.find_by(id: api_session.account_id)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
StandardId::Events.publish(
|
|
87
|
+
StandardId::Events::SESSION_EXPIRED,
|
|
88
|
+
session: api_session,
|
|
89
|
+
account: account,
|
|
90
|
+
expired_at: api_session.respond_to?(:expires_at) ? api_session.expires_at : nil
|
|
91
|
+
)
|
|
92
|
+
end
|
|
54
93
|
end
|
|
55
94
|
end
|
|
56
95
|
end
|
|
@@ -13,7 +13,7 @@ module StandardId
|
|
|
13
13
|
ip_address: @request.remote_ip,
|
|
14
14
|
device_id: device_id || SecureRandom.uuid,
|
|
15
15
|
device_agent: device_agent || @request.user_agent,
|
|
16
|
-
expires_at:
|
|
16
|
+
expires_at: StandardId::DeviceSession.expiry
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -18,6 +18,10 @@ StandardConfig.schema.draw do
|
|
|
18
18
|
field :inertia_component_namespace, type: :string, default: "standard_id"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
scope :events do
|
|
22
|
+
field :enable_logging, type: :boolean, default: false
|
|
23
|
+
end
|
|
24
|
+
|
|
21
25
|
scope :passwordless do
|
|
22
26
|
field :code_ttl, type: :integer, default: 600 # 10 minutes in seconds
|
|
23
27
|
field :max_attempts, type: :integer, default: 3
|
|
@@ -31,6 +35,13 @@ StandardConfig.schema.draw do
|
|
|
31
35
|
field :require_numbers, type: :boolean, default: false
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
scope :session do
|
|
39
|
+
field :browser_session_lifetime, type: :integer, default: 86400 # 24 hours in seconds
|
|
40
|
+
field :browser_session_remember_me_lifetime, type: :integer, default: 2592000 # 30 days in seconds
|
|
41
|
+
field :device_session_lifetime, type: :integer, default: 2592000 # 30 days in seconds
|
|
42
|
+
field :service_session_lifetime, type: :integer, default: 7776000 # 90 days in seconds
|
|
43
|
+
end
|
|
44
|
+
|
|
34
45
|
scope :oauth do
|
|
35
46
|
field :default_token_lifetime, type: :integer, default: 3600 # 1 hour in seconds
|
|
36
47
|
field :refresh_token_lifetime, type: :integer, default: 2592000 # 30 days in seconds
|
|
@@ -42,16 +53,7 @@ StandardConfig.schema.draw do
|
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
scope :social do
|
|
45
|
-
field :google_client_id, type: :string, default: nil
|
|
46
|
-
field :google_client_secret, type: :string, default: nil
|
|
47
|
-
field :apple_client_id, type: :string, default: nil
|
|
48
|
-
field :apple_client_secret, type: :string, default: nil
|
|
49
|
-
field :apple_private_key, type: :string, default: nil
|
|
50
|
-
field :apple_key_id, type: :string, default: nil
|
|
51
|
-
field :apple_team_id, type: :string, default: nil
|
|
52
|
-
field :apple_mobile_client_id, type: :string, default: nil
|
|
53
56
|
field :social_account_attributes, type: :any, default: nil
|
|
54
57
|
field :allowed_redirect_url_prefixes, type: :array, default: []
|
|
55
|
-
field :social_callback, type: :any, default: nil
|
|
56
58
|
end
|
|
57
59
|
end
|
data/lib/standard_id/engine.rb
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace StandardId
|
|
4
|
+
|
|
5
|
+
config.after_initialize do
|
|
6
|
+
if StandardId.config.events.enable_logging
|
|
7
|
+
StandardId::Events::Subscribers::LoggingSubscriber.attach
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
StandardId::Events::Subscribers::AccountStatusSubscriber.attach
|
|
11
|
+
StandardId::Events::Subscribers::AccountLockingSubscriber.attach
|
|
12
|
+
end
|
|
4
13
|
end
|
|
5
14
|
end
|
data/lib/standard_id/errors.rb
CHANGED
|
@@ -4,6 +4,18 @@ module StandardId
|
|
|
4
4
|
class InvalidSessionError < StandardError; end
|
|
5
5
|
class ExpiredSessionError < InvalidSessionError; end
|
|
6
6
|
class RevokedSessionError < InvalidSessionError; end
|
|
7
|
+
class AccountDeactivatedError < StandardError; end
|
|
8
|
+
|
|
9
|
+
class AccountLockedError < StandardError
|
|
10
|
+
attr_reader :account, :lock_reason, :locked_at
|
|
11
|
+
|
|
12
|
+
def initialize(account)
|
|
13
|
+
@account = account
|
|
14
|
+
@lock_reason = account.lock_reason
|
|
15
|
+
@locked_at = account.locked_at
|
|
16
|
+
super("Account has been locked#{lock_reason ? ": #{lock_reason}" : ""}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
7
19
|
|
|
8
20
|
class OAuthError < StandardError
|
|
9
21
|
def oauth_error_code
|
|
@@ -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
|