standard_id 0.1.2 → 0.1.4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -1
  3. data/app/controllers/concerns/standard_id/api_authentication.rb +4 -0
  4. data/app/controllers/concerns/standard_id/social_authentication.rb +105 -0
  5. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +68 -0
  6. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +67 -72
  7. data/app/controllers/standard_id/web/login_controller.rb +20 -17
  8. data/app/controllers/standard_id/web/signup_controller.rb +1 -1
  9. data/app/views/standard_id/web/auth/callback/providers/apple_mobile.html.erb +50 -0
  10. data/app/views/standard_id/web/login/show.html.erb +3 -3
  11. data/app/views/standard_id/web/signup/show.html.erb +1 -1
  12. data/config/routes/api.rb +1 -1
  13. data/config/routes/web.rb +1 -0
  14. data/lib/generators/standard_id/install/templates/standard_id.rb +26 -0
  15. data/lib/standard_config/config.rb +4 -1
  16. data/lib/standard_id/api/authentication_guard.rb +36 -0
  17. data/lib/standard_id/config/schema.rb +6 -0
  18. data/lib/standard_id/http_client.rb +22 -0
  19. data/lib/standard_id/jwt_service.rb +19 -5
  20. data/lib/standard_id/oauth/authorization_code_flow.rb +8 -0
  21. data/lib/standard_id/oauth/client_credentials_flow.rb +8 -0
  22. data/lib/standard_id/oauth/password_flow.rb +4 -0
  23. data/lib/standard_id/oauth/passwordless_otp_flow.rb +4 -0
  24. data/lib/standard_id/oauth/social_flow.rb +61 -0
  25. data/lib/standard_id/oauth/subflows/social_login_grant.rb +9 -19
  26. data/lib/standard_id/oauth/token_grant_flow.rb +54 -1
  27. data/lib/standard_id/social_providers/apple.rb +184 -0
  28. data/lib/standard_id/social_providers/google.rb +168 -0
  29. data/lib/standard_id/social_providers/response_builder.rb +18 -0
  30. data/lib/standard_id/utils/callable_parameter_filter.rb +36 -0
  31. data/lib/standard_id/version.rb +1 -1
  32. data/lib/standard_id.rb +5 -0
  33. metadata +10 -2
  34. data/app/controllers/standard_id/api/providers_controller.rb +0 -174
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8da5350b060ff2f0494489d5cc6718cc08b2dbca835a9c28927e228b712e39d
4
- data.tar.gz: 96ac14f364fd07b8d6750d31bd015b7bc2717d692206a203b3fe8161a6a1008f
3
+ metadata.gz: 69b5bf55ec553e3b3708be74454f71c489125cfbf940f08b72e0225f3999a785
4
+ data.tar.gz: '0908e70df1d46b755d5b00db7b2d1607e6982b10d04f07e47315063934dbe3a2'
5
5
  SHA512:
6
- metadata.gz: 195c91598a768df91279b0c6bda4d8b312f73a94d4c52c68b2864fd75453f2c474059c4f7d740e0d8c0b4086471ea193c9ded4bb7b492f3c098c950f941f6140
7
- data.tar.gz: 80d64202a7b69456e241952b79f0a65ea9a88ed8b242ff31015e11854d141bd32ccbeaccc2f60ccddf78d188dbbec74e48f839c9181a2b4cdce01917001e4125
6
+ metadata.gz: dc37d401a1ec647be6750b5ec05f23b633e752704c00e69e8e721230070c574ebf5c52b0f3d515808c4aa59c12172b815f2c6cb20ff0b7c38775e5d7e721ae6f
7
+ data.tar.gz: 1987f39eeb1b7fe3924de437ad6de02d05650a84964c3cbb20ac9733dec5a75c5d7a4a288ae543443cd34c16d8eefb00569d85a38679d4693531ff9ae9815418
data/README.md CHANGED
@@ -133,6 +133,27 @@ end
133
133
 
134
134
  `default_token_lifetime` is applied to every OAuth grant unless you override it in `oauth.token_lifetimes`. Keys map to OAuth grant types (for example `:password`, `:client_credentials`, `:refresh_token`) and should return durations in seconds. Non-token endpoint flows such as the implicit flow can be customized with their symbol key (e.g. `:implicit`). Refresh tokens can be tuned separately through `oauth.refresh_token_lifetime`.
