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 +4 -4
- data/CHANGELOG.md +59 -0
- data/lib/kinetic_sdk/core/core-sdk.rb +9 -1
- data/lib/kinetic_sdk/core/lib/jwt.rb +74 -35
- data/lib/kinetic_sdk/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fb9f7ab44be718c7df520bdb50bf97e0b33d184340d610608393ab9847834a0
|
|
4
|
+
data.tar.gz: d8331e8aeefdf808cc55ff2546df6e8edacde76835300d733b32f1cd72eb528b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
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 =
|
|
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 = "#{@
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
65
|
+
raise StandardError.new("Unable to retrieve token: #{oauth_error_message(response)}")
|
|
25
66
|
end
|
|
26
67
|
end
|
|
27
68
|
|
|
28
|
-
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Builds an actionable message from an OAuth error response.
|
|
29
72
|
#
|
|
30
|
-
#
|
|
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
|
|
33
|
-
# @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
data/lib/kinetic_sdk/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: slugify
|