kinetic_sdk 7.0.0.rc3 → 7.0.0.rc5

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: 7728373bd9c4a15c643c54aed99acd289e5f776ee0819c8b14f7aea9ec54c106
4
- data.tar.gz: d22c5e6907360c11cb886dfb2c58f5c80c5434fcd2b8d669f846b074a57d78ae
3
+ metadata.gz: 3fb9f7ab44be718c7df520bdb50bf97e0b33d184340d610608393ab9847834a0
4
+ data.tar.gz: d8331e8aeefdf808cc55ff2546df6e8edacde76835300d733b32f1cd72eb528b
5
5
  SHA512:
6
- metadata.gz: 0eaa0e7c58dead1010dbd1bd7964f42cf762a712ab7bc888037e6a8e6709b2af1b2562df834ed0d759cc1e677473ffacad035582b10542baaaf45d58401c4979
7
- data.tar.gz: f882b337544ec35443094eb971e821859d9480994ab7200519f090607672bc4e35ae284a04bb84aa2ffdc7fb15f1e6755c9712905c9188c518bb3751a06fdde3
6
+ metadata.gz: ef422ca2c44ff3b50d13de5a01aeb2c72bd3f1fd76797979da7e30668183591c8919d7d3d3a958db6dfe77010414bcf182ec72fbc108db9c3e875fde249fae2b
7
+ data.tar.gz: 93edce1847353966e76ef873ab96969dafcadf47ea923f13a492a8def392c6933700d0a44b22d1e790fa956d36ded1abe77bd3c8e4d9d3aa6e4b64f663f95f13
data/CHANGELOG.md CHANGED
@@ -1,6 +1,65 @@
1
1
  # Change Log
2
2
 
3
3
 
4
+ ## [7.0.0.rc5](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc5) (2026-09-01)
5
+
6
+ **Fixed bugs:**
7
+
8
+ - Fixed `401 invalid_client` when retrieving a JWT with a client secret containing `+`, `%`
9
+ or a space, despite correct credentials. The token request now authenticates with
10
+ `client_secret_post`, sending `client_id` and `client_secret` as form parameters.
11
+
12
+ `client_secret_basic` requires both values to be form-urlencoded before base64 encoding
13
+ (RFC 6749 section 2.3.1), and Spring Authorization Server's
14
+ `ClientSecretBasicAuthenticationConverter` correspondingly URL-decodes them. The SDK sent
15
+ them raw, so a `+` in a secret was decoded to a space and the bcrypt comparison failed.
16
+ Kinetic Coordinator generates integration user passwords from a character set containing
17
+ `+`, which is why the failure appeared intermittently across tenants.
18
+
19
+ `header_basic_auth` is deliberately unchanged. It is shared by every ordinary HTTP Basic
20
+ user authentication call site, and Core authenticates those with Spring Security's
21
+ `BasicAuthenticationConverter`, which does not URL-decode. Adding encoding there would
22
+ break any user whose password contains `+` or `%`.
23
+
24
+ - The token request no longer sends an Authorization header, and strips any header inherited
25
+ from the caller, so it cannot compete with the form credentials. This also keeps the client
26
+ secret out of the SDK's debug log, which prints request headers.
27
+
28
+
29
+ ## [7.0.0.rc4](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc4) (2026-09-01)
30
+
31
+ **Breaking changes:**
32
+
33
+ - `jwt_token` now uses the OAuth 2.0 `client_credentials` grant, a single POST to
34
+ `/{space_slug}/app/oauth2/token`. Kinetic Platform 7 runs a standards compliant
35
+ authorization server in which `/app/oauth2/authorize` is an interactive endpoint
36
+ requiring a browser session, consent, a registered redirect_uri and PKCE, so the
37
+ previous authorization-code flow returned `400`. The response shape is unchanged:
38
+ callers reading `jwt_response.content["access_token"]` are unaffected.
39
+ - `jwt_code` has been removed. It drove the authorization-code flow that no longer
40
+ applies to machine to machine clients.
41
+ - The OAuth client used by the SDK must be registered in the space as a **confidential**
42
+ client (or with no `clientType`, which defaults to confidential). Public clients and
43
+ the built in system client register only the `authorization_code` grant and are
44
+ rejected by the token endpoint.
45
+
46
+ **Fixed bugs:**
47
+
48
+ - The OAuth routes are now space scoped (`/{space_slug}/app/oauth2/...`) when the SDK is
49
+ built from an `:app_server_url` plus a `:space_slug`. The authorization server resolves
50
+ the space from the request path and rejects requests made outside a space context.
51
+ - OAuth failures now report the `error` and `error_description` from a JSON error, or the
52
+ `X-Kinetic-CID` correlation id when Core answers with its generic HTML error page,
53
+ instead of dumping the inspected response object.
54
+
55
+ **Implemented enhancements:**
56
+
57
+ - Added the `:oauth_scope` option, defaulting to `full`. Valid scopes are `full`, `read`,
58
+ `write`, `admin`, `submissions` and `submissions:read`.
59
+ - Added specs covering the token exchange, scope configuration, and the failure modes for
60
+ both the Integrator and Discussions SDKs.
61
+
62
+
4
63
  ## [7.0.0.rc3](https://github.com/kineticdata/kinetic-sdk-rb/tree/7.0.0.rc3) (2026-09-01)
