kinde_sdk 1.3.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc5ca096d7760fdbb44aad50f80581bedad34c59961401731a46fdcfe12de6e8
4
- data.tar.gz: 7c172304b38b70bb4446e25db64cd3ce98c28aadded415d32a776b82885095d5
3
+ metadata.gz: a9750676886f9fbf88e29b5efd42b0e04982bafa5f2575005211b81ab5c35dcd
4
+ data.tar.gz: 06e41ce5145e22c146a1c3c77eb2da4fc9759c416c7d3bf0806a5132d0412a83
5
5
  SHA512:
6
- metadata.gz: 963aabbb2e5d9ea92c3369d127f21ea3181aa646bbf087a1e4322c7d4721f0c267a8ba0f5f55d5a1c389bdca7d75844193fa039969f6760fd0c59419e6e8897b
7
- data.tar.gz: 520cd6180f22cf93041b0de2cd89898d6640c275dd4f9d8e19efa4514969b18dc4a440265ab3a1b35aa81ab0707adad6eeb45c11d7bc8ab68eb978c7bb969616
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
@@ -1,3 +1,3 @@
1
1
  module KindeSdk
2
- VERSION = "1.3.1"
2
+ VERSION = "1.5.0"
3
3
  end
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
@@ -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": "eyJ", "id_token": "test", "refresh_token": "test","expires_in": 86399, "scope": "", "token_type": "bearer" }.to_json,
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" => 1679514154,
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
- let(:token) { JWT.encode(hash_to_encode, nil, "none") }
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({ "access_token": token, "expires_at": expires_at }) }
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.3.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-20 00:00:00.000000000 Z
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