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
@@ -0,0 +1,137 @@
1
+ require_relative "events/definitions"
2
+ require_relative "events/event"
3
+
4
+ module StandardId
5
+ module Events
6
+ # Event namespace prefix for all StandardId events
7
+ NAMESPACE = "standard_id"
8
+
9
+ class << self
10
+ # Publish an event with the given name and payload
11
+ #
12
+ # @param event_name [String, Symbol] The event name (use constants from Definitions)
13
+ # @param payload [Hash] The event payload data
14
+ # @yield [Hash] Optional block that receives the payload, useful for lazy evaluation
15
+ # @return [void]
16
+ #
17
+ # @example Simple publish
18
+ # StandardId::Events.publish(:authentication_succeeded, account: user)
19
+ #
20
+ # @example With block for lazy payload
21
+ # StandardId::Events.publish(:authentication_succeeded) do
22
+ # { account: expensive_lookup, duration_ms: calculate_duration }
23
+ # end
24
+ #
25
+ def publish(event_name, payload = {}, &block)
26
+ event_payload = block ? block.call.merge(payload) : payload
27
+ full_event_name = namespaced_event_name(event_name)
28
+
29
+ # Add standard metadata to all events
30
+ enriched_payload = enrich_payload(event_payload, event_name)
31
+
32
+ ActiveSupport::Notifications.instrument(full_event_name, enriched_payload)
33
+ rescue ActiveSupport::Notifications::InstrumentationSubscriberError => e
34
+ # Re-raise the first exception only (stop at first failure)
35
+ # This prevents confusing "multiple exceptions" messages when
36
+ # multiple guards (e.g., AccountStatus + AccountLocking) both fail
37
+ raise e.exceptions.first if e.exceptions.any?
38
+ end
39
+
40
+ # Subscribe to an event with a block or callable
41
+ #
42
+ # @param event_names [String, Symbol, Array<String, Symbol>] The event name(s) to subscribe to
43
+ # @yield [StandardId::Events::Event] The event object with name, payload, and timing
44
+ # @return [ActiveSupport::Notifications::Fanout::Subscribers::Evented, Array] The subscription(s)
45
+ #
46
+ # @example Block subscription
47
+ # StandardId::Events.subscribe(:authentication_succeeded) do |event|
48
+ # puts "Login from #{event.payload[:ip_address]}"
49
+ # end
50
+ #
51
+ # @example Multiple events subscription
52
+ # StandardId::Events.subscribe(:session_creating, :session_validating) do |event|
53
+ # check_account_status(event)
54
+ # end
55
+ #
56
+ # @example Pattern subscription (subscribe to all authentication events)
57
+ # StandardId::Events.subscribe(/authentication/) do |event|
58
+ # audit_log(event)
59
+ # end
60
+ #
61
+ def subscribe(*event_names, &block)
62
+ event_names = event_names.flatten
63
+
64
+ if event_names.size == 1
65
+ subscribe_single(event_names.first, &block)
66
+ else
67
+ event_names.map { |event_name| subscribe_single(event_name, &block) }
68
+ end
69
+ end
70
+
71
+ # Subscribe to an event pattern using a regex
72
+ #
73
+ # @param pattern [Regexp] The pattern to match event names
74
+ # @yield [StandardId::Events::Event] The event object
75
+ # @return [ActiveSupport::Notifications::Fanout::Subscribers::Evented] The subscription
76
+ #
77
+ def subscribe_to_pattern(pattern, &block)
78
+ subscribe_single(pattern, &block)
79
+ end
80
+
81
+ # Unsubscribe from events
82
+ #
83
+ # @param subscribers [Object, Array<Object>] The subscriber(s) returned from subscribe()
84
+ # @return [void]
85
+ #
86
+ def unsubscribe(*subscribers)
87
+ subscribers.flatten.each do |subscriber|
88
+ ActiveSupport::Notifications.unsubscribe(subscriber)
89
+ end
90
+ end
91
+
92
+ # Get the full namespaced event name
93
+ #
94
+ # @param event_name [String, Symbol] The short event name
95
+ # @return [String] The full namespaced event name
96
+ #
97
+ def namespaced_event_name(event_name)
98
+ return event_name.to_s if event_name.to_s.start_with?("#{NAMESPACE}.")
99
+
100
+ "#{NAMESPACE}.#{event_name}"
101
+ end
102
+
103
+ private
104
+
105
+ def subscribe_single(event_name, &block)
106
+ pattern = event_name.is_a?(Regexp) ? event_name : namespaced_event_name(event_name)
107
+
108
+ ActiveSupport::Notifications.subscribe(pattern) do |name, start, finish, id, payload|
109
+ event = Event.new(
110
+ name: name,
111
+ payload: payload,
112
+ started_at: start,
113
+ finished_at: finish,
114
+ transaction_id: id
115
+ )
116
+ block.call(event)
117
+ end
118
+ end
119
+
120
+ def enrich_payload(payload, event_name)
121
+ enriched = {
122
+ event_type: event_name.to_s,
123
+ event_id: SecureRandom.uuid,
124
+ timestamp: Time.current.iso8601
125
+ }
126
+
127
+ if defined?(::Current) && ::Current.respond_to?(:request_id)
128
+ enriched[:request_id] = ::Current.request_id if ::Current.request_id.present?
129
+ enriched[:ip_address] ||= ::Current.ip_address if ::Current.respond_to?(:ip_address) && ::Current.ip_address.present?
130
+ enriched[:user_agent] ||= ::Current.user_agent if ::Current.respond_to?(:user_agent) && ::Current.user_agent.present?
131
+ end
132
+
133
+ enriched.merge(payload)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -21,10 +21,20 @@ module StandardId
21
21
  end
