standard_id 0.1.3 → 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.
- checksums.yaml +4 -4
- data/README.md +23 -1
- data/app/controllers/concerns/standard_id/social_authentication.rb +105 -0
- data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +68 -0
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +67 -72
- data/app/controllers/standard_id/web/login_controller.rb +20 -17
- data/app/controllers/standard_id/web/signup_controller.rb +1 -1
- data/app/views/standard_id/web/auth/callback/providers/apple_mobile.html.erb +50 -0
- data/app/views/standard_id/web/login/show.html.erb +3 -3
- data/app/views/standard_id/web/signup/show.html.erb +1 -1
- data/config/routes/api.rb +1 -1
- data/config/routes/web.rb +1 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +17 -0
- data/lib/standard_config/config.rb +4 -1
- data/lib/standard_id/config/schema.rb +4 -0
- data/lib/standard_id/http_client.rb +22 -0
- data/lib/standard_id/oauth/social_flow.rb +61 -0
- data/lib/standard_id/oauth/subflows/social_login_grant.rb +9 -19
- data/lib/standard_id/oauth/token_grant_flow.rb +2 -15
- data/lib/standard_id/social_providers/apple.rb +184 -0
- data/lib/standard_id/social_providers/google.rb +168 -0
- data/lib/standard_id/social_providers/response_builder.rb +18 -0
- data/lib/standard_id/utils/callable_parameter_filter.rb +36 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +5 -0
- metadata +10 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69b5bf55ec553e3b3708be74454f71c489125cfbf940f08b72e0225f3999a785
|
|
4
|
+
data.tar.gz: '0908e70df1d46b755d5b00db7b2d1607e6982b10d04f07e47315063934dbe3a2'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc37d401a1ec647be6750b5ec05f23b633e752704c00e69e8e721230070c574ebf5c52b0f3d515808c4aa59c12172b815f2c6cb20ff0b7c38775e5d7e721ae6f
|
|
7
|
+
data.tar.gz: 1987f39eeb1b7fe3924de437ad6de02d05650a84964c3cbb20ac9733dec5a75c5d7a4a288ae543443cd34c16d8eefb00569d85a38679d4693531ff9ae9815418
|
data/README.md
CHANGED
|
@@ -163,13 +163,35 @@ StandardId.configure do |config|
|
|
|
163
163
|
config.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
164
164
|
|
|
165
165
|
# Apple Sign In
|
|
166
|
+
config.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"]
|
|
166
167
|
config.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
|
|
167
168
|
config.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
|
|
168
169
|
config.social.apple_key_id = ENV["APPLE_KEY_ID"]
|
|
169
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
|
+
}
|
|
170
190
|
end
|
|
171
191
|
```
|
|
172
192
|
|
|
193
|
+
`social_info` is an indifferent-access hash containing at least `email`, `name`, and `provider_id`.
|
|
194
|
+
|
|
173
195
|
### Passwordless Authentication
|
|
174
196
|
|
|
175
197
|
```ruby
|
|
@@ -222,7 +244,7 @@ redirect_to "/api/authorize?" + {
|
|
|
222
244
|
response_type: "code",
|
|
223
245
|
client_id: "your_client_id",
|
|
224
246
|
redirect_uri: "https://your-app.com/callback",
|
|
225
|
-
connection: "google
|
|
247
|
+
connection: "google"
|
|
226
248
|
}.to_query
|
|
227
249
|
|
|
228
250
|
# Apple login
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
run_social_callback(
|
|
57
|
+
provider: connection,
|
|
58
|
+
social_info: social_info,
|
|
59
|
+
provider_tokens: provider_tokens,
|
|
60
|
+
account: account,
|
|
61
|
+
)
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
58
|
-
|
|
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
|
|
69
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
def decode_state_params
|
|
81
|
+
encoded_state = params[:state]
|
|
82
|
+
raise StandardId::InvalidRequestError, "Missing state parameter" if encoded_state.blank?
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
51
|
-
|
|
37
|
+
when "google"
|
|
38
|
+
google_authorization_url
|
|
52
39
|
when "apple"
|
|
53
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
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
data/config/routes/web.rb
CHANGED
|
@@ -20,6 +20,7 @@ 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
|
|
23
24
|
# }
|
|
24
25
|
# c.oauth.scope_claims = {
|
|
25
26
|
# profile: %i[email display_name]
|
|
@@ -34,10 +35,26 @@ StandardId.configure do |c|
|
|
|
34
35
|
# Social login credentials (if enabled in your app)
|
|
35
36
|
# c.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
36
37
|
# c.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
38
|
+
# c.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"]
|
|
37
39
|
# c.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
|
|
38
40
|
# c.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
|
|
39
41
|
# c.social.apple_key_id = ENV["APPLE_KEY_ID"]
|
|
40
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
|
+
# }
|
|
41
58
|
|
|
42
59
|
# OIDC Logout allow list
|
|
43
60
|
# c.allowed_post_logout_redirect_uris = [
|
|
@@ -23,9 +23,10 @@ module StandardConfig
|
|
|
23
23
|
# If set, Authorization endpoints can redirect to this path with a redirect_uri param
|
|
24
24
|
attr_accessor :login_url
|
|
25
25
|
|
|
26
|
-
# Social login provider credentials
|
|
26
|
+
# Social login provider credentials and hooks
|
|
27
27
|
attr_accessor :google_client_id, :google_client_secret
|
|
28
28
|
attr_accessor :apple_client_id, :apple_client_secret, :apple_private_key, :apple_key_id, :apple_team_id
|
|
29
|
+
attr_accessor :social_account_attributes, :social_callback
|
|
29
30
|
|
|
30
31
|
# Passwordless authentication callbacks
|
|
31
32
|
# These should be callable objects (procs/lambdas) that accept (recipient, code) parameters
|
|
@@ -54,6 +55,8 @@ module StandardConfig
|
|
|
54
55
|
@apple_private_key = nil
|
|
55
56
|
@apple_key_id = nil
|
|
56
57
|
@apple_team_id = nil
|
|
58
|
+
@social_account_attributes = nil
|
|
59
|
+
@social_callback = nil
|
|
57
60
|
@passwordless_email_sender = nil
|
|
58
61
|
@passwordless_sms_sender = nil
|
|
59
62
|
@allowed_post_logout_redirect_uris = []
|
|
@@ -47,5 +47,9 @@ StandardConfig.schema.draw do
|
|
|
47
47
|
field :apple_private_key, type: :string, default: nil
|
|
48
48
|
field :apple_key_id, type: :string, default: nil
|
|
49
49
|
field :apple_team_id, type: :string, default: nil
|
|
50
|
+
field :apple_mobile_client_id, type: :string, default: nil
|
|
51
|
+
field :social_account_attributes, type: :any, default: nil
|
|
52
|
+
field :allowed_redirect_url_prefixes, type: :array, default: []
|
|
53
|
+
field :social_callback, type: :any, default: nil
|
|
50
54
|
end
|
|
51
55
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
module StandardId
|
|
5
|
+
class HttpClient
|
|
6
|
+
class << self
|
|
7
|
+
def post_form(endpoint, params)
|
|
8
|
+
uri = URI(endpoint)
|
|
9
|
+
Net::HTTP.post_form(uri, params)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get_with_bearer(endpoint, access_token)
|
|
13
|
+
uri = URI(endpoint)
|
|
14
|
+
request = Net::HTTP::Get.new(uri)
|
|
15
|
+
request["Authorization"] = "Bearer #{access_token}"
|
|
16
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
17
|
+
http.request(request)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|