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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +529 -20
  3. data/app/controllers/concerns/standard_id/inertia_rendering.rb +49 -0
  4. data/app/controllers/concerns/standard_id/inertia_support.rb +31 -0
  5. data/app/controllers/concerns/standard_id/set_current_request_details.rb +19 -0
  6. data/app/controllers/concerns/standard_id/social_authentication.rb +86 -37
  7. data/app/controllers/concerns/standard_id/web_authentication.rb +50 -1
  8. data/app/controllers/standard_id/api/base_controller.rb +1 -0
  9. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +7 -18
  10. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +33 -37
  11. data/app/controllers/standard_id/web/base_controller.rb +1 -0
  12. data/app/controllers/standard_id/web/login_controller.rb +12 -21
  13. data/app/controllers/standard_id/web/signup_controller.rb +11 -8
  14. data/app/forms/standard_id/web/signup_form.rb +32 -1
  15. data/app/models/standard_id/browser_session.rb +8 -0
  16. data/app/models/standard_id/client_secret_credential.rb +11 -0
  17. data/app/models/standard_id/device_session.rb +4 -0
  18. data/app/models/standard_id/identifier.rb +28 -0
  19. data/app/models/standard_id/service_session.rb +1 -1
  20. data/app/models/standard_id/session.rb +16 -2
  21. data/app/views/standard_id/web/auth/callback/providers/{apple_mobile.html.erb → mobile_callback.html.erb} +1 -1
  22. data/config/routes/api.rb +1 -2
  23. data/config/routes/web.rb +4 -3
  24. data/lib/generators/standard_id/install/templates/standard_id.rb +19 -8
  25. data/lib/standard_config/config.rb +13 -12
  26. data/lib/standard_config/config_provider.rb +6 -6
  27. data/lib/standard_config/schema.rb +2 -2
  28. data/lib/standard_id/account_locking.rb +86 -0
  29. data/lib/standard_id/account_status.rb +45 -0
  30. data/lib/standard_id/api/authentication_guard.rb +40 -1
  31. data/lib/standard_id/api/token_manager.rb +1 -1
  32. data/lib/standard_id/config/schema.rb +13 -9
  33. data/lib/standard_id/current_attributes.rb +9 -0
  34. data/lib/standard_id/engine.rb +9 -0
  35. data/lib/standard_id/errors.rb +12 -0
  36. data/lib/standard_id/events/definitions.rb +157 -0
  37. data/lib/standard_id/events/event.rb +123 -0
  38. data/lib/standard_id/events/subscribers/account_locking_subscriber.rb +17 -0
  39. data/lib/standard_id/events/subscribers/account_status_subscriber.rb +17 -0
  40. data/lib/standard_id/events/subscribers/base.rb +165 -0
  41. data/lib/standard_id/events/subscribers/logging_subscriber.rb +122 -0
  42. data/lib/standard_id/events.rb +137 -0
  43. data/lib/standard_id/oauth/authorization_code_flow.rb +10 -0
  44. data/lib/standard_id/oauth/client_credentials_flow.rb +31 -0
  45. data/lib/standard_id/oauth/password_flow.rb +36 -4
  46. data/lib/standard_id/oauth/passwordless_otp_flow.rb +38 -2
  47. data/lib/standard_id/oauth/subflows/social_login_grant.rb +11 -22
  48. data/lib/standard_id/oauth/token_grant_flow.rb +22 -1
  49. data/lib/standard_id/passwordless/base_strategy.rb +32 -0
  50. data/lib/standard_id/provider_registry.rb +73 -0
  51. data/lib/standard_id/{social_providers → providers}/apple.rb +46 -7
  52. data/lib/standard_id/providers/base.rb +242 -0
  53. data/lib/standard_id/{social_providers → providers}/google.rb +26 -7
  54. data/lib/standard_id/version.rb +1 -1
  55. data/lib/standard_id/web/authentication_guard.rb +29 -0
  56. data/lib/standard_id/web/session_manager.rb +39 -1
  57. data/lib/standard_id/web/token_manager.rb +2 -2
  58. data/lib/standard_id.rb +13 -2
  59. metadata +20 -6
  60. data/lib/standard_id/social_providers/response_builder.rb +0 -18