22
22
 
23
23
  @authorization_code.mark_as_used!
24
+ emit_code_consumed
24
25
  end
25
26
 
26
27
  private
27
28
 
29
+ def emit_code_consumed
30
+ StandardId::Events.publish(
31
+ StandardId::Events::OAUTH_CODE_CONSUMED,
32
+ authorization_code: @authorization_code,
33
+ client_id: @credential.client_id,
34
+ account: @authorization_code.account
35
+ )
36
+ end
37
+
28
38
  def subject_id
29
39
  @authorization_code.account_id
30
40
  end
@@ -5,7 +5,12 @@ module StandardId
5
5
  permit_params :organization
6
6
 
7
7
  def authenticate!
8
+ emit_authentication_started
8
9
  @credential = validate_client_secret!(params[:client_id], params[:client_secret])
10
+ emit_authentication_succeeded
11
+ rescue StandardId::InvalidClientError => e
12
+ emit_authentication_failed(e.message)
13
+ raise
9
14
  end
10
15
 
11
16
  private
@@ -37,6 +42,32 @@ module StandardId
37
42
  def token_account
38
43
  nil
39
44
  end
45
+
46
+ def emit_authentication_started
47
+ StandardId::Events.publish(
48
+ StandardId::Events::AUTHENTICATION_ATTEMPT_STARTED,
49
+ account_lookup: params[:client_id],
50
+ auth_method: "client_credentials"
51
+ )
52
+ end
53
+
54
+ def emit_authentication_succeeded
55
+ StandardId::Events.publish(
56
+ StandardId::Events::AUTHENTICATION_SUCCEEDED,
57
+ client_application: @credential&.client_application,
58
+ auth_method: "client_credentials"
59
+ )
60
+ end
61
+
62
+ def emit_authentication_failed(error_message)
63
+ StandardId::Events.publish(
64
+ StandardId::Events::AUTHENTICATION_FAILED,
65
+ account_lookup: params[:client_id],
66
+ auth_method: "client_credentials",
67
+ error_code: "invalid_client",
68
+ error_message: error_message
69
+ )
70
+ end
40
71
  end
41
72
  end
42
73
  end
@@ -6,15 +6,47 @@ module StandardId
6
6
 
7
7
  def authenticate!
8
8
  validate_client_secret!(params[:client_id], params[:client_secret]) if params[:client_secret].present?
9
+ emit_authentication_started
9
10
 
10
11
  @account = authenticate_account(params[:username], params[:password])
11
- raise StandardId::InvalidGrantError, "Invalid username or password" if @account.blank?
12
12
 
13
+ if @account.blank?
14
+ emit_authentication_failed
15
+ raise StandardId::InvalidGrantError, "Invalid username or password"
16
+ end
17
+
18
+ emit_password_validated
13
19
  validate_requested_scope!
14
20
  end
15
21
 
16
22
  private
17
23
 
24
+ def emit_authentication_started
25
+ StandardId::Events.publish(
26
+ StandardId::Events::AUTHENTICATION_ATTEMPT_STARTED,
27
+ account_lookup: params[:username],
28
+ auth_method: "password"
29
+ )
30
+ end
31
+
32
+ def emit_authentication_failed
33
+ StandardId::Events.publish(
34
+ StandardId::Events::AUTHENTICATION_FAILED,
35
+ account_lookup: params[:username],
36
+ auth_method: "password",
37
+ error_code: "invalid_credentials",
38
+ error_message: "Invalid username or password"
39
+ )
40
+ end
41
+
42
+ def emit_password_validated
43
+ StandardId::Events.publish(
44
+ StandardId::Events::PASSWORD_VALIDATED,
45
+ account: @account,
46
+ credential_id: @credential&.id
47
+ )
48
+ end
49
+
18
50
  def subject_id