135
135
 
136
+ ### Custom Token Claims
137
+
138
+ You can add additional JWT claims for any token issued through the OAuth token endpoint by mapping scopes to claim names and providing callbacks to resolve each claim. Scopes listed in `oauth.scope_claims` are evaluated against the requested token scopes; when a scope matches, every claim listed for that scope is resolved via the callable defined in `oauth.claim_resolvers`.
139
+
140
+ ```ruby
141
+ StandardId.configure do |config|
142
+ config.oauth.scope_claims = {
143
+ profile: %i[email display_name]
144
+ }
145
+
146
+ config.oauth.claim_resolvers = {
147
+ email: ->(account:) { account.email },
148
+ display_name: ->(account:, client:) {
149
+ "#{account.name} for #{client.client_id}"
150
+ }
151
+ }
152
+ end
153
+ ```
154
+
155
+ Resolvers receive keyword arguments with the context containing `client`, `account`, and `request`, so you can reference only what you need. This lets you, for example, pull organization info off the client application or decorate claims with account attributes.
156
+
136
157
  ### Social Login Setup
137
158
 
138
159
  ```ruby
@@ -142,13 +163,35 @@ StandardId.configure do |config|
142
163
  config.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
143
164
 
144
165
  # Apple Sign In
166
+ config.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"]
145
167
  config.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
146
168
  config.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
147
169
  config.social.apple_key_id = ENV["APPLE_KEY_ID"]
148
170
  config.social.apple_team_id = ENV["APPLE_TEAM_ID"]
171
+ config.social.allowed_redirect_url_prefixes = ["sidekicklabs://"]
172
+
173
+ # Optional: adjust which attributes are persisted during social signup
174
+ config.social.social_account_attributes = ->(social_info:, provider:) {
175
+ {
176
+ email: social_info[:email],
177
+ name: social_info[:name] || social_info[:given_name]
178
+ }
179
+ }
180
+
181
+ # Optional: run a callback whenever a social login completes
182
+ config.social.social_callback = ->(social_info:, provider:, tokens:, account:) {
183
+ AuditLog.social_login(
184
+ provider: provider,
185
+ email: social_info[:email],
186
+ tokens: tokens,
187
+ account_id: account.id,
188
+ )
189
+ }
149
190
  end
150
191
  ```
151
192
 
193
+ `social_info` is an indifferent-access hash containing at least `email`, `name`, and `provider_id`.
194
+
152
195
  ### Passwordless Authentication
153
196
 