@@ -2,64 +2,70 @@ module StandardId
2
2
  module SocialAuthentication
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ prepend_before_action :prepare_provider
7
+ end
8
+
5
9
  private
6
10
 
7
- def get_user_info_from_provider(connection, redirect_uri: nil, flow: :web)
8
- case connection
9
- when "google"
10
- StandardId::SocialProviders::Google.get_user_info(
11
- code: params[:code],
12
- id_token: params[:id_token],
13
- access_token: params[:access_token],
14
- redirect_uri: redirect_uri
15
- )
16
- when "apple"
17
- StandardId::SocialProviders::Apple.get_user_info(
18
- code: params[:code],
19
- id_token: params[:id_token],
20
- redirect_uri: redirect_uri,
21
- client_id: apple_client_id_for_flow(flow)
22
- )
23
- else
24
- raise StandardId::InvalidRequestError, "Unsupported provider: #{connection}"
25
- end
11
+ attr_reader :provider
12
+
13
+ def prepare_provider
14
+ @provider = StandardId::ProviderRegistry.get(params[:provider])
15
+ rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
16
+ raise StandardId::InvalidRequestError, e.message
26
17
  end
27
18
 
28
- def apple_client_id_for_flow(flow)
29
- flow == :web ? StandardId.config.apple_client_id : StandardId.config.apple_mobile_client_id
19
+ def get_user_info_from_provider(redirect_uri: nil, flow: :web)
20
+ provider_params = {
21
+ code: params[:code],
22
+ id_token: params[:id_token],
23
+ access_token: params[:access_token],
24
+ redirect_uri: redirect_uri
25
+ }
26
+
27
+ resolved_params = provider.resolve_params(provider_params, context: { flow: flow })
28
+ provider.get_user_info(**resolved_params.compact)
30
29
  end
31
30
 
32
- def find_or_create_account_from_social(raw_social_info, provider)
31
+ def find_or_create_account_from_social(raw_social_info)
33
32
  social_info = raw_social_info.to_h.with_indifferent_access
34
33
  email = social_info[:email]
35
- raise StandardId::InvalidRequestError, "No email provided by #{provider}" if email.blank?
34
+ raise StandardId::InvalidRequestError, "No email provided by #{provider.provider_name}" if email.blank?
35
+
36
+ emit_social_user_info_fetched(provider, social_info, email)
36
37
 
37
38
  identifier = StandardId::EmailIdentifier.find_by(value: email)
38
39
 
39
40
  if identifier.present?
41
+ emit_social_account_linked(identifier.account, provider, identifier)
40
42
  identifier.account
41
43
  else
42
- account = build_account_from_social(social_info, provider)
44
+ account = build_account_from_social(social_info)
43
45
  identifier = StandardId::EmailIdentifier.create!(
44
46
  account: account,
45
47
  value: email
46
48
  )
47
49
  identifier.verify! if identifier.respond_to?(:verify!)
50
+ emit_social_account_created(account, provider, social_info)
48
51
  account
49
52
  end
50
53
  end
51
54
 
52
- def build_account_from_social(social_info, provider)
53
- attrs = resolve_account_attributes(social_info, provider)
54
- StandardId.account_class.create!(attrs)
55
+ def build_account_from_social(social_info)
56
+ emit_account_creating_from_social(social_info)
57
+ attrs = resolve_account_attributes(social_info)
58
+ account = StandardId.account_class.create!(attrs)
59
+ emit_account_created_from_social(account)
60
+ account
55
61
  end
56
62
 
57
- def resolve_account_attributes(social_info, provider)
63
+ def resolve_account_attributes(social_info)
58
64
  resolver = StandardId.config.social_account_attributes
59
65
  attrs = if resolver.respond_to?(:call)