19
51
  @account.id
20
52
  end
@@ -40,11 +72,11 @@ module StandardId
40
72
  end
41
73
 
42
74
  def authenticate_account(username, password)
43
- StandardId::PasswordCredential
75
+ @credential = StandardId::PasswordCredential
44
76
  .includes(credential: :account)
45
77
  .find_by(login: username)
46
- &.authenticate(password)
47
- &.account
78
+
79
+ @credential&.authenticate(password)&.account
48
80
  end
49
81
 
50
82
  def validate_requested_scope!
@@ -7,16 +7,52 @@ module StandardId
7
7
  def authenticate!
8
8
  validate_client_secret!(params[:client_id], params[:client_secret]) if params[:client_secret].present?
9
9
 
10
- raise StandardId::InvalidGrantError, "Invalid or expired verification code" if code_challenge.blank?
11
- raise StandardId::InvalidGrantError, "Unable to authenticate user" if account.blank?
10
+ if code_challenge.blank?
11
+ emit_otp_validation_failed
12
+ raise StandardId::InvalidGrantError, "Invalid or expired verification code"
13
+ end
14
+
15
+ if account.blank?
16
+ raise StandardId::InvalidGrantError, "Unable to authenticate user"
17
+ end
12
18
 
13
19
  validate_requested_scope!
14
20
 
15
21
  code_challenge.use!
22
+ emit_otp_validated
16
23
  end
17
24
 
18
25
  private
19
26
 
27
+ def emit_otp_validated
28
+ StandardId::Events.publish(
29
+ StandardId::Events::OTP_VALIDATED,
30
+ account: account,
31
+ channel: params[:connection]
32
+ )
33
+ StandardId::Events.publish(
34
+ StandardId::Events::PASSWORDLESS_CODE_VERIFIED,
35
+ code_challenge: code_challenge,
36
+ account: account,
37
+ channel: params[:connection]
38
+ )
39
+ end
40
+
41
+ def emit_otp_validation_failed
42
+ StandardId::Events.publish(
43
+ StandardId::Events::OTP_VALIDATION_FAILED,
44
+ identifier: params[:username],
45
+ channel: params[:connection],
46
+ attempts: nil
47
+ )
48
+ StandardId::Events.publish(
49
+ StandardId::Events::PASSWORDLESS_CODE_FAILED,
50
+ identifier: params[:username],
51
+ channel: params[:connection],
52
+ attempts: nil
53
+ )
54
+ end
55
+
20
56
  def subject_id
21
57
  account.id
22
58
  end
@@ -9,29 +9,18 @@ module StandardId
9
9
  private
10
10
 
11
11
  def social_provider_url
12
- @social_provider_url ||= case params[:connection]
13
- when "google"
14
- build_google_oauth_url
15
- when "apple"
16
- build_apple_oauth_url
17
- else
18
- raise StandardId::InvalidRequestError, "Unsupported connection: #{params[:connection]}"
19
- end
20
- end
12
+ @social_provider_url ||= begin
13
+ connection = params[:connection]
14
+ provider = StandardId::ProviderRegistry.get(connection)
21
15
 
22
- def build_google_oauth_url
23
- StandardId::SocialProviders::Google.authorization_url(
24
- state: encode_state_with_original_params,
25
- redirect_uri: "#{params[:base_url]}/api/oauth/callback/google",
26
- scope: "openid email profile"
27
- )
28
- end
29
-
30
- def build_apple_oauth_url
31
- StandardId::SocialProviders::Apple.authorization_url(
32
- state: encode_state_with_original_params,
33
- redirect_uri: "#{params[:base_url]}/api/oauth/callback/apple"
34
- )
16
+ provider.authorization_url(
17
+ state: encode_state_with_original_params,
18
+ redirect_uri: "#{params[:base_url]}/api/oauth/callback/#{connection}",
19
+ scope: provider.default_scope
20
+ )
21
+ rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
22
+ raise StandardId::InvalidRequestError, e.message
23
+ end
35
24
  end
36
25
 
37
26
  def encode_state_with_original_params
@@ -35,6 +35,7 @@ module StandardId
35
35
  end