154
197
  ```ruby
@@ -201,7 +244,7 @@ redirect_to "/api/authorize?" + {
201
244
  response_type: "code",
202
245
  client_id: "your_client_id",
203
246
  redirect_uri: "https://your-app.com/callback",
204
- connection: "google-oauth2"
247
+ connection: "google"
205
248
  }.to_query
206
249
 
207
250
  # Apple login
@@ -14,6 +14,10 @@ module StandardId
14
14
  authentication_guard.require_session!(session_manager)
15
15
  end
16
16
 
17
+ def require_scopes!(*required_scopes)
18
+ authentication_guard.require_scopes!(session_manager, *required_scopes)
19
+ end
20
+
17
21
  def session_manager
18
22
  @session_manager ||= StandardId::Api::SessionManager.new(token_manager, request:)
19
23
  end
@@ -0,0 +1,105 @@
1
+ module StandardId
2
+ module SocialAuthentication
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
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
26
+ end
27
+
28
+ def apple_client_id_for_flow(flow)
29
+ flow == :web ? StandardId.config.apple_client_id : StandardId.config.apple_mobile_client_id
30
+ end
31
+
32
+ def find_or_create_account_from_social(raw_social_info, provider)
33
+ social_info = raw_social_info.to_h.with_indifferent_access
34
+ email = social_info[:email]
35
+ raise StandardId::InvalidRequestError, "No email provided by #{provider}" if email.blank?
36
+
37
+ identifier = StandardId::EmailIdentifier.find_by(value: email)
38
+
39
+ if identifier.present?
40
+ identifier.account
41
+ else
42
+ account = build_account_from_social(social_info, provider)
43
+ identifier = StandardId::EmailIdentifier.create!(
44
+ account: account,
45
+ value: email
46
+ )
47
+ identifier.verify! if identifier.respond_to?(:verify!)
48
+ account
49
+ end
50
+ end
51
+
52
+ def build_account_from_social(social_info, provider)
53
+ attrs = resolve_account_attributes(social_info, provider)
54
+ StandardId.account_class.create!(attrs)
55
+ end
56
+
57
+ def resolve_account_attributes(social_info, provider)
58
+ resolver = StandardId.config.social_account_attributes
59
+ attrs = if resolver.respond_to?(:call)
60
+ resolver.call(social_info: social_info, provider: provider)
61
+ else
62
+ {
63
+ email: social_info[:email],
64
+ name: social_info[:name].presence || social_info[:given_name].presence || social_info[:email]
65
+ }
66
+ end
67
+
68
+ unless attrs.is_a?(Hash)
69
+ raise StandardId::InvalidRequestError, "Social account attribute resolver must return a hash"
70
+ end
71
+
72
+ attrs.symbolize_keys
73
+ end
74
+
75
+ def allow_other_host_redirect?(redirect_uri)
76
+ return false if redirect_uri.blank?
77
+
78
+ allowed = Array(StandardId.config.allowed_redirect_url_prefixes)
79
+ return false if allowed.blank?
80
+
81
+ allowed.any? do |entry|
82
+ case entry
83
+ when Regexp
84
+ entry.match?(redirect_uri)
85
+ else
86
+ redirect_uri.start_with?(entry.to_s)
87
+ end
88
+ end
89
+ end
90
+
91
+ def run_social_callback(provider:, social_info:, provider_tokens:, account:)
92
+ callback = StandardId.config.social_callback
93
+
94
+ payload = {
95
+ provider: provider,
96
+ social_info: social_info,
97
+ tokens: provider_tokens.presence,
98
+ account: account
99
+ }
100
+
101
+ filtered_payload = StandardId::Utils::CallableParameterFilter.filter(callback, payload)
102
+ callback.call(**filtered_payload.symbolize_keys)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,68 @@
1
+ module StandardId
2
+ module Api::Oauth
3
+ module Callback
4
+ class ProvidersController < BaseController
5
+ include StandardId::SocialAuthentication
6
+
7
+ skip_before_action :validate_content_type!
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)
22
+ original_params = decode_state_params
23
+ flow = resolve_flow_for(connection)
24
+ provider_response = get_user_info_from_provider(connection, flow: flow)
25
+ social_info = provider_response[:user_info]
26
+ provider_tokens = provider_response[:tokens]
27
+ account = find_or_create_account_from_social(social_info, connection)
28
+
29
+ flow = StandardId::Oauth::SocialFlow.new(
30
+ params,
31
+ request,
32
+ account: account,
33
+ connection: connection,
34
+ original_params: original_params
35
+ )
36
+
37
+ token_response = flow.execute
38
+ run_social_callback(
39
+ provider: connection,
40
+ social_info: social_info,
41
+ provider_tokens: provider_tokens,
42
+ account: account,
43
+ )
44
+ render json: token_response, status: :ok
45
+ end
46
+
47
+ def decode_state_params
48
+ encoded_state = params[:state]
49
+
50
+ return {} if encoded_state.blank?
51
+
52
+ begin
53
+ JSON.parse(Base64.urlsafe_decode64(encoded_state))
54
+ rescue JSON::ParserError, ArgumentError
55
+ raise StandardId::InvalidRequestError, "Invalid state parameter"
56
+ end
57
+ end
58
+
59
+ def resolve_flow_for(connection)
60
+ return :mobile unless connection == "apple"
61
+
62
+ flow_param = params[:flow].to_s.downcase
63
+ flow_param == "web" ? :web : :mobile
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -4,107 +4,88 @@ module StandardId
4
4
  module Callback
5
5
  class ProvidersController < StandardId::Web::BaseController
6
6
  include StandardId::WebAuthentication
7
+ include StandardId::SocialAuthentication
7
8
 
8
9
  # Social callbacks must be accessible without an existing browser session
9
10
  # because they create/sign-in the session upon successful callback.
10
- skip_before_action :require_browser_session!, only: [:google, :apple]
11
+ skip_before_action :require_browser_session!, only: [:google, :apple, :apple_mobile]
12
+ skip_before_action :verify_authenticity_token, only: [:apple, :apple_mobile]
11
13
 
12
14
  def google
13
- handle_social_callback("google-oauth2")
15
+ handle_social_callback("google")
14
16
  end
15
17
 
16
18
  def apple
17
19
  handle_social_callback("apple")
18
20
  end
19
21
 
20
- private
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
21
29
 
22
- def handle_social_callback(provider)
23
- # This handles the callback from social providers in web context
24
- # After successful social auth, we need to:
25
- # 1. Extract user info from the callback
26
- # 2. Create/find account
27
- # 3. Sign in the user with web session
28
- # 4. Redirect to original destination
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
29
36
 
37
+ private
38
+
39
+ def handle_social_callback(connection)
30
40
  if params[:error].present?
31
41
  handle_callback_error
32
42
  return
33
43
  end
34
44
 
45
+ state_data = nil
46
+
35
47
  begin
36
- user_info = extract_user_info(provider)
37
- account = find_or_create_account_from_social(user_info, provider)
48
+ 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)
51
+ social_info = provider_response[:user_info]
52
+ provider_tokens = provider_response[:tokens]
53
+ account = find_or_create_account_from_social(social_info, connection)
38
54
  session_manager.sign_in_account(account)
39
55
 
40
- redirect_to decode_redirect_uri, notice: "Successfully signed in with #{provider.humanize}"
41
- rescue StandardId::OAuthError => e
42
- redirect_to login_path, alert: "Authentication failed: #{e.message}"
43
- end
44
- end
56
+ run_social_callback(
57
+ provider: connection,
58
+ social_info: social_info,
59
+ provider_tokens: provider_tokens,
60
+ account: account,
61
+ )
45
62
 
46
- def extract_user_info(provider)
47
- case provider
48
- when "google-oauth2"
49
- extract_google_user_info
50
- when "apple"
51
- extract_apple_user_info
52
- else
53
- raise StandardId::InvalidRequestError, "Unsupported connection/provider: #{provider}"
63
+ destination = state_data["redirect_uri"]
64
+ redirect_options = { notice: "Successfully signed in with #{connection.humanize}" }
65
+ redirect_options[:allow_other_host] = true if allow_other_host_redirect?(destination)
66
+ redirect_to destination, redirect_options
67
+ rescue StandardId::OAuthError => e
68
+ redirect_to StandardId::WebEngine.routes.url_helpers.login_path(redirect_uri: state_data&.dig("redirect_uri")), alert: "Authentication failed: #{e.message}"
54
69
  end
55
70
  end
56
71
 
57
- def extract_google_user_info
58
- # Exchange code for Google user info
59
- # This would integrate with Google OAuth API
60
- {
61
- email: params[:email] || "user@example.com", # Placeholder
62
- name: params[:name] || "Google User",
63
- provider: "google-oauth2",
64
- provider_id: params[:sub] || "google_123"
65
- }
72
+ def google_callback_url
73
+ auth_callback_google_url
66
74
  end
67
75
 
68
- def extract_apple_user_info
69
- # Extract user info from Apple Sign In callback
70
- # This would decode the Apple ID token
71
- {
72
- email: params[:email] || "user@privaterelay.appleid.com", # Placeholder
73
- name: params[:name] || "Apple User",
74
- provider: "apple",
75
- provider_id: params[:sub] || "apple_123"
76
- }
76
+ def apple_callback_url
77
+ auth_callback_apple_url
77
78
  end
78
79
 
79
- def find_or_create_account_from_social(user_info, provider)
80
- # Find existing account by email or create new one
81
- identifier = StandardId::EmailIdentifier.find_by(value: user_info[:email])
80
+ def decode_state_params
81
+ encoded_state = params[:state]
82
+ raise StandardId::InvalidRequestError, "Missing state parameter" if encoded_state.blank?
82
83
 
83
- if identifier
84
- identifier.account
85
- else
86
- # Create new account with social login
87
- account = ::Account.create!(
88
- email: user_info[:email],
89
- name: user_info[:name].presence || "User"
90
- )
91
- StandardId::EmailIdentifier.create!(
92
- account: account,
93
- value: user_info[:email]
94
- )
95
- account
96
- end
97
- end
98
-
99
- def decode_redirect_uri
100
- return "/" unless params[:state].present?
101
-
102
- begin
103
- state_data = JSON.parse(Base64.urlsafe_decode64(params[:state]))
104
- state_data["redirect_uri"] || "/"
105
- rescue JSON::ParserError, ArgumentError
106
- "/"
107
- end
84
+ state = JSON.parse(Base64.urlsafe_decode64(encoded_state))
85
+ state["redirect_uri"] ||= after_authentication_url
86
+ state
87
+ rescue JSON::ParserError, ArgumentError
88
+ raise StandardId::InvalidRequestError, "Invalid state parameter"
108
89
  end
109
90
 
110
91
  def handle_callback_error
@@ -117,7 +98,21 @@ module StandardId
117
98
  "Authentication failed"
118
99
  end
119
100
 
120
- redirect_to login_path, alert: error_message
101
+ redirect_to StandardId::WebEngine.routes.url_helpers.login_path, alert: error_message
102
+ end
103
+
104
+ def mobile_relay_params
105
+ params.permit(:code, :state, :user, :userIdentifier, :id_token, :identity_token, :nonce).to_h.compact
106
+ end
107
+
108
+ def build_mobile_redirect(destination, extra_params)
109
+ uri = URI.parse(destination)
110
+ existing = Rack::Utils.parse_nested_query(uri.query)
111
+ merged = existing.merge(extra_params)
112
+ uri.query = merged.to_query.presence
113
+ uri.to_s
114
+ rescue URI::InvalidURIError
115
+ destination
121
116
  end
122
117
  end
123
118
  end
@@ -33,32 +33,35 @@ module StandardId
33
33
  end
34
34
 
35
35
  def social_login_url
36
- uri = URI.parse("/api/authorize")
37
- query = {
38
- response_type: "code",
39
- client_id: StandardId.config.default_client_id,
40
- redirect_uri: callback_url,
41
- connection: params[:connection],
42
- state: encode_state
43
- }.to_query
44
- uri.query = query
45
- uri.to_s
46
- end
47
-
48
- def callback_url
49
36
  case params[:connection]
50
- when "google-oauth2"
51
- auth_callback_google_path
37
+ when "google"
38
+ google_authorization_url
52
39
  when "apple"
53
- auth_callback_apple_path
40
+ apple_authorization_url
41
+ else
42
+ raise StandardId::InvalidRequestError, "Unsupported social connection: #{connection}"
54
43
  end
55
44
  end
56
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
52
+
53
+ def apple_authorization_url
54
+ StandardId::SocialProviders::Apple.authorization_url(
55
+ state: encode_state,
56
+ redirect_uri: auth_callback_apple_url
57
+ )
58
+ end
59
+
57
60
  def encode_state
58
61
  Base64.urlsafe_encode64({
59
62
  redirect_uri: params[:redirect_uri] || after_authentication_url,
60
63
  timestamp: Time.current.to_i
61
- }.to_json)
64
+ }.compact.to_json)
62
65
  end
63
66
 
64
67
  def login_params
@@ -56,7 +56,7 @@ module StandardId
56
56
 
57
57
  def callback_url
58
58
  case params[:connection]
59
- when "google-oauth2"
59
+ when "google"
60
60
  auth_callback_google_url
61
61
  when "apple"
62
62
  auth_callback_apple_url
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Continuing Sign in with Apple…</title>
6
+ <style>
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
9
+ margin: 0;
10
+ padding: 2rem;
11
+ text-align: center;
12
+ color: #111;
13
+ background: #f8f8f8;
14
+ }
15
+
16
+ .card {
17
+ margin: 0 auto;
18
+ max-width: 420px;
19
+ padding: 2rem;
20
+ background: #fff;
21
+ border-radius: 12px;
22
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
23
+ }
24
+
25
+ h1 {
26
+ font-size: 1.3rem;
27
+ margin-bottom: 0.5rem;
28
+ }
29
+
30
+ p {
31
+ margin: 0;
32
+ color: #555;
33
+ }
34
+ </style>
35
+ <script>
36
+ document.addEventListener("DOMContentLoaded", function () {
37
+ var target = "<%= j @mobile_redirect_url %>";
38
+ if (target) {
39
+ window.location.replace(target);
40
+ }
41
+ });
42
+ </script>
43
+ </head>
44
+ <body>
45
+ <div class="card">
46
+ <h1>Returning to your app…</h1>
47
+ <p>You can close this window if it doesn't redirect automatically.</p>
48
+ </div>
49
+ </body>
50
+ </html>
@@ -68,8 +68,8 @@
68
68
 
69
69
  <div class="mt-6 grid grid-cols-2 gap-4">
70
70
  <% if StandardId.config.google_client_id.present? %>
71
- <%= form_with url: login_path, method: :post, local: true do |form| %>
72
- <%= form.hidden_field :connection, value: "google-oauth2" %>
71
+ <%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
72
+ <%= form.hidden_field :connection, value: "google" %>
73
73
  <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
74
74
  <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
75
75
  <svg viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5">
@@ -84,7 +84,7 @@
84
84
  <% end %>
85
85
 
86
86
  <% if StandardId.config.apple_client_id.present? %>
87
- <%= form_with url: login_path, method: :post, local: true do |form| %>
87
+ <%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
88
88
  <%= form.hidden_field :connection, value: "apple" %>
89
89
  <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
90
90
  <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
@@ -57,7 +57,7 @@
57
57
  <div class="mt-6 grid grid-cols-2 gap-4">
58
58
  <% if StandardId.config.google_client_id.present? %>
59
59
  <%= form_with url: signup_path, method: :post, local: true do |form| %>
60
- <%= form.hidden_field :connection, value: "google-oauth2" %>
60
+ <%= form.hidden_field :connection, value: "google" %>
61
61
  <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
62
62
  <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
63
63
  <svg viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5">
data/config/routes/api.rb CHANGED
@@ -16,7 +16,7 @@ StandardId::ApiEngine.routes.draw do
16
16
  resource :token, only: [:create]
17
17
 
18
18
  namespace :callback do
19
- get :google, to: "providers#google"
19
+ post :google, to: "providers#google"
20
20
  post :apple, to: "providers#apple"
21
21
  end
22
22
  end
data/config/routes/web.rb CHANGED
@@ -10,6 +10,7 @@ StandardId::WebEngine.routes.draw do
10
10
  namespace :callback do
11
11
  get :google, to: "providers#google"
12
12
  post :apple, to: "providers#apple"
13
+ post :apple_mobile, to: "providers#apple_mobile"
13
14
  end
14
15
  end
15
16
 
@@ -20,15 +20,41 @@ StandardId.configure do |c|
20
20
  # c.oauth.token_lifetimes = {
21
21
  # password: 8.hours,
22
22
  # client_credentials: 24.hours
23
+ # social: 24.hours
24
+ # }
25
+ # c.oauth.scope_claims = {
26
+ # profile: %i[email display_name]
27
+ # }
28
+ # c.oauth.claim_resolvers = {
29
+ # email: ->(account:) { account.email },
30
+ # display_name: ->(account:, client:) {
31
+ # "#{account.name} for #{client.client_id}"
32
+ # }
23
33
  # }
24
34
 
25
35
  # Social login credentials (if enabled in your app)
26
36
  # c.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
27
37
  # c.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
38
+ # c.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"]
28
39
  # c.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
29
40
  # c.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
30
41
  # c.social.apple_key_id = ENV["APPLE_KEY_ID"]
31
42
  # c.social.apple_team_id = ENV["APPLE_TEAM_ID"]
43
+ # c.social.allowed_redirect_url_prefixes = ["sidekicklabs://"]
44
+ # c.social.social_account_attributes = ->(social_info:, provider:) {
45
+ # {
46
+ # email: social_info[:email],
47
+ # name: social_info[:name] || social_info[:given_name]
48
+ # }
49
+ # }
50
+ # c.social.social_callback = ->(social_info:, provider:, tokens:, account:) {
51
+ # Analytics.track_social_login(
52
+ # provider: provider,
53
+ # email: social_info[:email],
54
+ # tokens: tokens,
55
+ # account_id: account.id
56
+ # )
57
+ # }
32
58
 
33
59
  # OIDC Logout allow list
34
60
  # c.allowed_post_logout_redirect_uris = [