60
66
  payload = {
61
67
  social_info: social_info,
62
- provider: provider
68
+ provider: provider.provider_name
63
69
  }
64
70
 
65
71
  filtered_payload = StandardId::Utils::CallableParameterFilter.filter(resolver, payload)
@@ -95,18 +101,61 @@ module StandardId
95
101
  end
96
102
 
97
103
  def run_social_callback(provider:, social_info:, provider_tokens:, account:)
98
- callback = StandardId.config.social_callback
99
- return if callback.blank?
104
+ emit_social_auth_completed(provider, social_info, provider_tokens, account)
105
+ end
100
106
 
101
- payload = {
107
+ def emit_social_user_info_fetched(provider, social_info, email)
108
+ StandardId::Events.publish(
109
+ StandardId::Events::SOCIAL_USER_INFO_FETCHED,
102
110
  provider: provider,
103
111
  social_info: social_info,
104
- tokens: provider_tokens.presence,
105
- account: account
106
- }
112
+ email: email
113
+ )
114
+ end
115
+
116
+ def emit_social_account_created(account, provider, social_info)
117
+ StandardId::Events.publish(
118
+ StandardId::Events::SOCIAL_ACCOUNT_CREATED,
119
+ account: account,
120
+ provider: provider,
121
+ social_info: social_info
122
+ )
123
+ end
124
+
125
+ def emit_social_account_linked(account, provider, identifier)
126
+ StandardId::Events.publish(
127
+ StandardId::Events::SOCIAL_ACCOUNT_LINKED,
128
+ account: account,
129
+ provider: provider,
130
+ identifier: identifier
131
+ )
132
+ end
133
+
134
+ def emit_social_auth_completed(provider, social_info, provider_tokens, account)
135
+ StandardId::Events.publish(
136
+ StandardId::Events::SOCIAL_AUTH_COMPLETED,
137
+ account: account,
138
+ provider: provider,
139
+ social_info: social_info,
140
+ tokens: provider_tokens
141
+ )
142
+ end
143
+
144
+ def emit_account_creating_from_social(social_info)
145
+ StandardId::Events.publish(
146
+ StandardId::Events::ACCOUNT_CREATING,
147
+ account_params: resolve_account_attributes(social_info),
148
+ auth_method: "social:#{provider.provider_name}"
149
+ )
150
+ end
107
151
 
108
- filtered_payload = StandardId::Utils::CallableParameterFilter.filter(callback, payload)
109
- callback.call(**filtered_payload.symbolize_keys)
152
+ def emit_account_created_from_social(account)
153
+ StandardId::Events.publish(
154
+ StandardId::Events::ACCOUNT_CREATED,
155
+ account: account,
156
+ auth_method: "social:#{provider.provider_name}",
157
+ source: "social"
158
+ )
110
159
  end
111
160
  end
112
161
  end
@@ -3,6 +3,7 @@ module StandardId
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ include StandardId::InertiaSupport
6
7
  helper_method :current_account, :authenticated?
7
8
  end
8
9
 
@@ -18,6 +19,26 @@ module StandardId
18
19
  authentication_guard.require_session!(session_manager, session: session, request: request)
19
20
  end
20
21
 
22
+ # Require authentication with redirect to login page instead of raising an error.
23
+ # Use this for pages that should redirect unauthenticated users to login.
24
+ def authenticate_account!
25
+ return if authenticated?
26
+
27
+ store_location_for_redirect
28
+ redirect_to_login
29
+ end
30
+
31
+ # Store the current URL to redirect back after authentication
32
+ def store_location_for_redirect
33
+ session[:return_to_after_authenticating] = request.url if request.get?
34
+ end
35
+
36
+ # Redirect to login page, handling both Inertia and standard requests
37
+ def redirect_to_login
38
+ login_path = StandardId.config.login_url.presence || "/login"
39
+ redirect_with_inertia login_path
40
+ end
41
+
21
42
  def after_authentication_url
22
43
  # TODO: add configurable value