5
64
 
6
65
  **Fixed bugs:**
@@ -14,7 +14,8 @@ module KineticSdk
14
14
  include KineticSdk::Utils::KineticExportUtils
15
15
 
16
16
  attr_reader :api_url, :username, :options, :password, :proxy_url,
17
- :space_slug, :server, :version, :logger
17
+ :space_slug, :server, :version, :logger, :oauth_url,
18
+ :oauth_scope
18
19
 
19
20
  # Initalize the Core SDK with the web server URL, the space user
20
21
  # username and password, along with any custom option values.
@@ -55,6 +56,8 @@ module KineticSdk
55
56
  # * :max_redirects (Fixnum) (_defaults to: 5_) maximum number of redirects to follow
56
57
  # * :ssl_ca_file (String) full path to PEM certificate used to verify the server
57
58
  # * :ssl_verify_mode (String) (_defaults to: none_) - none | peer
59
+ # * :oauth_scope (String) (_defaults to: full_) scope requested when retrieving a
60
+ # JWT - full | read | write | admin | submissions | submissions:read
58
61
  #
59
62
  # Example: using a configuration file
60
63
  #
@@ -154,12 +157,17 @@ module KineticSdk
154
157
  @server = options[:app_server_url].chomp('/')
155
158
  @api_url = @server + (@space_slug.nil? ? "/app/api/v1" : "/#{@space_slug}/app/api/v1")
156
159
  @proxy_url = @space_slug.nil? ? nil : "#{@server}/#{@space_slug}/app/components"
160
+ # The authorization server resolves the space from the request path, so the
161
+ # OAuth routes must be space scoped exactly like the API routes.
162
+ @oauth_url = @server + (@space_slug.nil? ? "/app/oauth2" : "/#{@space_slug}/app/oauth2")
157
163
  else
158
164
  raise StandardError.new "The :space_slug option is required when using the :space_server_url option" if @space_slug.nil?
159
165
  @server = options[:space_server_url].chomp('/')
160
166
  @api_url = "#{@server}/app/api/v1"
161
167
  @proxy_url = "#{@server}/app/components"
168
+ @oauth_url = "#{@server}/app/oauth2"
162
169
  end
170
+ @oauth_scope = @options[:oauth_scope] || @options["oauth_scope"] || "full"
163
171
  @version = 1
164
172
  end
165
173
 
@@ -1,55 +1,94 @@
1
1
  module KineticSdk
2
2
  class Core
3
3
 
4
- # Gets an authentication token
4
+ # Gets an authentication token using the OAuth 2.0 client credentials grant.
5
+ #
6
+ # Kinetic Platform 7 runs a standards compliant authorization server, in which
7
+ # `/app/oauth2/authorize` is an interactive endpoint: it expects an authenticated
8
+ # browser session, a consent round trip, a registered redirect_uri, and PKCE. A
9
+ # machine to machine client cannot drive it, and should not try - it exchanges its
10
+ # own credentials for a token directly at the token endpoint instead.
11
+ #
12
+ # The OAuth client must be registered in the space as a confidential client (or with
13
+ # no clientType, which defaults to confidential) so that it carries the
14
+ # `client_credentials` grant. Public clients and the built in system client support
15
+ # only `authorization_code`, and will be rejected here.
16
+ #
17
+ # The client authenticates with `client_secret_post`, sending its credentials as form
18
+ # parameters rather than in an Authorization header. `client_secret_basic` requires the
19
+ # credentials to be form-urlencoded before base64 encoding (RFC 6749 section 2.3.1), which
20
+ # the shared `header_basic_auth` helper does not do - and must not start doing, because
21
+ # ordinary HTTP Basic user authentication takes credentials raw. A secret containing "+"
22
+ # would otherwise reach the server with that "+" decoded to a space.
5
23
  #
6
24
  # @param client_id [String] the oauth client id
7
25
  # @param client_secret [String] the oauth client secret
8
- # @param headers [Hash] hash of headers to send, default is basic authentication and accept JSON content type
26
+ # @param headers [Hash] additional headers to send. The Accept and Content-Type headers
27
+ # required by the token endpoint always take precedence, and any Authorization header
28
+ # is removed so it cannot compete with the form credentials.
29
+ # @param scope [String] scope to request, defaults to the +:oauth_scope+ option (+full+)
9
30
  # @return [KineticSdk::Utils::KineticHttpResponse] object, with +code+, +message+, +content_string+, and +content+ properties
10
- def jwt_token(client_id, client_secret, headers = default_headers)
11
- # retrieve the jwt code
12
- jwt_code = jwt_code(client_id, headers)
13
- # retrieve the jwt token
31
+ def jwt_token(client_id, client_secret, headers = {}, scope = oauth_scope)
14
32
  @logger.info("Retrieving JWT authorization token")
