standard_id 0.1.3 → 0.1.5

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.
@@ -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