23
44
  session.delete(:return_to_after_authenticating) || "/"
@@ -28,11 +49,39 @@ module StandardId
28
49
  password = login_params[:password]
29
50
  remember_me = ActiveModel::Type::Boolean.new.cast(login_params[:remember_me])
30
51
 
52
+ StandardId::Events.publish(
53
+ StandardId::Events::AUTHENTICATION_ATTEMPT_STARTED,
54
+ account_lookup: login,
55
+ auth_method: "password"
56
+ )
57
+
31
58
  StandardId::PasswordCredential.find_by(login:).tap do |password_credential|
32
- return nil unless password_credential&.authenticate(password)
59
+ unless password_credential&.authenticate(password)
60
+ StandardId::Events.publish(
61
+ StandardId::Events::AUTHENTICATION_FAILED,
62
+ account_lookup: login,
63
+ auth_method: "password",
64
+ error_code: "invalid_credentials",
65
+ error_message: "Invalid login or password"
66
+ )
67
+ return nil
68
+ end
69
+
70
+ StandardId::Events.publish(
71
+ StandardId::Events::PASSWORD_VALIDATED,
72
+ account: password_credential.account,
73
+ credential_id: password_credential.id
74
+ )
33
75
 
34
76
  session_manager.sign_in_account(password_credential.account)
35
77
  session_manager.set_remember_cookie(password_credential) if remember_me
78
+
79
+ StandardId::Events.publish(
80
+ StandardId::Events::AUTHENTICATION_SUCCEEDED,
81
+ account: password_credential.account,
82
+ auth_method: "password",
83
+ session_type: "browser"
84
+ )
36
85
  end
37
86
  end
38
87
 
@@ -2,6 +2,7 @@ module StandardId
2
2
  module Api
3
3
  class BaseController < ActionController::API
4
4
  include StandardId::ApiAuthentication
5
+ include StandardId::SetCurrentRequestDetails
5
6
 
6
7
  before_action :validate_content_type!
7
8
 
@@ -6,37 +6,24 @@ module StandardId
6
6
 
7
7
  skip_before_action :validate_content_type!
8
8
 
9
- def google
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
- flow = resolve_flow_for(connection)
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, connection)
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: 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: connection,
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: [:google, :apple, :apple_mobile]
12
- skip_before_action :verify_authenticity_token, only: [:apple, :apple_mobile]
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 google
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 = connection == "apple" ? apple_callback_url : google_callback_url
50
- provider_response = get_user_info_from_provider(connection, redirect_uri: redirect_uri)
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, connection)
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: connection,
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 #{connection.humanize}" }
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 google_callback_url
73
- auth_callback_google_url
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 apple_callback_url
77
- auth_callback_apple_url
72
+ def skip_csrf_verification?
73
+ provider.skip_csrf?
78
74
  end
79
75
 
80
76
  def decode_state_params
@@ -2,6 +2,7 @@ module StandardId
2
2
  module Web
3
3
  class BaseController < ApplicationController
4
4
  include StandardId::WebAuthentication
5
+ include StandardId::SetCurrentRequestDetails
5
6
 
6
7
  include StandardId::WebEngine.routes.url_helpers
7
8
  helper StandardId::WebEngine.routes.url_helpers
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class LoginController < BaseController
4
+ include StandardId::InertiaRendering
5
+
4
6
  layout "public"
5
7
 
6
8
  skip_before_action :require_browser_session!, only: [:show, :create]
@@ -11,6 +13,8 @@ module StandardId
11
13
  def show
12
14
  @redirect_uri = params[:redirect_uri] || after_authentication_url
13
15
  @connection = params[:connection]
16
+
17
+ render_with_inertia props: auth_page_props
14
18
  end
15
19
 
16
20
  def create
@@ -18,7 +22,7 @@ module StandardId
18
22
  redirect_to params[:redirect_uri] || after_authentication_url, status: :see_other, notice: "Successfully signed in"
19
23
  else
20
24
  flash.now[:alert] = "Invalid email or password"