36
36
 
37
37
  def generate_token_response
38
+ emit_token_issuing
38
39
  expires_in = token_expiry
39
40
  payload = build_jwt_payload(expires_in)
40
41
  access_token = StandardId::JwtService.encode(payload, expires_in: expires_in)
@@ -47,7 +48,7 @@ module StandardId
47
48
 
48
49
  response[:scope] = token_scope if token_scope.present?
49
50
  response[:refresh_token] = generate_refresh_token if supports_refresh_token?
50
-
51
+ emit_token_issued(expires_in)
51
52
  response.compact
52
53
  end
53
54
 
@@ -159,6 +160,26 @@ module StandardId
159
160
  filtered_context = StandardId::Utils::CallableParameterFilter.filter(resolver, claim_resolvers_context)
160
161
  resolver.call(**filtered_context)
161
162
  end
163
+
164
+ def emit_token_issuing
165
+ StandardId::Events.publish(
166
+ StandardId::Events::OAUTH_TOKEN_ISSUING,
167
+ grant_type: grant_type,
168
+ client_id: client_id,
169
+ account: token_account,
170
+ scope: token_scope
171
+ )
172
+ end
173
+
174
+ def emit_token_issued(expires_in)
175
+ StandardId::Events.publish(
176
+ StandardId::Events::OAUTH_TOKEN_ISSUED,
177
+ grant_type: grant_type,
178
+ client_id: client_id,
179
+ account: token_account,
180
+ expires_in: expires_in
181
+ )
182
+ end
162
183
  end
163
184
  end
164
185
  end
@@ -16,8 +16,11 @@ module StandardId
16
16
  def start!(attrs)
17
17
  username = attrs[:username]
18
18
  validate_username!(username)
19
+ emit_code_requested(username)
19
20
  challenge = create_challenge!(username)
21
+ emit_code_generated(challenge, username)
20
22
  sender_callback&.call(username, challenge.code)
23
+ emit_code_sent(username)
21
24
  challenge
22
25
  end
23
26
 
@@ -66,6 +69,35 @@ module StandardId
66
69
  # Implement in subclasses
67
70
  nil
68
71
  end
72
+
73
+ private
74
+
75
+ def emit_code_requested(username)
76
+ StandardId::Events.publish(
77
+ StandardId::Events::PASSWORDLESS_CODE_REQUESTED,
78
+ identifier: username,
79
+ channel: connection_type
80
+ )
81
+ end
82
+
83
+ def emit_code_generated(challenge, username)
84
+ StandardId::Events.publish(
85
+ StandardId::Events::PASSWORDLESS_CODE_GENERATED,
86
+ code_challenge: challenge,
87
+ identifier: username,
88
+ channel: connection_type,
89
+ expires_at: challenge.expires_at
90
+ )
91
+ end
92
+
93
+ def emit_code_sent(username)
94
+ StandardId::Events.publish(
95
+ StandardId::Events::PASSWORDLESS_CODE_SENT,
96
+ identifier: username,
97
+ channel: connection_type,
98
+ delivery_status: "sent"
99
+ )
100
+ end
69
101
  end
70
102
  end
71
103
  end
@@ -0,0 +1,73 @@
1
+ module StandardId
2
+ class ProviderRegistry
3
+ class ProviderNotFoundError < StandardError; end
4
+ class InvalidProviderError < StandardError; end
5
+
6
+ @providers = {}
7
+
8
+ class << self
9
+ # Register a provider
10
+ # @param name [Symbol, String] Provider identifier
11
+ # @param provider_class [Class] Provider implementation class
12
+ def register(name, provider_class)
13
+ validate_provider!(provider_class)
14
+ @providers[name.to_s] = provider_class
15
+ register_config_schema(provider_class)
16
+ provider_class.setup if provider_class.respond_to?(:setup)
17
+ provider_class
18
+ end
19
+
20
+ # Get provider by name
21
+ # @param name [Symbol, String] Provider identifier
22
+ # @return [Class] Provider class
23
+ # @raise [ProviderNotFoundError] if provider not found
24
+ def get(name)
25
+ @providers[name.to_s] || raise(
26
+ ProviderNotFoundError,
27
+ "Unknown provider: #{name}. Available providers: #{@providers.keys.join(', ')}"
28
+ )
29
+ end
30
+
31
+
32
+ # Get all registered providers
33
+ # @return [Hash] Provider name => class mapping
34
+ def all
35
+ @providers.dup
36
+ end
37
+
38
+ # Check if provider is registered
39
+ # @param name [Symbol, String] Provider identifier
40
+ # @return [Boolean]
41
+ def registered?(name)
42
+ @providers.key?(name.to_s)
43
+ end
44
+
45
+ private
46
+
47
+ # Register provider's config schema fields with StandardConfig
48
+ # @param provider_class [Class] Provider implementation class
49
+ def register_config_schema(provider_class)
50
+ schema = provider_class.config_schema
51
+ return if schema.nil? || schema.empty?
52
+
53
+ StandardConfig.schema.scope(:social) do
54
+ schema.each do |field_name, options|
55
+ field field_name, **options
56
+ end
57
+ end
58
+ end
59
+
60
+ def validate_provider!(provider_class)
61
+ unless provider_class.is_a?(Class)
62
+ raise InvalidProviderError,
63
+ "Provider must be a class, got #{provider_class.class.name}"
64
+ end
65
+
66
+ unless provider_class < StandardId::Providers::Base
67
+ raise InvalidProviderError,
68
+ "Provider #{provider_class.name} must inherit from StandardId::Providers::Base"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -2,13 +2,11 @@ require "uri"
2
2
  require "net/http"
