kinde_sdk 1.4.0 → 1.5.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/lib/kinde_sdk/configuration.rb +8 -0
- data/lib/kinde_sdk/controllers/auth_controller.rb +2 -2
- data/lib/kinde_sdk/version.rb +1 -1
- data/lib/kinde_sdk.rb +67 -0
- data/spec/kinde_sdk_spec.rb +44 -5
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9750676886f9fbf88e29b5efd42b0e04982bafa5f2575005211b81ab5c35dcd
|
4
|
+
data.tar.gz: 06e41ce5145e22c146a1c3c77eb2da4fc9759c416c7d3bf0806a5132d0412a83
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85f536327bc49538a3645452b6c7570b6489ce3cc4d169c080d63455bbb320d779738f7ea8798e10829425485b62f94da9c88f0b25b64d382c4866deda8454c6
|
7
|
+
data.tar.gz: a699df4b5e9051fa43fa44ba85f97804966d4544a551add332237fb5b27734a6aa0584f9d0148d0ecc70351d4dee585ef7d017dbe5023059143e067563f161e7
|
@@ -10,6 +10,11 @@ module KindeSdk
|
|
10
10
|
attr_accessor :authorize_url
|
11
11
|
attr_accessor :token_url
|
12
12
|
|
13
|
+
attr_accessor :jwks_url
|
14
|
+
attr_accessor :jwks
|
15
|
+
attr_accessor :expected_issuer
|
16
|
+
attr_accessor :expected_audience
|
17
|
+
|
13
18
|
attr_accessor :logger
|
14
19
|
attr_accessor :debugging
|
15
20
|
attr_accessor :oauth_client
|
@@ -19,6 +24,9 @@ module KindeSdk
|
|
19
24
|
def initialize
|
20
25
|
@authorize_url = '/oauth2/auth'
|
21
26
|
@token_url = '/oauth2/token'
|
27
|
+
@jwks_url = '/.well-known/jwks.json'
|
28
|
+
@expected_audience = nil
|
29
|
+
@expected_issuer = nil
|
22
30
|
@debugging = false
|
23
31
|
@logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
24
32
|
@scope = 'openid offline email profile'
|
@@ -24,7 +24,7 @@ module KindeSdk
|
|
24
24
|
requested_at: Time.current.to_i,
|
25
25
|
redirect_url: auth_data[:url]
|
26
26
|
}
|
27
|
-
|
27
|
+
|
28
28
|
redirect_to auth_data[:url], allow_other_host: true
|
29
29
|
end
|
30
30
|
|
@@ -90,7 +90,7 @@ module KindeSdk
|
|
90
90
|
def validate_state
|
91
91
|
# Check if nonce and state exist in session
|
92
92
|
unless session[:auth_nonce] && session[:auth_state]
|
93
|
-
Rails.logger.warn("Missing session state or nonce")
|
93
|
+
Rails.logger.warn("Missing session state or nonce [#{session[:auth_nonce]}] [#{session[:auth_state]}]")
|
94
94
|
redirect_to "/", alert: "Invalid authentication state"
|
95
95
|
return
|
96
96
|
end
|
data/lib/kinde_sdk/version.rb
CHANGED
data/lib/kinde_sdk.rb
CHANGED
@@ -10,6 +10,10 @@ require 'oauth2'
|
|
10
10
|
require 'pkce_challenge'
|
11
11
|
require 'faraday/follow_redirects'
|
12
12
|
require 'uri'
|
13
|
+
require 'httparty'
|
14
|
+
require 'jwt'
|
15
|
+
require 'openssl'
|
16
|
+
require 'base64'
|
13
17
|
|
14
18
|
module KindeSdk
|
15
19
|
class << self
|
@@ -102,6 +106,8 @@ module KindeSdk
|
|
102
106
|
#
|
103
107
|
# @return [KindeSdk::Client]
|
104
108
|
def client(tokens_hash)
|
109
|
+
validate_jwt_token(tokens_hash)
|
110
|
+
|
105
111
|
sdk_api_client = api_client(tokens_hash[:access_token] || tokens_hash["access_token"])
|
106
112
|
KindeSdk::Client.new(sdk_api_client, tokens_hash, @config.auto_refresh_tokens)
|
107
113
|
end
|
@@ -135,6 +141,8 @@ module KindeSdk
|
|
135
141
|
audience: "#{@config.domain}/api",
|
136
142
|
domain: @config.domain
|
137
143
|
)
|
144
|
+
validate_jwt_token(hash)
|
145
|
+
|
138
146
|
OAuth2::AccessToken.from_hash(@config.oauth_client(
|
139
147
|
client_id: client_id,
|
140
148
|
client_secret: client_secret,
|
@@ -150,6 +158,8 @@ module KindeSdk
|
|
150
158
|
audience: "#{@config.domain}/api",
|
151
159
|
domain: @config.domain
|
152
160
|
)
|
161
|
+
validate_jwt_token(hash)
|
162
|
+
|
153
163
|
OAuth2::AccessToken.from_hash(@config.oauth_client(
|
154
164
|
client_id: client_id,
|
155
165
|
client_secret: client_secret,
|
@@ -182,5 +192,62 @@ module KindeSdk
|
|
182
192
|
rescue URI::InvalidURIError
|
183
193
|
default_scheme
|
184
194
|
end
|
195
|
+
|
196
|
+
|
197
|
+
def validate_jwt_token(token_hash)
|
198
|
+
token_hash.each do |key, token|
|
199
|
+
next unless %w[access_token id_token].include?(key.to_s.downcase)
|
200
|
+
begin
|
201
|
+
jwt_validation(token, "#{@config.domain}#{@config.jwks_url}", @config.expected_issuer, @config.expected_audience)
|
202
|
+
rescue JWT::DecodeError
|
203
|
+
Rails.logger.error("Invalid JWT token: #{key}")
|
204
|
+
raise JWT::DecodeError, "Invalid #{key.to_s.capitalize.gsub('_', ' ')}"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
# Method to validate a JWT token with caching for JWKS
|
211
|
+
def jwt_validation(jwt_token, jwks_url, expected_issuer, expected_audience)
|
212
|
+
@cached_jwks ||= fetch_jwks(jwks_url)
|
213
|
+
|
214
|
+
begin
|
215
|
+
validate_token(jwt_token, @cached_jwks, expected_issuer, expected_audience)
|
216
|
+
rescue JWT::DecodeError, StandardError
|
217
|
+
# If validation fails, fetch JWKS again and retry validation
|
218
|
+
@cached_jwks = fetch_jwks(jwks_url)
|
219
|
+
validate_token(jwt_token, @cached_jwks, expected_issuer, expected_audience)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
# Fetch JWKS from the URL
|
226
|
+
def fetch_jwks(jwks_url)
|
227
|
+
jwks_response = HTTParty.get(jwks_url)
|
228
|
+
JSON.parse(jwks_response.body)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Validate the JWT token using the provided JWKS
|
232
|
+
def validate_token(jwt_token, jwks_hash, expected_issuer, expected_audience)
|
233
|
+
# Decode token header to get 'kid'
|
234
|
+
decoded_token = JWT.decode(jwt_token, nil, false) # [payload, header]
|
235
|
+
header = decoded_token[1]
|
236
|
+
kid = header['kid']
|
237
|
+
|
238
|
+
# Find the matching JWK
|
239
|
+
jwks = JWT::JWK::Set.new(jwks_hash)
|
240
|
+
jwks.filter! {|key| key[:use] == 'sig' }
|
241
|
+
algorithms = jwks.map { |key| key[:alg] }.compact.uniq
|
242
|
+
payload, _header = JWT.decode(jwt_token, nil, true, algorithms: algorithms, jwks: jwks)
|
243
|
+
{ valid: true, payload: payload }
|
244
|
+
rescue JWT::DecodeError => e
|
245
|
+
Rails.logger.error("Token validation failed: #{e.message}")
|
246
|
+
raise JWT::DecodeError, "Token validation failed: #{e.message}"
|
247
|
+
rescue StandardError => e
|
248
|
+
Rails.logger.error("Unexpected error: #{e.message}")
|
249
|
+
raise StandardError, "Unexpected error: #{e.message}"
|
250
|
+
end
|
251
|
+
|
185
252
|
end
|
186
253
|
end
|
data/spec/kinde_sdk_spec.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'jwt'
|
3
|
+
require 'openssl'
|
4
|
+
require 'webmock/rspec'
|
5
|
+
|
2
6
|
|
3
7
|
describe KindeSdk do
|
4
8
|
let(:domain) { "http://example.com" }
|
@@ -8,6 +12,13 @@ describe KindeSdk do
|
|
8
12
|
let(:logout_url) { "http://localhost/logout-callback" }
|
9
13
|
let(:auto_refresh_tokens) { true }
|
10
14
|
|
15
|
+
let(:optional_parameters) { { kid: 'my-kid', use: 'sig', alg: 'RS512' } }
|
16
|
+
let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) }
|
17
|
+
let(:jwk) { JWT::JWK.new(rsa_key, optional_parameters) }
|
18
|
+
let(:payload) { { data: 'data' } }
|
19
|
+
let(:token) { JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) }
|
20
|
+
let(:jwks_hash) { JWT::JWK::Set.new(jwk).export }
|
21
|
+
|
11
22
|
before do
|
12
23
|
KindeSdk.configure do |c|
|
13
24
|
c.domain = domain
|
@@ -74,7 +85,19 @@ describe KindeSdk do
|
|
74
85
|
)
|
75
86
|
.to_return(
|
76
87
|
status: 200,
|
77
|
-
body: { "access_token"
|
88
|
+
body: { "access_token" => "eyJ", "id_token" => "test", "refresh_token" => "test", "expires_in" => 86399, "scope" => "", "token_type" => "bearer" }.to_json,
|
89
|
+
headers: { "content-type" => "application/json;charset=UTF-8" }
|
90
|
+
)
|
91
|
+
stub_request(:get, "#{domain}/.well-known/jwks.json")
|
92
|
+
.with(
|
93
|
+
headers: {
|
94
|
+
'Accept'=>'*/*',
|
95
|
+
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
96
|
+
'User-Agent'=>'Ruby'
|
97
|
+
})
|
98
|
+
.to_return(
|
99
|
+
status: 200,
|
100
|
+
body: jwks_hash.to_json,
|
78
101
|
headers: { "content-type" => "application/json;charset=UTF-8" }
|
79
102
|
)
|
80
103
|
end
|
@@ -123,13 +146,13 @@ describe KindeSdk do
|
|
123
146
|
let(:hash_to_encode) do
|
124
147
|
{ "aud" => [],
|
125
148
|
"azp" => "19ebb687cd2f405c9f2daf645a8db895",
|
126
|
-
"exp" => 1679600554,
|
127
149
|
"feature_flags" => {
|
128
150
|
"asd" => { "t" => "b", "v" => true },
|
129
151
|
"eeeeee" => { "t" => "i", "v" => 111 },
|
130
152
|
"qqq" => { "t" => "s", "v" => "aa" }
|
131
153
|
},
|
132
|
-
"iat"
|
154
|
+
"iat": Time.now.to_i, # Issued at: current time
|
155
|
+
"exp": Time.now.to_i + 3600, # Expiration time: 1 hour from now
|
133
156
|
"iss" => "https://example.kinde.com",
|
134
157
|
"jti" => "22c48b2c-da46-4661-a7ff-425c23eceab5",
|
135
158
|
"org_code" => "org_cb4544175bc",
|
@@ -137,9 +160,23 @@ describe KindeSdk do
|
|
137
160
|
"scp" => ["openid", "offline"],
|
138
161
|
"sub" => "kp:b17adf719f7d4b87b611d1a88a09fd15" }
|
139
162
|
end
|
140
|
-
|
163
|
+
before do
|
164
|
+
stub_request(:get, "#{domain}/.well-known/jwks.json")
|
165
|
+
.with(
|
166
|
+
headers: {
|
167
|
+
'Accept'=>'*/*',
|
168
|
+
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
169
|
+
'User-Agent'=>'Ruby'
|
170
|
+
})
|
171
|
+
.to_return(
|
172
|
+
status: 200,
|
173
|
+
body: jwks_hash.to_json,
|
174
|
+
headers: { "content-type" => "application/json;charset=UTF-8" }
|
175
|
+
)
|
176
|
+
end
|
177
|
+
let(:token) { JWT.encode(hash_to_encode, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) }
|
141
178
|
let(:expires_at) { Time.now.to_i + 10000000 }
|
142
|
-
let(:client) { described_class.client({
|
179
|
+
let(:client) { described_class.client({ access_token: token, expires_at: expires_at }) }
|
143
180
|
|
144
181
|
context "with feature flags" do
|
145
182
|
it "returns existing flags", :aggregate_failures do
|
@@ -229,3 +266,5 @@ describe KindeSdk do
|
|
229
266
|
end
|
230
267
|
end
|
231
268
|
end
|
269
|
+
|
270
|
+
|
metadata
CHANGED
@@ -1,14 +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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kinde Australia Pty Ltd
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: typhoeus
|
@@ -44,6 +44,34 @@ dependencies:
|
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '2.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: httparty
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.19.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.19.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: jwt
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.2'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '2.2'
|
47
75
|
- !ruby/object:Gem::Dependency
|
48
76
|
name: pkce_challenge
|
49
77
|
requirement: !ruby/object:Gem::Requirement
|