21
- render :show, status: :unprocessable_content
25
+ render_with_inertia action: :show, props: auth_page_props, status: :unprocessable_content
22
26
  end
23
27
  end
24
28
 
@@ -29,32 +33,19 @@ module StandardId
29
33
  end
30
34
 
31
35
  def redirect_if_social_login
32
- redirect_to social_login_url, allow_other_host: true if params[:connection].present?
36
+ redirect_with_inertia social_login_url, allow_other_host: true if params[:connection].present?
33
37
  end
34
38
 
35
39
  def social_login_url
36
- case params[:connection]
37
- when "google"
38
- google_authorization_url
39
- when "apple"
40
- apple_authorization_url
41
- else
42
- raise StandardId::InvalidRequestError, "Unsupported social connection: #{connection}"
43
- end
44
- end
45
-
46
- def google_authorization_url
47
- StandardId::SocialProviders::Google.authorization_url(
48
- state: encode_state,
49
- redirect_uri: auth_callback_google_url
50
- )
51
- end
40
+ connection = params[:connection]
41
+ provider = StandardId::ProviderRegistry.get(connection)
52
42
 
53
- def apple_authorization_url
54
- StandardId::SocialProviders::Apple.authorization_url(
43
+ provider.authorization_url(
55
44
  state: encode_state,
56
- redirect_uri: auth_callback_apple_url
45
+ redirect_uri: "#{request.base_url}#{provider.callback_path}"
57
46
  )
47
+ rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
48
+ raise StandardId::InvalidRequestError, e.message
58
49
  end
59
50
 
60
51
  def encode_state
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class SignupController < BaseController
4
+ include StandardId::InertiaRendering
5
+
4
6
  layout "public"
5
7
 
6
8
  skip_before_action :require_browser_session!, only: [:show, :create]
@@ -11,6 +13,8 @@ module StandardId
11
13
  def show
12
14
  @redirect_uri = params[:redirect_uri] || after_authentication_url
13
15
  @connection = params[:connection] # For social login detection
16
+
17
+ render_with_inertia props: auth_page_props
14
18
  end
15
19
 
16
20
  def create
@@ -24,7 +28,7 @@ module StandardId
24
28
  end
25
29
 
26
30
  def redirect_if_social_login
27
- redirect_to social_signup_url, allow_other_host: true if params[:connection].present?
31
+ redirect_with_inertia social_signup_url, allow_other_host: true if params[:connection].present?
28
32
  end
29
33
 
30
34
  def handle_password_signup
@@ -35,8 +39,10 @@ module StandardId
35
39
  redirect_to params[:redirect_uri] || after_authentication_url,
36
40
  notice: "Account created successfully"
37
41
  else
42
+ @redirect_uri = params[:redirect_uri] || after_authentication_url
43
+ @connection = params[:connection]
38
44
  flash.now[:alert] = form.errors.full_messages.join(", ")
39
- render :show, status: :unprocessable_content
45
+ render_with_inertia action: :show, props: auth_page_props(errors: form.errors.to_hash), status: :unprocessable_content
40
46
  end
41
47
  end
42
48
 
@@ -55,12 +61,9 @@ module StandardId
55
61
  end
56
62
 
57
63
  def callback_url
58
- case params[:connection]
59
- when "google"
60
- auth_callback_google_url
61
- when "apple"
62
- auth_callback_apple_url
63
- end
64
+ connection = params[:connection]
65
+ provider = StandardId::ProviderRegistry.get(connection)
66
+ "#{request.base_url}#{provider.callback_path}"
64
67
  end
65
68
 
66
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
- StandardId::PasswordCredential.create!(
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
@@ -3,6 +3,10 @@ module StandardId
3
3
  validates :device_id, presence: true
4
4
  validates :device_agent, presence: true
5
5
 
6
+ def self.expiry
7
+ StandardId.config.session.device_session_lifetime.seconds.from_now
8
+ end
9
+
6
10
  def device_info
7
11
  return {} if device_agent.blank?
8
12