3
3
  require "json"
4
4
  require "jwt"
5
- require_relative "response_builder"
5
+ require_relative "base"
6
6
 
7
7
  module StandardId
8
- module SocialProviders
9
- class Apple
10
- include ResponseBuilder
11
-
8
+ module Providers
9
+ class Apple < Base
12
10
  ISSUER = "https://appleid.apple.com".freeze
13
11
  AUTH_ENDPOINT = "#{ISSUER}/auth/authorize".freeze
14
12
  TOKEN_ENDPOINT = "#{ISSUER}/auth/token".freeze
@@ -17,7 +15,14 @@ module StandardId
17
15
  DEFAULT_RESPONSE_MODE = "form_post".freeze
18
16
 
19
17
  class << self
20
- def authorization_url(state:, redirect_uri:, scope: DEFAULT_SCOPE, response_mode: DEFAULT_RESPONSE_MODE)
18
+ def provider_name
19
+ "apple"
20
+ end
21
+
22
+ def authorization_url(state:, redirect_uri:, **options)
23
+ scope = options[:scope] || DEFAULT_SCOPE
24
+ response_mode = options[:response_mode] || DEFAULT_RESPONSE_MODE
25
+
21
26
  ensure_basic_credentials!
22
27
 
23
28
  query = {
@@ -32,7 +37,9 @@ module StandardId
32
37
  "#{AUTH_ENDPOINT}?#{URI.encode_www_form(query)}"
33
38
  end
34
39
 
35
- def get_user_info(code: nil, id_token: nil, redirect_uri: nil, client_id: StandardId.config.apple_client_id)
40
+ def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, **options)
41
+ client_id = options[:client_id] || StandardId.config.apple_client_id
42
+
36
43
  if id_token.present?
37
44
  build_response(
38
45
  verify_id_token(id_token: id_token, client_id: client_id),
@@ -45,6 +52,35 @@ module StandardId
45
52
  end
46
53
  end
47
54
 
55
+ def config_schema
56
+ {
57
+ apple_client_id: { type: :string, default: nil },
58
+ apple_mobile_client_id: { type: :string, default: nil },
59
+ apple_private_key: { type: :string, default: nil },
60
+ apple_key_id: { type: :string, default: nil },
61
+ apple_team_id: { type: :string, default: nil }
62
+ }
63
+ end
64
+
65
+ def default_scope
66
+ DEFAULT_SCOPE
67
+ end
68
+
69
+ def skip_csrf?
70
+ true
71
+ end
72
+
73
+ def supports_mobile_callback?
74
+ true
75
+ end
76
+
77
+ def resolve_params(params, context: {})
78
+ flow = context[:flow] || :web
79
+ client_id = flow == :mobile ? StandardId.config.apple_mobile_client_id : StandardId.config.apple_client_id
80
+
81
+ params.merge(client_id: client_id)
82
+ end
83
+
48
84
  def exchange_code_for_user_info(code:, redirect_uri:, client_id: StandardId.config.apple_client_id)
49
85
  ensure_full_credentials!(client_id: client_id)
50
86
  raise StandardId::InvalidRequestError, "Missing authorization code" if code.blank?
@@ -182,3 +218,6 @@ module StandardId
182
218
  end
183
219
  end
184
220
  end
221
+
222
+ # Auto-register with the provider registry
223
+ StandardId::ProviderRegistry.register(:apple, StandardId::Providers::Apple)