15
- url = "#{@server}/app/oauth2/token?grant_type=authorization_code&response_type=token&client_id=#{client_id}&code=#{jwt_code}"
16
- token_headers = header_accept_json.merge(header_basic_auth(client_id, client_secret))
17
- response = post(url, {}, token_headers, { :max_redirects => 0 })
33
+ url = "#{@oauth_url}/token"
34
+
35
+ # The required headers are merged last so a caller cannot override them. Any inherited
36
+ # Authorization header (the SDK user's basic auth, for instance) is dropped: the
37
+ # authorization server would read it as a competing client authentication.
38
+ token_headers = headers
39
+ .merge(header_accept_json)
40
+ .merge({ "Content-Type" => "application/x-www-form-urlencoded" })
41
+ token_headers.delete_if { |key, _value| key.to_s.casecmp("authorization").zero? }
42
+
43
+ # URI.encode_www_form applies the same application/x-www-form-urlencoded rules the
44
+ # server decodes with, so "+", "%", ":" and spaces in a secret survive the round trip.
45
+ payload = {
46
+ "grant_type" => "client_credentials",
47
+ "client_id" => client_id,
48
+ "client_secret" => client_secret,
49
+ }
50
+ payload["scope"] = scope unless scope.nil? || scope.to_s.empty?
18
51
 
19
- if response.status == 401
20
- raise StandardError.new "#{response.message}, the oauth client id and secret are invalid."
21
- elsif response.status == 200
52
+ # Redirects are not followed: the token endpoint has no reason to redirect, and
53
+ # following one would forward the client credentials to another host.
54
+ response = post(url, URI.encode_www_form(payload), token_headers, { :max_redirects => 0 })
55
+
56
+ case response.status
57
+ when 200
22
58
  response
59
+ when 401
60
+ raise StandardError.new(
61
+ "Unable to retrieve token: #{oauth_error_message(response)}. The oauth client id " \
62
+ "and secret are invalid, or no such client is registered in this space."
63
+ )
23
64
  else
24
- raise StandardError.new "Unable to retrieve token: #{response}"
65
+ raise StandardError.new("Unable to retrieve token: #{oauth_error_message(response)}")
25
66
  end
26
67
  end
27
68
 
28
- # Gets an authentication code.
69
+ private
70
+
71
+ # Builds an actionable message from an OAuth error response.
29
72
  #
30
- # This method should really never need to be called externally.
73
+ # The authorization server answers most failures with a JSON OAuth error, but falls
74
+ # back to Core's generic HTML error page for others. That page carries no error code,
75
+ # only a correlation id in the X-Kinetic-CID response header, which is what is needed
76
+ # to find the matching entry in the server log.
31
77
  #
32
- # @param client_id [String]
33
- # @param headers [Hash] hash of headers to send, default is basic authentication and accept JSON content type
34
- # @return [KineticSdk::Utils::KineticHttpResponse] object, with +code+, +message+, +content_string+, and +content+ properties
35
- def jwt_code(client_id, headers = default_headers)
36
- @logger.info("Retrieving JWT authorization code")
37
- url = "#{@server}/app/oauth2/authorize?grant_type=authorization_code&response_type=code&client_id=#{client_id}"
38
- response = post(url, {}, headers, { :max_redirects => -1 })
39
-
40
- if response.status == 401
41
- raise StandardError.new "#{response.message}: #{response.content["error"]}"
42
- elsif response.status == 302 || response.status == 303
43
- location = response.headers["location"]
44
- if location.nil?
45
- raise StandardError.new "Unable to retrieve code: #{response.inspect}"
46
- elsif !location.include?("?code=")
47
- raise StandardError.new "Unable to retrieve code, the authorize endpoint redirected to #{location} without an authorization code."
48
- else
49
- location.split("?code=").last.split("#/").first
50
- end
78
+ # @param response [KineticSdk::Utils::KineticHttpResponse] the failed response
79
+ # @return [String] a single line description of the failure
80
+ def oauth_error_message(response)
81
+ content = response.content.is_a?(Hash) ? response.content : {}
82
+ if content["error"]
83
+ message = "#{response.status} #{content["error"]}"
84
+ message += " - #{content["error_description"]}" if content["error_description"]
85
+ message
51
86
  else
52
- raise StandardError.new "Unable to retrieve code #{response.inspect}"
87
+ message = "#{response.status} #{response.message}"
88
+ headers = response.headers
89
+ correlation_id = headers.is_a?(Hash) ? headers["x-kinetic-cid"] : nil
90
+ message += " (correlation id #{correlation_id})" if correlation_id
91
+ message
53
92
  end
54
93
  end
55
94
  end
@@ -3,5 +3,5 @@ module KineticSdk
3
3
  # Version of Kinetic SDK
4
4
  #
5
5
  # @return [String] Version of the SDK
6
- VERSION = "7.0.0.rc3"
6
+ VERSION = "7.0.0.rc5"
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kinetic_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0.rc3
4
+ version: 7.0.0.rc5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinetic Data
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-01 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slugify