kinde_sdk 1.7.1 → 1.8.0
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/app/controllers/kinde_sdk/auth_controller.rb +10 -2
- data/kinde_api/Gemfile +2 -2
- data/kinde_api/docs/ConnectionsApi.md +1 -1
- data/kinde_api/lib/kinde_api/api/connections_api.rb +1 -1
- data/kinde_api/lib/kinde_api/api_client.rb +7 -1
- data/kinde_api/lib/kinde_api/models/get_connections_response.rb +1 -1
- data/kinde_api/spec/api_client_spec.rb +3 -1
- data/kinde_api/spec/models/get_connections_response_spec.rb +18 -2
- data/lib/kinde_sdk/client/entitlements.rb +2 -16
- data/lib/kinde_sdk/client/feature_flags.rb +7 -16
- data/lib/kinde_sdk/client/permissions.rb +1 -28
- data/lib/kinde_sdk/client/roles.rb +10 -42
- data/lib/kinde_sdk/client.rb +17 -16
- data/lib/kinde_sdk/logging.rb +98 -0
- data/lib/kinde_sdk/token_hash.rb +58 -0
- data/lib/kinde_sdk/token_manager.rb +10 -6
- data/lib/kinde_sdk/token_store.rb +1 -1
- data/lib/kinde_sdk/version.rb +1 -1
- data/lib/kinde_sdk.rb +68 -22
- data/spec/examples.txt +43 -27
- data/spec/kinde_sdk_spec.rb +225 -0
- metadata +16 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e745fcc09127b3a90b6b4049d9bc490f268896ab75612681a648600b4bb8033
|
|
4
|
+
data.tar.gz: 63cdad5a63d19c05b86cff557750ce5b75cdd8f65c99a30eb88ea6cc5032791e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6aada675872e57b16fce0d548f0b02c205a9c2e0aa71a8f6d2160424b4625fb237d84df2c549bbf3112efc5d49f2d0465934a387ec285355e52581e4c2ddd014
|
|
7
|
+
data.tar.gz: 2dd83d28cbd2fd1211411c84ce578721684e675b072588c97e4422b5f201bad5c623c0e57c49866b04aa8e6948b131a663fd6096896f33b263694a0bc683fef1
|
|
@@ -20,9 +20,17 @@ module KindeSdk
|
|
|
20
20
|
def auth
|
|
21
21
|
# Generate a secure random nonce for CSRF protection
|
|
22
22
|
nonce = SecureRandom.urlsafe_base64(16)
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
# Get authorization URL and PKCE code verifier from SDK
|
|
25
|
-
|
|
25
|
+
auth_params = { nonce: nonce }
|
|
26
|
+
|
|
27
|
+
# Check for invitation_code in query parameters
|
|
28
|
+
# Validate to prevent injection and ensure it's a reasonable length
|
|
29
|
+
if params[:invitation_code].is_a?(String) && params[:invitation_code].strip.length > 0 && params[:invitation_code].length <= 255
|
|
30
|
+
auth_params[:invitation_code] = params[:invitation_code].strip
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
auth_data = KindeSdk.auth_url(**auth_params)
|
|
26
34
|
|
|
27
35
|
# Store PKCE code verifier and nonce in session for validation
|
|
28
36
|
session[:code_verifier] = auth_data[:code_verifier] if auth_data[:code_verifier].present?
|
data/kinde_api/Gemfile
CHANGED
|
@@ -251,7 +251,7 @@ module KindeApi
|
|
|
251
251
|
# header parameters
|
|
252
252
|
header_params = opts[:header_params] || {}
|
|
253
253
|
# HTTP header 'Accept' (if needed)
|
|
254
|
-
header_params['Accept'] = @api_client.select_header_accept(['application/json
|
|
254
|
+
header_params['Accept'] = @api_client.select_header_accept(['application/json'])
|
|
255
255
|
|
|
256
256
|
# form parameters
|
|
257
257
|
form_params = opts[:form_params] || {}
|
|
@@ -328,9 +328,15 @@ module KindeApi
|
|
|
328
328
|
# @return [String] the Accept header (e.g. application/json)
|
|
329
329
|
def select_header_accept(accepts)
|
|
330
330
|
return nil if accepts.nil? || accepts.empty?
|
|
331
|
+
# Prefer plain application/json; the API rejects charset variants in Accept.
|
|
332
|
+
plain_json = accepts.find { |s| s.strip.casecmp('application/json').zero? }
|
|
333
|
+
return plain_json if plain_json
|
|
331
334
|
# use JSON when present, otherwise use all of the provided
|
|
332
335
|
json_accept = accepts.find { |s| json_mime?(s) }
|
|
333
|
-
json_accept
|
|
336
|
+
if json_accept
|
|
337
|
+
return json_accept.strip.casecmp('application/json').zero? ? json_accept : 'application/json'
|
|
338
|
+
end
|
|
339
|
+
accepts.join(',')
|
|
334
340
|
end
|
|
335
341
|
|
|
336
342
|
# Return Content-Type header based on an array of content types provided.
|
|
@@ -187,7 +187,9 @@ describe KindeApi::ApiClient do
|
|
|
187
187
|
expect(api_client.select_header_accept([])).to be_nil
|
|
188
188
|
|
|
189
189
|
expect(api_client.select_header_accept(['application/json'])).to eq('application/json')
|
|
190
|
-
expect(api_client.select_header_accept(['application/
|
|
190
|
+
expect(api_client.select_header_accept(['application/json; charset=utf-8'])).to eq('application/json')
|
|
191
|
+
expect(api_client.select_header_accept(['application/xml', 'application/json; charset=UTF8'])).to eq('application/json')
|
|
192
|
+
expect(api_client.select_header_accept(['application/json; charset=utf-8', 'application/json'])).to eq('application/json')
|
|
191
193
|
expect(api_client.select_header_accept(['APPLICATION/JSON', 'text/html'])).to eq('APPLICATION/JSON')
|
|
192
194
|
|
|
193
195
|
expect(api_client.select_header_accept(['application/xml'])).to eq('application/xml')
|
|
@@ -38,8 +38,24 @@ describe KindeApi::GetConnectionsResponse do
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
describe 'test attribute "connections"' do
|
|
41
|
-
it '
|
|
42
|
-
|
|
41
|
+
it 'deserializes flat connection objects from the list endpoint' do
|
|
42
|
+
response = described_class.build_from_hash(
|
|
43
|
+
'connections' => [
|
|
44
|
+
{
|
|
45
|
+
'id' => 'conn_123',
|
|
46
|
+
'name' => 'google',
|
|
47
|
+
'display_name' => 'Google',
|
|
48
|
+
'strategy' => 'oauth2'
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
connection = response.connections.first
|
|
54
|
+
expect(connection).to be_a(KindeApi::ConnectionConnection)
|
|
55
|
+
expect(connection.id).to eq('conn_123')
|
|
56
|
+
expect(connection.name).to eq('google')
|
|
57
|
+
expect(connection.display_name).to eq('Google')
|
|
58
|
+
expect(connection.strategy).to eq('oauth2')
|
|
43
59
|
end
|
|
44
60
|
end
|
|
45
61
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module KindeSdk
|
|
2
2
|
class Client
|
|
3
3
|
module Entitlements
|
|
4
|
+
include KindeSdk::Logging
|
|
4
5
|
# Get all entitlements for the authenticated user
|
|
5
6
|
# Matches the JavaScript SDK API: getEntitlements(options?)
|
|
6
7
|
#
|
|
@@ -66,21 +67,6 @@ module KindeSdk
|
|
|
66
67
|
# JavaScript SDK compatible aliases
|
|
67
68
|
alias_method :hasBillingEntitlements, :has_billing_entitlements?
|
|
68
69
|
alias_method :hasEntitlements, :has_entitlements?
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
# Configurable logging that works with or without Rails
|
|
73
|
-
def log_error(message)
|
|
74
|
-
if defined?(Rails) && Rails.logger
|
|
75
|
-
Rails.logger.error(message)
|
|
76
|
-
elsif @logger
|
|
77
|
-
@logger.error(message)
|
|
78
|
-
elsif respond_to?(:logger) && logger
|
|
79
|
-
logger.error(message)
|
|
80
|
-
else
|
|
81
|
-
$stderr.puts "[KindeSdk] ERROR: #{message}"
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
70
|
end
|
|
85
71
|
end
|
|
86
|
-
end
|
|
72
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module KindeSdk
|
|
2
2
|
class Client
|
|
3
3
|
module FeatureFlags
|
|
4
|
+
include KindeSdk::Logging
|
|
4
5
|
# Get all feature flags for the authenticated user
|
|
5
6
|
# Matches the JavaScript SDK API: getFlags(options?)
|
|
6
7
|
#
|
|
@@ -137,27 +138,30 @@ module KindeSdk
|
|
|
137
138
|
end
|
|
138
139
|
|
|
139
140
|
# Extract the key from a flag object (handles both symbol and string keys)
|
|
141
|
+
# Uses key? to properly handle falsy values
|
|
140
142
|
#
|
|
141
143
|
# @param flag [Hash] Flag object
|
|
142
144
|
# @return [String] The flag key
|
|
143
145
|
def get_flag_key(flag)
|
|
144
|
-
flag[:key]
|
|
146
|
+
flag.key?(:key) ? flag[:key] : flag['key']
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
# Extract the value from a flag object (handles both symbol and string keys)
|
|
150
|
+
# Uses key? to properly handle boolean false values (critical fix)
|
|
148
151
|
#
|
|
149
152
|
# @param flag [Hash] Flag object
|
|
150
153
|
# @return [Object] The flag value
|
|
151
154
|
def get_flag_value(flag)
|
|
152
|
-
flag[:value]
|
|
155
|
+
flag.key?(:value) ? flag[:value] : flag['value']
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
# Extract the type from a flag object (handles both symbol and string keys)
|
|
159
|
+
# Uses key? to properly handle falsy values
|
|
156
160
|
#
|
|
157
161
|
# @param flag [Hash] Flag object
|
|
158
162
|
# @return [String] The flag type
|
|
159
163
|
def get_flag_type(flag)
|
|
160
|
-
flag[:type]
|
|
164
|
+
flag.key?(:type) ? flag[:type] : flag['type']
|
|
161
165
|
end
|
|
162
166
|
|
|
163
167
|
# Legacy get_flag implementation for backward compatibility
|
|
@@ -282,19 +286,6 @@ module KindeSdk
|
|
|
282
286
|
def check_type(value, type)
|
|
283
287
|
type == "s" && value.is_a?(String) || type == "b" && (value == false || value == true) || type == "i" && value.is_a?(Integer)
|
|
284
288
|
end
|
|
285
|
-
|
|
286
|
-
# Configurable logging that works with or without Rails
|
|
287
|
-
def log_error(message)
|
|
288
|
-
if defined?(Rails) && Rails.logger
|
|
289
|
-
Rails.logger.error(message)
|
|
290
|
-
elsif @logger
|
|
291
|
-
@logger.error(message)
|
|
292
|
-
elsif respond_to?(:logger) && logger
|
|
293
|
-
logger.error(message)
|
|
294
|
-
else
|
|
295
|
-
$stderr.puts "[KindeSdk] ERROR: #{message}"
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
289
|
end
|
|
299
290
|
end
|
|
300
291
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module KindeSdk
|
|
2
2
|
class Client
|
|
3
3
|
module Permissions
|
|
4
|
+
include KindeSdk::Logging
|
|
4
5
|
# Get all permissions for the authenticated user
|
|
5
6
|
# Matches the JavaScript SDK API: getPermissions(options?)
|
|
6
7
|
#
|
|
@@ -178,34 +179,6 @@ module KindeSdk
|
|
|
178
179
|
get_permissions_from_token
|
|
179
180
|
end
|
|
180
181
|
end
|
|
181
|
-
|
|
182
|
-
# Configurable logging that works with or without Rails
|
|
183
|
-
#
|
|
184
|
-
# @param message [String] The error message to log
|
|
185
|
-
def log_error(message)
|
|
186
|
-
if defined?(Rails) && Rails.logger
|
|
187
|
-
Rails.logger.error(message)
|
|
188
|
-
elsif @logger
|
|
189
|
-
@logger.error(message)
|
|
190
|
-
elsif respond_to?(:logger) && logger
|
|
191
|
-
logger.error(message)
|
|
192
|
-
else
|
|
193
|
-
# Fallback to STDERR if no logger available
|
|
194
|
-
$stderr.puts "[KindeSdk] ERROR: #{message}"
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def log_warning(message)
|
|
199
|
-
if defined?(Rails) && Rails.logger
|
|
200
|
-
Rails.logger.warn(message)
|
|
201
|
-
elsif @logger
|
|
202
|
-
@logger.warn(message)
|
|
203
|
-
elsif respond_to?(:logger) && logger
|
|
204
|
-
logger.warn(message)
|
|
205
|
-
else
|
|
206
|
-
$stderr.puts "[KindeSdk] WARNING: #{message}"
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
182
|
end
|
|
210
183
|
end
|
|
211
184
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module KindeSdk
|
|
2
2
|
class Client
|
|
3
3
|
module Roles
|
|
4
|
+
include KindeSdk::Logging
|
|
4
5
|
# Get all roles for the authenticated user
|
|
5
6
|
# Matches the JavaScript SDK API: getRoles(options?)
|
|
6
7
|
# Implements smart fallback: uses API automatically if token claims are empty
|
|
@@ -166,53 +167,20 @@ module KindeSdk
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# Helper to extract field from hash or object
|
|
170
|
+
# Checks Hash first to avoid issues with Hash#key requiring an argument
|
|
171
|
+
#
|
|
172
|
+
# @param item [Hash, Object] The item to extract from
|
|
173
|
+
# @param field [Symbol] The field name to extract
|
|
174
|
+
# @return [Object, nil] The field value or nil
|
|
169
175
|
def extract_field(item, field)
|
|
170
|
-
if item.
|
|
176
|
+
if item.is_a?(Hash)
|
|
177
|
+
item.key?(field) ? item[field] : item[field.to_s]
|
|
178
|
+
elsif item.respond_to?(field)
|
|
171
179
|
item.public_send(field)
|
|
172
|
-
elsif item.is_a?(Hash)
|
|
173
|
-
item[field] || item[field.to_s]
|
|
174
180
|
else
|
|
175
181
|
nil
|
|
176
182
|
end
|
|
177
183
|
end
|
|
178
|
-
|
|
179
|
-
# Configurable logging that works with or without Rails
|
|
180
|
-
# Supports multiple log levels for better debugging
|
|
181
|
-
def log_error(message)
|
|
182
|
-
write_log(:error, message)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def log_warning(message)
|
|
186
|
-
write_log(:warn, message)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def log_info(message)
|
|
190
|
-
write_log(:info, message)
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def log_debug(message)
|
|
194
|
-
write_log(:debug, message)
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def write_log(level, message)
|
|
198
|
-
formatted_message = "[KindeSdk::Roles] #{message}"
|
|
199
|
-
|
|
200
|
-
if defined?(Rails) && Rails.logger
|
|
201
|
-
Rails.logger.public_send(level, formatted_message)
|
|
202
|
-
elsif @logger && @logger.respond_to?(level)
|
|
203
|
-
@logger.public_send(level, formatted_message)
|
|
204
|
-
elsif respond_to?(:logger) && logger && logger.respond_to?(level)
|
|
205
|
-
logger.public_send(level, formatted_message)
|
|
206
|
-
else
|
|
207
|
-
# Fallback based on level
|
|
208
|
-
case level
|
|
209
|
-
when :error, :warn
|
|
210
|
-
$stderr.puts formatted_message
|
|
211
|
-
when :info, :debug
|
|
212
|
-
$stdout.puts formatted_message if ENV['KINDE_DEBUG']
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
184
|
end
|
|
217
185
|
end
|
|
218
|
-
end
|
|
186
|
+
end
|
data/lib/kinde_sdk/client.rb
CHANGED
|
@@ -21,6 +21,7 @@ module KindeSdk
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class Client
|
|
24
|
+
include Logging
|
|
24
25
|
include FeatureFlags
|
|
25
26
|
include Permissions
|
|
26
27
|
include Roles
|
|
@@ -120,7 +121,7 @@ module KindeSdk
|
|
|
120
121
|
begin
|
|
121
122
|
{ url: URI.parse(result['url']) }
|
|
122
123
|
rescue URI::InvalidURIError => e
|
|
123
|
-
|
|
124
|
+
log_error(e.message)
|
|
124
125
|
raise StandardError, "Invalid URL format received from API: #{result['url']}"
|
|
125
126
|
end
|
|
126
127
|
end
|
|
@@ -176,7 +177,7 @@ module KindeSdk
|
|
|
176
177
|
def entitlements(page_size: 10, starting_after: nil)
|
|
177
178
|
frontend.get_entitlements(page_size: page_size, starting_after: starting_after)
|
|
178
179
|
rescue StandardError => e
|
|
179
|
-
|
|
180
|
+
log_error("Failed to fetch entitlements: #{e.message}")
|
|
180
181
|
raise KindeSdk::APIError, "Unable to fetch entitlements: #{e.message}"
|
|
181
182
|
end
|
|
182
183
|
|
|
@@ -191,7 +192,7 @@ module KindeSdk
|
|
|
191
192
|
|
|
192
193
|
paginate_all_results('entitlements') { |starting_after| entitlements(page_size: 100, starting_after: starting_after) }
|
|
193
194
|
rescue StandardError => e
|
|
194
|
-
|
|
195
|
+
log_error("Failed to fetch all entitlements: #{e.message}")
|
|
195
196
|
raise KindeSdk::APIError, "Unable to fetch all entitlements: #{e.message}"
|
|
196
197
|
end
|
|
197
198
|
|
|
@@ -206,7 +207,7 @@ module KindeSdk
|
|
|
206
207
|
def entitlement(key)
|
|
207
208
|
frontend.get_entitlement(key)
|
|
208
209
|
rescue StandardError => e
|
|
209
|
-
|
|
210
|
+
log_error("Failed to fetch entitlement for #{key}: #{e.message}")
|
|
210
211
|
raise KindeSdk::APIError, "Unable to fetch entitlement: #{e.message}"
|
|
211
212
|
end
|
|
212
213
|
|
|
@@ -221,7 +222,7 @@ module KindeSdk
|
|
|
221
222
|
|
|
222
223
|
entitlements.find { |entitlement| entitlement.feature_key == key }
|
|
223
224
|
rescue StandardError => e
|
|
224
|
-
|
|
225
|
+
log_error("Failed to get entitlement for #{key}: #{e.message}")
|
|
225
226
|
raise KindeSdk::APIError, "Unable to get entitlement: #{e.message}"
|
|
226
227
|
end
|
|
227
228
|
|
|
@@ -231,9 +232,9 @@ module KindeSdk
|
|
|
231
232
|
# @return [Boolean] True if the user has the entitlement, false otherwise
|
|
232
233
|
def has_entitlement?(feature_key)
|
|
233
234
|
entitlement_response = entitlement(feature_key)
|
|
234
|
-
entitlement_response&.data&.entitlement.
|
|
235
|
+
!entitlement_response&.data&.entitlement.nil?
|
|
235
236
|
rescue StandardError => e
|
|
236
|
-
|
|
237
|
+
log_error("Error checking entitlement for #{feature_key}: #{e.message}")
|
|
237
238
|
false
|
|
238
239
|
end
|
|
239
240
|
|
|
@@ -244,7 +245,7 @@ module KindeSdk
|
|
|
244
245
|
def hasEntitlement(key)
|
|
245
246
|
getEntitlement(key) != nil
|
|
246
247
|
rescue StandardError => e
|
|
247
|
-
|
|
248
|
+
log_error("Error checking entitlement for #{key}: #{e.message}")
|
|
248
249
|
false
|
|
249
250
|
end
|
|
250
251
|
|
|
@@ -257,7 +258,7 @@ module KindeSdk
|
|
|
257
258
|
entitlement = getEntitlement(key)
|
|
258
259
|
entitlement ? entitlement.entitlement_limit_max : nil
|
|
259
260
|
rescue StandardError => e
|
|
260
|
-
|
|
261
|
+
log_error("Error getting entitlement limit for #{key}: #{e.message}")
|
|
261
262
|
nil
|
|
262
263
|
end
|
|
263
264
|
|
|
@@ -273,7 +274,7 @@ module KindeSdk
|
|
|
273
274
|
def user_feature_flags(page_size: 10, starting_after: nil)
|
|
274
275
|
frontend.get_feature_flags(page_size: page_size, starting_after: starting_after)
|
|
275
276
|
rescue StandardError => e
|
|
276
|
-
|
|
277
|
+
log_error("Failed to fetch feature flags: #{e.message}")
|
|
277
278
|
raise KindeSdk::APIError, "Unable to fetch feature flags: #{e.message}"
|
|
278
279
|
end
|
|
279
280
|
|
|
@@ -286,7 +287,7 @@ module KindeSdk
|
|
|
286
287
|
def user_permissions(page_size: 10, starting_after: nil)
|
|
287
288
|
frontend.get_user_permissions(page_size: page_size, starting_after: starting_after)
|
|
288
289
|
rescue StandardError => e
|
|
289
|
-
|
|
290
|
+
log_error("Failed to fetch permissions: #{e.message}")
|
|
290
291
|
raise KindeSdk::APIError, "Unable to fetch permissions: #{e.message}"
|
|
291
292
|
end
|
|
292
293
|
|
|
@@ -299,7 +300,7 @@ module KindeSdk
|
|
|
299
300
|
def user_properties(page_size: 10, starting_after: nil)
|
|
300
301
|
frontend.get_user_properties(page_size: page_size, starting_after: starting_after)
|
|
301
302
|
rescue StandardError => e
|
|
302
|
-
|
|
303
|
+
log_error("Failed to fetch properties: #{e.message}")
|
|
303
304
|
raise KindeSdk::APIError, "Unable to fetch properties: #{e.message}"
|
|
304
305
|
end
|
|
305
306
|
|
|
@@ -312,7 +313,7 @@ module KindeSdk
|
|
|
312
313
|
def user_roles(page_size: 10, starting_after: nil)
|
|
313
314
|
frontend.get_user_roles(page_size: page_size, starting_after: starting_after)
|
|
314
315
|
rescue StandardError => e
|
|
315
|
-
|
|
316
|
+
log_error("Failed to fetch roles: #{e.message}")
|
|
316
317
|
raise KindeSdk::APIError, "Unable to fetch roles: #{e.message}"
|
|
317
318
|
end
|
|
318
319
|
|
|
@@ -325,7 +326,7 @@ module KindeSdk
|
|
|
325
326
|
def portal_link(return_url:, page: PortalPage::PROFILE)
|
|
326
327
|
frontend.get_portal_link(subnav: page, return_url: return_url)
|
|
327
328
|
rescue StandardError => e
|
|
328
|
-
|
|
329
|
+
log_error("Failed to get portal link: #{e.message}")
|
|
329
330
|
raise KindeSdk::APIError, "Unable to get portal link: #{e.message}"
|
|
330
331
|
end
|
|
331
332
|
|
|
@@ -347,7 +348,7 @@ module KindeSdk
|
|
|
347
348
|
|
|
348
349
|
frontend.get_portal_link(subnav: sub_nav, return_url: return_url)
|
|
349
350
|
rescue StandardError => e
|
|
350
|
-
|
|
351
|
+
log_error("Failed to generate portal URL: #{e.message}")
|
|
351
352
|
raise KindeSdk::APIError, "Unable to generate portal URL: #{e.message}"
|
|
352
353
|
end
|
|
353
354
|
|
|
@@ -358,7 +359,7 @@ module KindeSdk
|
|
|
358
359
|
def enhanced_user_profile
|
|
359
360
|
frontend.get_user_profile_v2
|
|
360
361
|
rescue StandardError => e
|
|
361
|
-
|
|
362
|
+
log_error("Failed to fetch enhanced profile: #{e.message}")
|
|
362
363
|
raise KindeSdk::APIError, "Unable to fetch enhanced profile: #{e.message}"
|
|
363
364
|
end
|
|
364
365
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KindeSdk
|
|
4
|
+
# Shared logging module for consistent logging across all SDK components.
|
|
5
|
+
# Supports Rails logger, custom logger, or falls back to stdout/stderr.
|
|
6
|
+
#
|
|
7
|
+
# Can be used as:
|
|
8
|
+
# - Instance methods via `include KindeSdk::Logging`
|
|
9
|
+
# - Class methods via `include KindeSdk::Logging` inside `class << self`
|
|
10
|
+
# - Direct calls via `KindeSdk::Logging.log_error("message")`
|
|
11
|
+
#
|
|
12
|
+
# @example Using in a class (instance methods)
|
|
13
|
+
# class MyClass
|
|
14
|
+
# include KindeSdk::Logging
|
|
15
|
+
#
|
|
16
|
+
# def my_method
|
|
17
|
+
# log_info("Starting operation")
|
|
18
|
+
# log_error("Something went wrong")
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Using for class methods
|
|
23
|
+
# class MyClass
|
|
24
|
+
# class << self
|
|
25
|
+
# include KindeSdk::Logging
|
|
26
|
+
#
|
|
27
|
+
# def my_class_method
|
|
28
|
+
# log_error("Class-level error")
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
module Logging
|
|
33
|
+
# Log an error message
|
|
34
|
+
#
|
|
35
|
+
# @param message [String] The message to log
|
|
36
|
+
def log_error(message)
|
|
37
|
+
Logging.write_log(:error, message)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Log a warning message
|
|
41
|
+
#
|
|
42
|
+
# @param message [String] The message to log
|
|
43
|
+
def log_warning(message)
|
|
44
|
+
Logging.write_log(:warn, message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Log an info message
|
|
48
|
+
#
|
|
49
|
+
# @param message [String] The message to log
|
|
50
|
+
def log_info(message)
|
|
51
|
+
Logging.write_log(:info, message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Log a debug message
|
|
55
|
+
#
|
|
56
|
+
# @param message [String] The message to log
|
|
57
|
+
def log_debug(message)
|
|
58
|
+
Logging.write_log(:debug, message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
# Write a log message at the specified level
|
|
63
|
+
# Tries multiple logger sources in order of preference:
|
|
64
|
+
# 1. Rails.logger (if Rails is defined)
|
|
65
|
+
# 2. Fallback to $stderr/$stdout based on level
|
|
66
|
+
#
|
|
67
|
+
# @param level [Symbol] The log level (:error, :warn, :info, :debug)
|
|
68
|
+
# @param message [String] The message to log
|
|
69
|
+
def write_log(level, message)
|
|
70
|
+
formatted_message = "[KindeSdk] #{message}"
|
|
71
|
+
|
|
72
|
+
logger_instance = resolve_logger
|
|
73
|
+
if logger_instance&.respond_to?(level)
|
|
74
|
+
logger_instance.public_send(level, formatted_message)
|
|
75
|
+
else
|
|
76
|
+
# Fallback based on level
|
|
77
|
+
case level
|
|
78
|
+
when :error, :warn
|
|
79
|
+
$stderr.puts formatted_message
|
|
80
|
+
when :info, :debug
|
|
81
|
+
$stdout.puts formatted_message if ENV['KINDE_DEBUG']
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Resolve the appropriate logger instance
|
|
87
|
+
#
|
|
88
|
+
# @return [Logger, nil] The logger to use, or nil if none available
|
|
89
|
+
def resolve_logger
|
|
90
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
91
|
+
Rails.logger
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module KindeSdk
|
|
2
|
+
# Normalizes token hash key types for oauth2 round-trips and SDK consumers.
|
|
3
|
+
# Accepts string or symbol keys on input; internal storage uses symbol keys.
|
|
4
|
+
module TokenHash
|
|
5
|
+
PUBLIC_KEYS = %i[
|
|
6
|
+
access_token refresh_token expires_at id_token scope token_type expires_in
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Symbolize keys while preserving the full oauth2 hash contract.
|
|
12
|
+
def normalize(raw)
|
|
13
|
+
return {} if raw.nil?
|
|
14
|
+
return {} if raw.respond_to?(:empty?) && raw.empty?
|
|
15
|
+
|
|
16
|
+
raw.each_with_object({}) do |(key, value), memo|
|
|
17
|
+
memo[key.to_sym] = value
|
|
18
|
+
end.tap do |hash|
|
|
19
|
+
hash[:access_token] = hash[:access_token].to_s if hash[:access_token]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def access_token(raw)
|
|
24
|
+
normalize(raw)[:access_token]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reduced shape for documented public SDK responses (e.g. fetch_tokens).
|
|
28
|
+
def public_tokens(raw)
|
|
29
|
+
normalize(raw).slice(*PUBLIC_KEYS).compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def from_access_token(token)
|
|
33
|
+
public_tokens(
|
|
34
|
+
access_token: token.token,
|
|
35
|
+
refresh_token: token.refresh_token,
|
|
36
|
+
expires_at: token.expires_at,
|
|
37
|
+
id_token: token_param(token, "id_token"),
|
|
38
|
+
scope: token_param(token, "scope"),
|
|
39
|
+
token_type: token_param(token, "token_type"),
|
|
40
|
+
expires_in: token_param(token, "expires_in")
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# String keys for session storage and documented public API responses.
|
|
45
|
+
def for_session(raw)
|
|
46
|
+
public_tokens(raw).transform_keys(&:to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Full oauth2 hash with string keys for refresh_token responses.
|
|
50
|
+
def for_refresh_response(raw)
|
|
51
|
+
normalize(raw).transform_keys(&:to_s)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def token_param(token, key)
|
|
55
|
+
token.params[key] || token.params[key.to_sym]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module KindeSdk
|
|
2
2
|
class TokenManager
|
|
3
|
+
include Logging
|
|
4
|
+
|
|
3
5
|
attr_reader :tokens, :bearer_token, :expires_at
|
|
4
6
|
|
|
5
7
|
def initialize(tokens = nil)
|
|
@@ -7,19 +9,19 @@ module KindeSdk
|
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
def set_tokens(tokens)
|
|
10
|
-
@tokens =
|
|
12
|
+
@tokens = TokenHash.normalize(tokens).freeze
|
|
11
13
|
@bearer_token = @tokens[:access_token]
|
|
12
14
|
@expires_at = @tokens[:expires_at]
|
|
13
15
|
@tokens
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def token_expired?
|
|
17
|
-
return true
|
|
19
|
+
return true if @tokens.nil? || @tokens.empty?
|
|
18
20
|
begin
|
|
19
21
|
KindeSdk.validate_jwt_token(@tokens)
|
|
20
22
|
@expires_at.to_i > 0 && (@expires_at <= Time.now.to_i)
|
|
21
|
-
rescue
|
|
22
|
-
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
log_error("Error checking token expiration: #{e.message}")
|
|
23
25
|
true
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -31,6 +33,8 @@ module KindeSdk
|
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
class << self
|
|
36
|
+
extend KindeSdk::Logging
|
|
37
|
+
|
|
34
38
|
def create_store(tokens = nil)
|
|
35
39
|
TokenStore.new(tokens)
|
|
36
40
|
end
|
|
@@ -71,7 +75,7 @@ module KindeSdk
|
|
|
71
75
|
def safe_refresh(tokens)
|
|
72
76
|
KindeSdk.refresh_token(tokens)
|
|
73
77
|
rescue StandardError => e
|
|
74
|
-
|
|
78
|
+
log_error("Token refresh failed: #{e.message}")
|
|
75
79
|
nil
|
|
76
80
|
end
|
|
77
81
|
|
|
@@ -82,4 +86,4 @@ module KindeSdk
|
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
end
|
|
85
|
-
end
|
|
89
|
+
end
|
data/lib/kinde_sdk/version.rb
CHANGED
data/lib/kinde_sdk.rb
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
require "logger"
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
# Rails is optional - SDK works in non-Rails Ruby applications
|
|
4
|
+
begin
|
|
5
|
+
require "rails"
|
|
6
|
+
rescue LoadError
|
|
7
|
+
# Rails not available, that's fine
|
|
8
|
+
end
|
|
9
|
+
|
|
3
10
|
require "kinde_sdk/version"
|
|
4
11
|
require "kinde_sdk/configuration"
|
|
12
|
+
require "kinde_sdk/logging"
|
|
13
|
+
require "kinde_sdk/token_hash"
|
|
5
14
|
require "kinde_sdk/client/feature_flags"
|
|
6
15
|
require "kinde_sdk/client/permissions"
|
|
7
16
|
require "kinde_sdk/client"
|
|
@@ -24,6 +33,8 @@ module KindeSdk
|
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
class << self
|
|
36
|
+
include Logging
|
|
37
|
+
|
|
27
38
|
attr_accessor :config
|
|
28
39
|
|
|
29
40
|
if defined?(Rails)
|
|
@@ -55,6 +66,13 @@ module KindeSdk
|
|
|
55
66
|
scope: @config.scope,
|
|
56
67
|
supports_reauth: "true"
|
|
57
68
|
}.merge(**kwargs)
|
|
69
|
+
|
|
70
|
+
if params[:invitation_code].is_a?(String) && !params[:invitation_code].strip.empty?
|
|
71
|
+
params[:invitation_code] = params[:invitation_code].strip
|
|
72
|
+
params[:is_invitation] = "true"
|
|
73
|
+
else
|
|
74
|
+
params.delete(:invitation_code)
|
|
75
|
+
end
|
|
58
76
|
return { url: @config.oauth_client(
|
|
59
77
|
client_id: client_id,
|
|
60
78
|
client_secret: client_secret,
|
|
@@ -98,14 +116,7 @@ module KindeSdk
|
|
|
98
116
|
authorize_url: "#{domain}/oauth2/auth",
|
|
99
117
|
token_url: "#{domain}/oauth2/token").auth_code.get_token(code.to_s, params)
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
access_token: token.token, # The access token
|
|
103
|
-
id_token: token.params['id_token'], # The ID token from params
|
|
104
|
-
expires_at: token.expires_at, # Optional: expiration time
|
|
105
|
-
refresh_token: token.refresh_token, # Optional: if present
|
|
106
|
-
scope: token.params['scope'], # The scopes requested
|
|
107
|
-
token_type: token.params['token_type'] # The token type
|
|
108
|
-
}.compact
|
|
119
|
+
TokenHash.from_access_token(token)
|
|
109
120
|
end
|
|
110
121
|
|
|
111
122
|
# tokens_hash #=>
|
|
@@ -118,14 +129,46 @@ module KindeSdk
|
|
|
118
129
|
#
|
|
119
130
|
# @return [KindeSdk::Client]
|
|
120
131
|
def client(tokens_hash, auto_refresh_tokens = @config.auto_refresh_tokens, force_api = @config.force_api)
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
normalized_tokens = TokenHash.normalize(tokens_hash)
|
|
133
|
+
bearer_token = normalized_tokens[:access_token]
|
|
134
|
+
if bearer_token.nil? || bearer_token.empty?
|
|
135
|
+
raise AuthenticationError, "Missing access_token in token hash"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sdk_api_client = api_client(bearer_token)
|
|
139
|
+
KindeSdk::Client.new(sdk_api_client, normalized_tokens, auto_refresh_tokens, force_api)
|
|
123
140
|
end
|
|
124
141
|
|
|
125
142
|
def logout_url(logout_url: @config.logout_url, domain: @config.domain)
|
|
143
|
+
raise ArgumentError, "domain is required for logout_url" if domain.nil? || domain.to_s.strip.empty?
|
|
144
|
+
|
|
126
145
|
query = logout_url ? URI.encode_www_form(redirect: logout_url) : nil
|
|
127
|
-
|
|
128
|
-
|
|
146
|
+
normalized_domain = domain.to_s.strip
|
|
147
|
+
|
|
148
|
+
# Only allow http/https schemes; prepend https:// if no scheme present
|
|
149
|
+
if normalized_domain.match?(%r{\Ahttps?://}i)
|
|
150
|
+
# Valid http/https scheme - use as-is
|
|
151
|
+
elsif normalized_domain.match?(%r{\A\w+://})
|
|
152
|
+
# Invalid scheme (ftp://, file://, etc.)
|
|
153
|
+
raise ArgumentError, "invalid scheme in domain: #{domain} (only http/https allowed)"
|
|
154
|
+
else
|
|
155
|
+
# No scheme - default to https
|
|
156
|
+
normalized_domain = "https://#{normalized_domain}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
parsed = URI.parse(normalized_domain)
|
|
161
|
+
rescue URI::InvalidURIError
|
|
162
|
+
raise ArgumentError, "invalid domain format: #{domain}"
|
|
163
|
+
end
|
|
164
|
+
raise ArgumentError, "invalid domain format: #{domain}" if parsed.host.nil? || parsed.host.empty?
|
|
165
|
+
|
|
166
|
+
scheme = parsed.scheme || 'https'
|
|
167
|
+
# Only omit port if it matches the default for the scheme
|
|
168
|
+
default_port = scheme == 'https' ? 443 : 80
|
|
169
|
+
host_with_port = parsed.port && parsed.port != default_port ? "#{parsed.host}:#{parsed.port}" : parsed.host
|
|
170
|
+
|
|
171
|
+
"#{scheme}://#{host_with_port}/logout#{query ? "?#{query}" : ''}"
|
|
129
172
|
end
|
|
130
173
|
|
|
131
174
|
def client_credentials_access(
|
|
@@ -154,13 +197,13 @@ module KindeSdk
|
|
|
154
197
|
begin
|
|
155
198
|
validate_jwt_token(hash)
|
|
156
199
|
OAuth2::AccessToken.from_hash(@config.oauth_client(
|
|
157
|
-
client_id: client_id,
|
|
200
|
+
client_id: client_id,
|
|
158
201
|
client_secret: client_secret,
|
|
159
202
|
domain: domain,
|
|
160
203
|
authorize_url: "#{domain}/oauth2/auth",
|
|
161
|
-
token_url: "#{domain}/oauth2/token"), hash).expired?
|
|
204
|
+
token_url: "#{domain}/oauth2/token"), TokenHash.normalize(hash)).expired?
|
|
162
205
|
rescue JWT::DecodeError, OAuth2::Error => e
|
|
163
|
-
|
|
206
|
+
log_error("Error checking token expiration: #{e.message}")
|
|
164
207
|
true
|
|
165
208
|
end
|
|
166
209
|
end
|
|
@@ -172,12 +215,14 @@ module KindeSdk
|
|
|
172
215
|
audience: "#{@config.domain}/api",
|
|
173
216
|
domain: @config.domain
|
|
174
217
|
)
|
|
175
|
-
|
|
176
|
-
client_id: client_id,
|
|
218
|
+
oauth_client = @config.oauth_client(
|
|
219
|
+
client_id: client_id,
|
|
177
220
|
client_secret: client_secret,
|
|
178
221
|
domain: domain,
|
|
179
222
|
authorize_url: "#{domain}/oauth2/auth",
|
|
180
|
-
token_url: "#{domain}/oauth2/token")
|
|
223
|
+
token_url: "#{domain}/oauth2/token")
|
|
224
|
+
refreshed = OAuth2::AccessToken.from_hash(oauth_client, TokenHash.normalize(hash)).refresh
|
|
225
|
+
TokenHash.for_refresh_response(refreshed.to_hash)
|
|
181
226
|
end
|
|
182
227
|
|
|
183
228
|
# init sdk api client by bearer token
|
|
@@ -205,7 +250,7 @@ module KindeSdk
|
|
|
205
250
|
begin
|
|
206
251
|
jwt_validation(token, "#{@config.domain}#{@config.jwks_url}", @config.expected_issuer, @config.expected_audience)
|
|
207
252
|
rescue JWT::DecodeError
|
|
208
|
-
|
|
253
|
+
log_error("Invalid JWT token: #{key}")
|
|
209
254
|
raise JWT::DecodeError, "Invalid #{key.to_s.capitalize.gsub('_', ' ')}"
|
|
210
255
|
end
|
|
211
256
|
end
|
|
@@ -253,12 +298,13 @@ module KindeSdk
|
|
|
253
298
|
payload, _header = JWT.decode(jwt_token, nil, true, algorithms: algorithms, jwks: jwks)
|
|
254
299
|
{ valid: true, payload: payload }
|
|
255
300
|
rescue JWT::DecodeError => e
|
|
256
|
-
|
|
301
|
+
log_error("Token validation failed: #{e.message}")
|
|
257
302
|
raise JWT::DecodeError, "Token validation failed: #{e.message}"
|
|
258
303
|
rescue StandardError => e
|
|
259
|
-
|
|
304
|
+
log_error("Unexpected error: #{e.message}")
|
|
260
305
|
raise StandardError, "Unexpected error: #{e.message}"
|
|
261
306
|
end
|
|
262
307
|
|
|
263
308
|
end
|
|
264
309
|
end
|
|
310
|
+
|
data/spec/examples.txt
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
example_id | status | run_time |
|
|
2
2
|
------------------------------------- | ------ | --------------- |
|
|
3
|
-
./spec/kinde_sdk_spec.rb[1:1:1] | passed | 0.
|
|
4
|
-
./spec/kinde_sdk_spec.rb[1:1:2] | passed | 0.
|
|
5
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
6
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
7
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
8
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
9
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
10
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
11
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
12
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
13
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
14
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
15
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
16
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
17
|
-
./spec/kinde_sdk_spec.rb[1:6:
|
|
18
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
19
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
20
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
21
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
22
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
23
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
24
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
25
|
-
./spec/kinde_sdk_spec.rb[1:7]
|
|
26
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
27
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
28
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
29
|
-
./spec/kinde_sdk_spec.rb[1:
|
|
3
|
+
./spec/kinde_sdk_spec.rb[1:1:1] | passed | 0.05436 seconds |
|
|
4
|
+
./spec/kinde_sdk_spec.rb[1:1:2] | passed | 0.05822 seconds |
|
|
5
|
+
./spec/kinde_sdk_spec.rb[1:1:3] | passed | 0.03791 seconds |
|
|
6
|
+
./spec/kinde_sdk_spec.rb[1:1:4] | passed | 0.02654 seconds |
|
|
7
|
+
./spec/kinde_sdk_spec.rb[1:1:5] | passed | 0.02896 seconds |
|
|
8
|
+
./spec/kinde_sdk_spec.rb[1:1:6] | passed | 0.07656 seconds |
|
|
9
|
+
./spec/kinde_sdk_spec.rb[1:1:7] | passed | 0.06324 seconds |
|
|
10
|
+
./spec/kinde_sdk_spec.rb[1:2:1] | passed | 0.03315 seconds |
|
|
11
|
+
./spec/kinde_sdk_spec.rb[1:2:2:1] | passed | 0.03904 seconds |
|
|
12
|
+
./spec/kinde_sdk_spec.rb[1:3:1] | passed | 0.02823 seconds |
|
|
13
|
+
./spec/kinde_sdk_spec.rb[1:4:1] | passed | 0.06163 seconds |
|
|
14
|
+
./spec/kinde_sdk_spec.rb[1:4:2:1] | passed | 0.17631 seconds |
|
|
15
|
+
./spec/kinde_sdk_spec.rb[1:5:1] | passed | 0.07435 seconds |
|
|
16
|
+
./spec/kinde_sdk_spec.rb[1:5:2:1] | passed | 0.05682 seconds |
|
|
17
|
+
./spec/kinde_sdk_spec.rb[1:6:1] | passed | 0.04508 seconds |
|
|
18
|
+
./spec/kinde_sdk_spec.rb[1:7:1:1] | passed | 0.04243 seconds |
|
|
19
|
+
./spec/kinde_sdk_spec.rb[1:7:1:2] | passed | 0.14146 seconds |
|
|
20
|
+
./spec/kinde_sdk_spec.rb[1:7:2:1] | passed | 0.05834 seconds |
|
|
21
|
+
./spec/kinde_sdk_spec.rb[1:7:2:2] | passed | 0.04263 seconds |
|
|
22
|
+
./spec/kinde_sdk_spec.rb[1:7:2:3] | passed | 0.04012 seconds |
|
|
23
|
+
./spec/kinde_sdk_spec.rb[1:7:3:1] | passed | 0.08297 seconds |
|
|
24
|
+
./spec/kinde_sdk_spec.rb[1:7:3:2:1:1] | passed | 0.11155 seconds |
|
|
25
|
+
./spec/kinde_sdk_spec.rb[1:7:3:2:2:1] | passed | 0.04904 seconds |
|
|
26
|
+
./spec/kinde_sdk_spec.rb[1:7:4:1] | passed | 0.06419 seconds |
|
|
27
|
+
./spec/kinde_sdk_spec.rb[1:7:4:2] | passed | 0.09575 seconds |
|
|
28
|
+
./spec/kinde_sdk_spec.rb[1:7:4:3] | passed | 0.04359 seconds |
|
|
29
|
+
./spec/kinde_sdk_spec.rb[1:7:4:4] | passed | 0.04235 seconds |
|
|
30
|
+
./spec/kinde_sdk_spec.rb[1:7:4:5] | passed | 0.11869 seconds |
|
|
31
|
+
./spec/kinde_sdk_spec.rb[1:7:4:6] | passed | 0.07644 seconds |
|
|
32
|
+
./spec/kinde_sdk_spec.rb[1:7:5:1:1] | passed | 0.06087 seconds |
|
|
33
|
+
./spec/kinde_sdk_spec.rb[1:8] | passed | 0.07786 seconds |
|
|
34
|
+
./spec/kinde_sdk_spec.rb[1:9:1] | passed | 0.05067 seconds |
|
|
35
|
+
./spec/kinde_sdk_spec.rb[1:9:2] | passed | 0.08124 seconds |
|
|
36
|
+
./spec/kinde_sdk_spec.rb[1:9:3] | passed | 0.0672 seconds |
|
|
37
|
+
./spec/kinde_sdk_spec.rb[1:9:4] | passed | 0.07944 seconds |
|
|
38
|
+
./spec/kinde_sdk_spec.rb[2:1:1] | passed | 0.00005 seconds |
|
|
39
|
+
./spec/kinde_sdk_spec.rb[2:1:2] | passed | 0.00005 seconds |
|
|
40
|
+
./spec/kinde_sdk_spec.rb[2:1:3] | passed | 0.00005 seconds |
|
|
41
|
+
./spec/kinde_sdk_spec.rb[2:1:4] | passed | 0.00008 seconds |
|
|
42
|
+
./spec/kinde_sdk_spec.rb[2:2:1] | passed | 0.00005 seconds |
|
|
43
|
+
./spec/kinde_sdk_spec.rb[2:3:1] | passed | 0.00465 seconds |
|
|
44
|
+
./spec/kinde_sdk_spec.rb[2:4:1] | passed | 0.00006 seconds |
|
|
45
|
+
./spec/kinde_sdk_spec.rb[2:5:1] | passed | 0.00059 seconds |
|
data/spec/kinde_sdk_spec.rb
CHANGED
|
@@ -88,6 +88,36 @@ RSpec.describe KindeSdk do
|
|
|
88
88
|
auth_obj = described_class.auth_url(redirect_uri: "localhost:5000/another_callback")
|
|
89
89
|
expect(auth_obj[:url]).to match(/localhost%3A5000%2Fanother_callback/)
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
it "includes invitation_code and is_invitation when invitation_code is provided" do
|
|
93
|
+
auth_obj = described_class.auth_url(invitation_code: "test_invitation_123")
|
|
94
|
+
expect(auth_obj[:url]).to include("invitation_code=test_invitation_123")
|
|
95
|
+
expect(auth_obj[:url]).to include("is_invitation=true")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "does not include is_invitation when invitation_code is not provided" do
|
|
99
|
+
auth_obj = described_class.auth_url
|
|
100
|
+
expect(auth_obj[:url]).not_to include("is_invitation")
|
|
101
|
+
expect(auth_obj[:url]).not_to include("invitation_code")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "does not include is_invitation for empty invitation_code" do
|
|
105
|
+
auth_obj = described_class.auth_url(invitation_code: "")
|
|
106
|
+
expect(auth_obj[:url]).not_to include("is_invitation")
|
|
107
|
+
expect(auth_obj[:url]).not_to include("invitation_code")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "does not include is_invitation for whitespace-only invitation_code" do
|
|
111
|
+
auth_obj = described_class.auth_url(invitation_code: " ")
|
|
112
|
+
expect(auth_obj[:url]).not_to include("is_invitation")
|
|
113
|
+
expect(auth_obj[:url]).not_to include("invitation_code")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "includes invitation_code and is_invitation for valid invitation_code with whitespace" do
|
|
117
|
+
auth_obj = described_class.auth_url(invitation_code: " abc123 ")
|
|
118
|
+
expect(auth_obj[:url]).to include("invitation_code=abc123")
|
|
119
|
+
expect(auth_obj[:url]).to include("is_invitation=true")
|
|
120
|
+
end
|
|
91
121
|
end
|
|
92
122
|
|
|
93
123
|
describe "#logout_url" do
|
|
@@ -190,6 +220,51 @@ RSpec.describe KindeSdk do
|
|
|
190
220
|
end
|
|
191
221
|
end
|
|
192
222
|
|
|
223
|
+
describe "#refresh_token" do
|
|
224
|
+
it "returns string-keyed tokens with the full oauth2 hash contract" do
|
|
225
|
+
refreshed = double(
|
|
226
|
+
"OAuth2::AccessToken",
|
|
227
|
+
token: token,
|
|
228
|
+
refresh_token: refresh_token,
|
|
229
|
+
expires_at: Time.now.to_i + 7200,
|
|
230
|
+
params: {}
|
|
231
|
+
)
|
|
232
|
+
allow(refreshed).to receive(:to_hash).and_return(
|
|
233
|
+
access_token: token,
|
|
234
|
+
refresh_token: refresh_token,
|
|
235
|
+
expires_at: Time.now.to_i + 7200,
|
|
236
|
+
mode: :header,
|
|
237
|
+
header_format: "Bearer %s"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
access_token_object = double(refresh: refreshed)
|
|
241
|
+
allow(OAuth2::AccessToken).to receive(:from_hash).and_return(access_token_object)
|
|
242
|
+
|
|
243
|
+
stored_tokens = {
|
|
244
|
+
"access_token" => token,
|
|
245
|
+
"refresh_token" => refresh_token,
|
|
246
|
+
"expires_at" => Time.now.to_i + 3600,
|
|
247
|
+
"mode" => :query,
|
|
248
|
+
"param_name" => "access_token"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expect(OAuth2::AccessToken).to receive(:from_hash) do |_client, normalized|
|
|
252
|
+
expect(normalized[:access_token]).to eq(token)
|
|
253
|
+
expect(normalized[:mode]).to eq(:query)
|
|
254
|
+
expect(normalized[:param_name]).to eq("access_token")
|
|
255
|
+
access_token_object
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
result = described_class.refresh_token(stored_tokens)
|
|
259
|
+
|
|
260
|
+
expect(result["access_token"]).to eq(token)
|
|
261
|
+
expect(result["refresh_token"]).to eq(refresh_token)
|
|
262
|
+
expect(result["mode"]).to eq(:header)
|
|
263
|
+
expect(result["header_format"]).to eq("Bearer %s")
|
|
264
|
+
expect(result[:access_token]).to be_nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
193
268
|
describe "client" do
|
|
194
269
|
let(:hash_to_encode) do
|
|
195
270
|
{ "aud" => [],
|
|
@@ -213,6 +288,24 @@ RSpec.describe KindeSdk do
|
|
|
213
288
|
let(:tokens_hash) { { access_token: token, expires_at: expires_at, refresh_token: refresh_token } }
|
|
214
289
|
let(:client) { described_class.client(tokens_hash, auto_refresh_tokens) }
|
|
215
290
|
|
|
291
|
+
context "with string-key session tokens" do
|
|
292
|
+
it "accepts string keys" do
|
|
293
|
+
session_tokens = {
|
|
294
|
+
"access_token" => token,
|
|
295
|
+
"refresh_token" => refresh_token,
|
|
296
|
+
"expires_at" => expires_at
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
expect(described_class.client(session_tokens, false).bearer_token).to eq(token)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it "raises when access_token is missing" do
|
|
303
|
+
expect {
|
|
304
|
+
described_class.client({ "refresh_token" => refresh_token }, false)
|
|
305
|
+
}.to raise_error(KindeSdk::AuthenticationError, /Missing access_token/)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
216
309
|
context "with session integration" do
|
|
217
310
|
before do
|
|
218
311
|
KindeSdk::Current.set_session(mock_session)
|
|
@@ -447,4 +540,136 @@ RSpec.describe KindeSdk do
|
|
|
447
540
|
end
|
|
448
541
|
end
|
|
449
542
|
|
|
543
|
+
RSpec.describe KindeSdk::TokenHash do
|
|
544
|
+
describe ".normalize" do
|
|
545
|
+
it "accepts string keys" do
|
|
546
|
+
hash = described_class.normalize(
|
|
547
|
+
"access_token" => "abc",
|
|
548
|
+
"refresh_token" => "def",
|
|
549
|
+
"expires_at" => 123
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
expect(hash).to eq(
|
|
553
|
+
access_token: "abc",
|
|
554
|
+
refresh_token: "def",
|
|
555
|
+
expires_at: 123
|
|
556
|
+
)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
it "accepts symbol keys" do
|
|
560
|
+
hash = described_class.normalize(
|
|
561
|
+
access_token: "abc",
|
|
562
|
+
refresh_token: "def",
|
|
563
|
+
expires_at: 123
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
expect(hash[:access_token]).to eq("abc")
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it "returns an empty hash for nil" do
|
|
570
|
+
expect(described_class.normalize(nil)).to eq({})
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
it "preserves oauth2 reconstruction fields" do
|
|
574
|
+
hash = described_class.normalize(
|
|
575
|
+
"access_token" => "abc",
|
|
576
|
+
"mode" => :header,
|
|
577
|
+
header_format: "Bearer %s",
|
|
578
|
+
"token_name" => :access_token,
|
|
579
|
+
"expires_latency" => 5,
|
|
580
|
+
"expires" => 3600
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
expect(hash).to eq(
|
|
584
|
+
access_token: "abc",
|
|
585
|
+
mode: :header,
|
|
586
|
+
header_format: "Bearer %s",
|
|
587
|
+
token_name: :access_token,
|
|
588
|
+
expires_latency: 5,
|
|
589
|
+
expires: 3600
|
|
590
|
+
)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe ".public_tokens" do
|
|
595
|
+
it "returns only the documented public SDK token fields" do
|
|
596
|
+
hash = described_class.public_tokens(
|
|
597
|
+
access_token: "abc",
|
|
598
|
+
refresh_token: "def",
|
|
599
|
+
expires_at: 123,
|
|
600
|
+
mode: :header,
|
|
601
|
+
header_format: "Bearer %s"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
expect(hash).to eq(
|
|
605
|
+
access_token: "abc",
|
|
606
|
+
refresh_token: "def",
|
|
607
|
+
expires_at: 123
|
|
608
|
+
)
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
describe ".from_access_token" do
|
|
613
|
+
let(:oauth_token) do
|
|
614
|
+
double(
|
|
615
|
+
"OAuth2::AccessToken",
|
|
616
|
+
token: "access-token-value",
|
|
617
|
+
refresh_token: "refresh-token-value",
|
|
618
|
+
expires_at: 1_700_000_000,
|
|
619
|
+
params: {
|
|
620
|
+
"id_token" => "id-token",
|
|
621
|
+
"scope" => "openid profile",
|
|
622
|
+
"token_type" => "bearer"
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
it "builds a normalized hash without calling oauth2 #to_hash" do
|
|
628
|
+
expect(oauth_token).not_to receive(:to_hash)
|
|
629
|
+
|
|
630
|
+
hash = described_class.from_access_token(oauth_token)
|
|
631
|
+
|
|
632
|
+
expect(hash).to eq(
|
|
633
|
+
access_token: "access-token-value",
|
|
634
|
+
refresh_token: "refresh-token-value",
|
|
635
|
+
expires_at: 1_700_000_000,
|
|
636
|
+
id_token: "id-token",
|
|
637
|
+
scope: "openid profile",
|
|
638
|
+
token_type: "bearer"
|
|
639
|
+
)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
describe ".for_refresh_response" do
|
|
644
|
+
it "returns string keys while preserving the full oauth2 hash contract" do
|
|
645
|
+
response = described_class.for_refresh_response(
|
|
646
|
+
access_token: "abc",
|
|
647
|
+
refresh_token: "def",
|
|
648
|
+
expires_at: 123,
|
|
649
|
+
mode: :header,
|
|
650
|
+
param_name: "access_token"
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
expect(response).to eq(
|
|
654
|
+
"access_token" => "abc",
|
|
655
|
+
"refresh_token" => "def",
|
|
656
|
+
"expires_at" => 123,
|
|
657
|
+
"mode" => :header,
|
|
658
|
+
"param_name" => "access_token"
|
|
659
|
+
)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
describe ".for_session" do
|
|
664
|
+
it "returns string keys for documented session/API compatibility" do
|
|
665
|
+
session_hash = described_class.for_session(access_token: "abc", expires_at: 123)
|
|
666
|
+
|
|
667
|
+
expect(session_hash).to eq(
|
|
668
|
+
"access_token" => "abc",
|
|
669
|
+
"expires_at" => 123
|
|
670
|
+
)
|
|
671
|
+
expect(session_hash["access_token"]).to eq("abc")
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
end
|
|
450
675
|
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kinde_sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kinde Australia Pty Ltd
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-16 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: typhoeus
|
|
@@ -105,34 +106,34 @@ dependencies:
|
|
|
105
106
|
requirements:
|
|
106
107
|
- - "~>"
|
|
107
108
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '
|
|
109
|
+
version: '3.0'
|
|
109
110
|
type: :runtime
|
|
110
111
|
prerelease: false
|
|
111
112
|
version_requirements: !ruby/object:Gem::Requirement
|
|
112
113
|
requirements:
|
|
113
114
|
- - "~>"
|
|
114
115
|
- !ruby/object:Gem::Version
|
|
115
|
-
version: '
|
|
116
|
+
version: '3.0'
|
|
116
117
|
- !ruby/object:Gem::Dependency
|
|
117
118
|
name: rspec
|
|
118
119
|
requirement: !ruby/object:Gem::Requirement
|
|
119
120
|
requirements:
|
|
120
|
-
- - "~>"
|
|
121
|
-
- !ruby/object:Gem::Version
|
|
122
|
-
version: '3.6'
|
|
123
121
|
- - ">="
|
|
124
122
|
- !ruby/object:Gem::Version
|
|
125
123
|
version: 3.6.0
|
|
124
|
+
- - "~>"
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '3.6'
|
|
126
127
|
type: :development
|
|
127
128
|
prerelease: false
|
|
128
129
|
version_requirements: !ruby/object:Gem::Requirement
|
|
129
130
|
requirements:
|
|
130
|
-
- - "~>"
|
|
131
|
-
- !ruby/object:Gem::Version
|
|
132
|
-
version: '3.6'
|
|
133
131
|
- - ">="
|
|
134
132
|
- !ruby/object:Gem::Version
|
|
135
133
|
version: 3.6.0
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '3.6'
|
|
136
137
|
description: Integrate the Kinde API into any ruby-based applications, Rails or non-Rails
|
|
137
138
|
email:
|
|
138
139
|
- support@kinde.com
|
|
@@ -935,7 +936,9 @@ files:
|
|
|
935
936
|
- lib/kinde_sdk/engine.rb
|
|
936
937
|
- lib/kinde_sdk/errors.rb
|
|
937
938
|
- lib/kinde_sdk/internal/frontend_client.rb
|
|
939
|
+
- lib/kinde_sdk/logging.rb
|
|
938
940
|
- lib/kinde_sdk/middleware.rb
|
|
941
|
+
- lib/kinde_sdk/token_hash.rb
|
|
939
942
|
- lib/kinde_sdk/token_manager.rb
|
|
940
943
|
- lib/kinde_sdk/token_store.rb
|
|
941
944
|
- lib/kinde_sdk/version.rb
|
|
@@ -946,6 +949,7 @@ homepage: https://kinde.com/
|
|
|
946
949
|
licenses:
|
|
947
950
|
- MIT
|
|
948
951
|
metadata: {}
|
|
952
|
+
post_install_message:
|
|
949
953
|
rdoc_options: []
|
|
950
954
|
require_paths:
|
|
951
955
|
- lib
|
|
@@ -961,7 +965,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
961
965
|
- !ruby/object:Gem::Version
|
|
962
966
|
version: '0'
|
|
963
967
|
requirements: []
|
|
964
|
-
rubygems_version: 3.
|
|
968
|
+
rubygems_version: 3.0.3.1
|
|
969
|
+
signing_key:
|
|
965
970
|
specification_version: 4
|
|
966
971
|
summary: Kinde Management API Ruby Gem
|
|
967
972
|
test_files:
|