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.
- checksums.yaml +4 -4
- data/README.md +44 -1
- data/app/controllers/concerns/standard_id/api_authentication.rb +4 -0
- 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 +26 -0
- data/lib/standard_config/config.rb +4 -1
- data/lib/standard_id/api/authentication_guard.rb +36 -0
- data/lib/standard_id/config/schema.rb +6 -0
- data/lib/standard_id/http_client.rb +22 -0
- data/lib/standard_id/jwt_service.rb +19 -5
- data/lib/standard_id/oauth/authorization_code_flow.rb +8 -0
- data/lib/standard_id/oauth/client_credentials_flow.rb +8 -0
- data/lib/standard_id/oauth/password_flow.rb +4 -0
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +4 -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 +54 -1
- 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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
require_relative "response_builder"
|
|
2
|
+
|
|
3
|
+
module StandardId
|
|
4
|
+
module SocialProviders
|
|
5
|
+
class Google
|
|
6
|
+
include ResponseBuilder
|
|
7
|
+
|
|
8
|
+
AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth".freeze
|
|
9
|
+
TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token".freeze
|
|
10
|
+
USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v2/userinfo".freeze
|
|
11
|
+
TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo".freeze
|
|
12
|
+
DEFAULT_SCOPE = "openid email profile".freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def authorization_url(state:, redirect_uri:, scope: DEFAULT_SCOPE, prompt: nil)
|
|
16
|
+
query = {
|
|
17
|
+
client_id: credentials[:client_id],
|
|
18
|
+
redirect_uri: redirect_uri,
|
|
19
|
+
response_type: "code",
|
|
20
|
+
scope: scope,
|
|
21
|
+
state: state
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
query[:prompt] = prompt if prompt.present?
|
|
25
|
+
|
|
26
|
+
"#{AUTH_ENDPOINT}?#{URI.encode_www_form(query)}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil)
|
|
30
|
+
if id_token.present?
|
|
31
|
+
build_response(
|
|
32
|
+
verify_id_token(id_token: id_token),
|
|
33
|
+
tokens: { id_token: id_token }
|
|
34
|
+
)
|
|
35
|
+
elsif access_token.present?
|
|
36
|
+
build_response(
|
|
37
|
+
fetch_user_info(access_token: access_token),
|
|
38
|
+
tokens: { access_token: access_token }
|
|
39
|
+
)
|
|
40
|
+
elsif code.present?
|
|
41
|
+
exchange_code_for_user_info(code: code, redirect_uri: redirect_uri)
|
|
42
|
+
else
|
|
43
|
+
raise StandardId::InvalidRequestError, "Either code, id_token, or access_token must be provided"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def exchange_code_for_user_info(code:, redirect_uri:)
|
|
48
|
+
raise StandardId::InvalidRequestError, "Missing authorization code" if code.blank?
|
|
49
|
+
|
|
50
|
+
token_response = HttpClient.post_form(TOKEN_ENDPOINT, {
|
|
51
|
+
client_id: credentials[:client_id],
|
|
52
|
+
client_secret: credentials[:client_secret],
|
|
53
|
+
code: code,
|
|
54
|
+
grant_type: "authorization_code",
|
|
55
|
+
redirect_uri: redirect_uri
|
|
56
|
+
}.compact)
|
|
57
|
+
|
|
58
|
+
unless token_response.is_a?(Net::HTTPSuccess)
|
|
59
|
+
raise StandardId::InvalidRequestError, "Failed to exchange Google authorization code"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parsed_token = JSON.parse(token_response.body)
|
|
63
|
+
access_token = parsed_token["access_token"]
|
|
64
|
+
raise StandardId::InvalidRequestError, "Google response missing access token" if access_token.blank?
|
|
65
|
+
|
|
66
|
+
tokens = extract_token_payload(parsed_token)
|
|
67
|
+
user_info = fetch_user_info(access_token: access_token)
|
|
68
|
+
|
|
69
|
+
build_response(user_info, tokens: tokens)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
raise e if e.is_a?(StandardId::OAuthError)
|
|
72
|
+
raise StandardId::OAuthError, e.message, cause: e
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def verify_id_token(id_token:)
|
|
76
|
+
raise StandardId::InvalidRequestError, "Missing id_token" if id_token.blank?
|
|
77
|
+
|
|
78
|
+
response = HttpClient.post_form(TOKEN_INFO_ENDPOINT, id_token: id_token)
|
|
79
|
+
|
|
80
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
81
|
+
raise StandardId::InvalidRequestError, "Invalid or expired id_token"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
token_info = JSON.parse(response.body)
|
|
85
|
+
|
|
86
|
+
unless token_info["aud"] == credentials[:client_id]
|
|
87
|
+
raise StandardId::InvalidRequestError, "ID token audience mismatch. Expected: #{credentials[:client_id]}, got: #{token_info['aud']}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
unless ["accounts.google.com", "https://accounts.google.com"].include?(token_info["iss"])
|
|
91
|
+
raise StandardId::InvalidRequestError, "ID token issuer invalid. Expected Google, got: #{token_info['iss']}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"sub" => token_info["sub"],
|
|
96
|
+
"email" => token_info["email"],
|
|
97
|
+
"email_verified" => token_info["email_verified"],
|
|
98
|
+
"name" => token_info["name"],
|
|
99
|
+
"given_name" => token_info["given_name"],
|
|
100
|
+
"family_name" => token_info["family_name"],
|
|
101
|
+
"picture" => token_info["picture"],
|
|
102
|
+
"locale" => token_info["locale"]
|
|
103
|
+
}.compact
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
raise e if e.is_a?(StandardId::OAuthError)
|
|
106
|
+
raise StandardId::OAuthError, e.message, cause: e
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fetch_user_info(access_token:)
|
|
110
|
+
raise StandardId::InvalidRequestError, "Missing access token" if access_token.blank?
|
|
111
|
+
|
|
112
|
+
verify_token(access_token)
|
|
113
|
+
user_response = HttpClient.get_with_bearer(USERINFO_ENDPOINT, access_token)
|
|
114
|
+
|
|
115
|
+
unless user_response.is_a?(Net::HTTPSuccess)
|
|
116
|
+
raise StandardId::InvalidRequestError, "Failed to fetch Google user info"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
JSON.parse(user_response.body)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
raise e if e.is_a?(StandardId::OAuthError)
|
|
122
|
+
raise StandardId::OAuthError, e.message, cause: e
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def credentials
|
|
128
|
+
@credentials ||= begin
|
|
129
|
+
if StandardId.config.google_client_id.blank? || StandardId.config.google_client_secret.blank?
|
|
130
|
+
raise StandardId::InvalidRequestError, "Google provider is not configured"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
client_id: StandardId.config.google_client_id,
|
|
135
|
+
client_secret: StandardId.config.google_client_secret
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def verify_token(access_token)
|
|
141
|
+
token_info_uri = "https://www.googleapis.com/oauth2/v3/tokeninfo"
|
|
142
|
+
|
|
143
|
+
response = HttpClient.post_form(token_info_uri, access_token: access_token)
|
|
144
|
+
|
|
145
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
146
|
+
raise StandardId::InvalidRequestError, "Invalid or expired access token"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
token_info = JSON.parse(response.body)
|
|
150
|
+
|
|
151
|
+
unless token_info["aud"] == credentials[:client_id]
|
|
152
|
+
raise StandardId::InvalidRequestError, "Access token audience mismatch. Expected: #{credentials[:client_id]}, got: #{token_info['aud']}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
token_info
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def extract_token_payload(parsed_token)
|
|
159
|
+
{
|
|
160
|
+
access_token: parsed_token["access_token"],
|
|
161
|
+
refresh_token: parsed_token["refresh_token"],
|
|
162
|
+
id_token: parsed_token["id_token"]
|
|
163
|
+
}.compact
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
|
|
3
|
+
module StandardId
|
|
4
|
+
module SocialProviders
|
|
5
|
+
module ResponseBuilder
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def build_response(user_info, tokens: {})
|
|
10
|
+
{
|
|
11
|
+
user_info: user_info,
|
|
12
|
+
tokens: tokens.compact
|
|
13
|
+
}.with_indifferent_access
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Utils
|
|
3
|
+
class CallableParameterFilter
|
|
4
|
+
class << self
|
|
5
|
+
def filter(callable, context)
|
|
6
|
+
return {} unless callable.respond_to?(:call) && context.present?
|
|
7
|
+
|
|
8
|
+
payload = context.to_h.symbolize_keys
|
|
9
|
+
accepted_keys = accepted_parameters(callable)
|
|
10
|
+
return payload if accepted_keys.nil?
|
|
11
|
+
|
|
12
|
+
payload.slice(*accepted_keys)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def accepted_parameters(callable)
|
|
18
|
+
parameters = parameter_list(callable)
|
|
19
|
+
return nil if parameters.any? { |type, _| type == :keyrest }
|
|
20
|
+
|
|
21
|
+
parameters.map { |_, name| name&.to_sym }.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parameter_list(callable)
|
|
25
|
+
if callable.respond_to?(:parameters)
|
|
26
|
+
callable.parameters
|
|
27
|
+
elsif callable.respond_to?(:method)
|
|
28
|
+
callable.method(:call).parameters
|
|
29
|
+
else
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "standard_id/web_engine"
|
|
|
4
4
|
require "standard_id/api_engine"
|
|
5
5
|
require "standard_id/config/schema"
|
|
6
6
|
require "standard_id/errors"
|
|
7
|
+
require "standard_id/http_client"
|
|
7
8
|
require "standard_id/jwt_service"
|
|
8
9
|
require "standard_id/web/session_manager"
|
|
9
10
|
require "standard_id/web/token_manager"
|
|
@@ -18,6 +19,7 @@ require "standard_id/oauth/client_credentials_flow"
|
|
|
18
19
|
require "standard_id/oauth/authorization_code_flow"
|
|
19
20
|
require "standard_id/oauth/password_flow"
|
|
20
21
|
require "standard_id/oauth/refresh_token_flow"
|
|
22
|
+
require "standard_id/oauth/social_flow"
|
|
21
23
|
require "standard_id/oauth/authorization_flow"
|
|
22
24
|
require "standard_id/oauth/authorization_code_authorization_flow"
|
|
23
25
|
require "standard_id/oauth/implicit_authorization_flow"
|
|
@@ -28,6 +30,9 @@ require "standard_id/oauth/passwordless_otp_flow"
|
|
|
28
30
|
require "standard_id/passwordless/base_strategy"
|
|
29
31
|
require "standard_id/passwordless/email_strategy"
|
|
30
32
|
require "standard_id/passwordless/sms_strategy"
|
|
33
|
+
require "standard_id/utils/callable_parameter_filter"
|
|
34
|
+
require "standard_id/social_providers/google"
|
|
35
|
+
require "standard_id/social_providers/apple"
|
|
31
36
|
|
|
32
37
|
module StandardId
|
|
33
38
|
class << self
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -65,14 +65,15 @@ files:
|
|
|
65
65
|
- Rakefile
|
|
66
66
|
- app/assets/stylesheets/standard_id/application.css
|
|
67
67
|
- app/controllers/concerns/standard_id/api_authentication.rb
|
|
68
|
+
- app/controllers/concerns/standard_id/social_authentication.rb
|
|
68
69
|
- app/controllers/concerns/standard_id/web_authentication.rb
|
|
69
70
|
- app/controllers/standard_id/api/authorization_controller.rb
|
|
70
71
|
- app/controllers/standard_id/api/base_controller.rb
|
|
71
72
|
- app/controllers/standard_id/api/oauth/base_controller.rb
|
|
73
|
+
- app/controllers/standard_id/api/oauth/callback/providers_controller.rb
|
|
72
74
|
- app/controllers/standard_id/api/oauth/tokens_controller.rb
|
|
73
75
|
- app/controllers/standard_id/api/oidc/logout_controller.rb
|
|
74
76
|
- app/controllers/standard_id/api/passwordless_controller.rb
|
|
75
|
-
- app/controllers/standard_id/api/providers_controller.rb
|
|
76
77
|
- app/controllers/standard_id/api/userinfo_controller.rb
|
|
77
78
|
- app/controllers/standard_id/web/account_controller.rb
|
|
78
79
|
- app/controllers/standard_id/web/auth/callback/providers_controller.rb
|
|
@@ -114,6 +115,7 @@ files:
|
|
|
114
115
|
- app/models/standard_id/username_identifier.rb
|
|
115
116
|
- app/views/standard_id/web/account/edit.html.erb
|
|
116
117
|
- app/views/standard_id/web/account/show.html.erb
|
|
118
|
+
- app/views/standard_id/web/auth/callback/providers/apple_mobile.html.erb
|
|
117
119
|
- app/views/standard_id/web/login/show.html.erb
|
|
118
120
|
- app/views/standard_id/web/reset_password/confirm/show.html.erb
|
|
119
121
|
- app/views/standard_id/web/reset_password/start/show.html.erb
|
|
@@ -146,6 +148,7 @@ files:
|
|
|
146
148
|
- lib/standard_id/config/schema.rb
|
|
147
149
|
- lib/standard_id/engine.rb
|
|
148
150
|
- lib/standard_id/errors.rb
|
|
151
|
+
- lib/standard_id/http_client.rb
|
|
149
152
|
- lib/standard_id/jwt_service.rb
|
|
150
153
|
- lib/standard_id/oauth/authorization_code_authorization_flow.rb
|
|
151
154
|
- lib/standard_id/oauth/authorization_code_flow.rb
|
|
@@ -156,6 +159,7 @@ files:
|
|
|
156
159
|
- lib/standard_id/oauth/password_flow.rb
|
|
157
160
|
- lib/standard_id/oauth/passwordless_otp_flow.rb
|
|
158
161
|
- lib/standard_id/oauth/refresh_token_flow.rb
|
|
162
|
+
- lib/standard_id/oauth/social_flow.rb
|
|
159
163
|
- lib/standard_id/oauth/subflows/base.rb
|
|
160
164
|
- lib/standard_id/oauth/subflows/social_login_grant.rb
|
|
161
165
|
- lib/standard_id/oauth/subflows/traditional_code_grant.rb
|
|
@@ -164,6 +168,10 @@ files:
|
|
|
164
168
|
- lib/standard_id/passwordless/base_strategy.rb
|
|
165
169
|
- lib/standard_id/passwordless/email_strategy.rb
|
|
166
170
|
- lib/standard_id/passwordless/sms_strategy.rb
|
|
171
|
+
- lib/standard_id/social_providers/apple.rb
|
|
172
|
+
- lib/standard_id/social_providers/google.rb
|
|
173
|
+
- lib/standard_id/social_providers/response_builder.rb
|
|
174
|
+
- lib/standard_id/utils/callable_parameter_filter.rb
|
|
167
175
|
- lib/standard_id/version.rb
|
|
168
176
|
- lib/standard_id/web/authentication_guard.rb
|
|
169
177
|
- lib/standard_id/web/session_manager.rb
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
module StandardId
|
|
2
|
-
module Api
|
|
3
|
-
class ProvidersController < BaseController
|
|
4
|
-
skip_before_action :validate_content_type!
|
|
5
|
-
|
|
6
|
-
def google
|
|
7
|
-
expect_and_permit!([:state, :code], [:state, :code])
|
|
8
|
-
handle_social_callback("google-oauth2")
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def apple
|
|
12
|
-
expect_and_permit!([:state, :code], [:state, :code])
|
|
13
|
-
handle_social_callback("apple")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def handle_social_callback(provider)
|
|
19
|
-
original_params = decode_state_params
|
|
20
|
-
user_info = exchange_social_code_for_user_info(provider, params[:code])
|
|
21
|
-
account = find_or_create_account_from_social(user_info, provider)
|
|
22
|
-
|
|
23
|
-
authorization_code = generate_authorization_code
|
|
24
|
-
store_authorization_code(authorization_code, original_params, account, provider)
|
|
25
|
-
|
|
26
|
-
redirect_params = {
|
|
27
|
-
code: authorization_code,
|
|
28
|
-
state: original_params["state"]
|
|
29
|
-
}.compact
|
|
30
|
-
|
|
31
|
-
redirect_url = build_redirect_uri(original_params["redirect_uri"], redirect_params)
|
|
32
|
-
redirect_to redirect_url, allow_other_host: true, status: :found
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def decode_state_params
|
|
36
|
-
encoded_state = params[:state]
|
|
37
|
-
raise StandardId::InvalidRequestError, "Missing state parameter" if encoded_state.blank?
|
|
38
|
-
|
|
39
|
-
begin
|
|
40
|
-
JSON.parse(Base64.urlsafe_decode64(encoded_state))
|
|
41
|
-
rescue JSON::ParserError, ArgumentError
|
|
42
|
-
raise StandardId::InvalidRequestError, "Invalid state parameter"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def exchange_social_code_for_user_info(provider, code)
|
|
47
|
-
case provider
|
|
48
|
-
when "google-oauth2"
|
|
49
|
-
exchange_google_code(code)
|
|
50
|
-
when "apple"
|
|
51
|
-
exchange_apple_code(code)
|
|
52
|
-
else
|
|
53
|
-
raise StandardId::InvalidRequestError, "Unsupported provider: #{provider}"
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def exchange_google_code(code)
|
|
58
|
-
token_response = HTTParty.post("https://oauth2.googleapis.com/token", {
|
|
59
|
-
body: {
|
|
60
|
-
client_id: StandardId.config.google_client_id,
|
|
61
|
-
client_secret: StandardId.config.google_client_secret,
|
|
62
|
-
code: code,
|
|
63
|
-
grant_type: "authorization_code",
|
|
64
|
-
redirect_uri: "#{request.base_url}/api/oauth/callback/google"
|
|
65
|
-
},
|
|
66
|
-
headers: { "Content-Type" => "application/x-www-form-urlencoded" }
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
raise StandardId::InvalidRequestError, "Failed to exchange Google code" unless token_response.success?
|
|
70
|
-
|
|
71
|
-
access_token = token_response.parsed_response["access_token"]
|
|
72
|
-
|
|
73
|
-
user_response = HTTParty.get("https://www.googleapis.com/oauth2/v2/userinfo", {
|
|
74
|
-
headers: { "Authorization" => "Bearer #{access_token}" }
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
raise StandardId::InvalidRequestError, "Failed to get Google user info" unless user_response.success?
|
|
78
|
-
|
|
79
|
-
user_response.parsed_response
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def exchange_apple_code(code)
|
|
83
|
-
client_secret = generate_apple_client_secret
|
|
84
|
-
|
|
85
|
-
token_response = HTTParty.post("https://appleid.apple.com/auth/token", {
|
|
86
|
-
body: {
|
|
87
|
-
client_id: StandardId.config.apple_client_id,
|
|
88
|
-
client_secret: client_secret,
|
|
89
|
-
code: code,
|
|
90
|
-
grant_type: "authorization_code",
|
|
91
|
-
redirect_uri: "#{request.base_url}/api/oauth/callback/apple"
|
|
92
|
-
},
|
|
93
|
-
headers: { "Content-Type" => "application/x-www-form-urlencoded" }
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
raise StandardId::InvalidRequestError, "Failed to exchange Apple code" unless token_response.success?
|
|
97
|
-
|
|
98
|
-
id_token = token_response.parsed_response["id_token"]
|
|
99
|
-
JWT.decode(id_token, nil, false)[0]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def generate_apple_client_secret
|
|
103
|
-
header = {
|
|
104
|
-
alg: "ES256",
|
|
105
|
-
kid: StandardId.config.apple_key_id
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
payload = {
|
|
109
|
-
iss: StandardId.config.apple_team_id,
|
|
110
|
-
iat: Time.current.to_i,
|
|
111
|
-
exp: Time.current.to_i + 3600,
|
|
112
|
-
aud: "https://appleid.apple.com",
|
|
113
|
-
sub: StandardId.config.apple_client_id
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private_key = OpenSSL::PKey::EC.new(StandardId.config.apple_private_key)
|
|
117
|
-
JWT.encode(payload, private_key, "ES256", header)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def find_or_create_account_from_social(user_info, provider)
|
|
121
|
-
email = user_info["email"]
|
|
122
|
-
raise StandardId::InvalidRequestError, "No email provided by #{provider}" if email.blank?
|
|
123
|
-
|
|
124
|
-
identifier = StandardId::EmailIdentifier.find_by(value: email)
|
|
125
|
-
|
|
126
|
-
return identifier.account if identifier.present?
|
|
127
|
-
|
|
128
|
-
account = Account.create!(
|
|
129
|
-
name: (user_info["name"] || user_info["given_name"] || email),
|
|
130
|
-
email:
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
identifier = StandardId::EmailIdentifier.create!(
|
|
134
|
-
account:,
|
|
135
|
-
value: email
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
identifier.verify!
|
|
139
|
-
|
|
140
|
-
account
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def generate_authorization_code
|
|
144
|
-
SecureRandom.urlsafe_base64(32)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def store_authorization_code(code, original_params, account, provider)
|
|
148
|
-
StandardId::AuthorizationCode.issue!(
|
|
149
|
-
plaintext_code: code,
|
|
150
|
-
client_id: original_params["client_id"],
|
|
151
|
-
redirect_uri: original_params["redirect_uri"],
|
|
152
|
-
scope: original_params["scope"],
|
|
153
|
-
audience: original_params["audience"],
|
|
154
|
-
account: account,
|
|
155
|
-
code_challenge: original_params["code_challenge"],
|
|
156
|
-
code_challenge_method: original_params["code_challenge_method"],
|
|
157
|
-
metadata: { state: original_params["state"], provider: provider }.compact
|
|
158
|
-
)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def build_redirect_uri(base_uri, params_hash)
|
|
162
|
-
uri = URI.parse(base_uri)
|
|
163
|
-
query_params = URI.decode_www_form(uri.query || "")
|
|
164
|
-
|
|
165
|
-
params_hash.each do |key, value|
|
|
166
|
-
query_params << [key.to_s, value.to_s] if value.present?
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
uri.query = URI.encode_www_form(query_params)
|
|
170
|
-
uri.to_s
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|