keycloak-sdk 1.0.1 → 1.2.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/README.md +2 -2
- data/lib/keycloak_sdk/admin/call.rb +2 -1
- data/lib/keycloak_sdk/auth_client.rb +41 -27
- data/lib/keycloak_sdk/errors.rb +39 -0
- data/lib/keycloak_sdk/jwks_store.rb +2 -2
- data/lib/keycloak_sdk/jwt_validator.rb +12 -5
- data/lib/keycloak_sdk/token_provider.rb +1 -1
- data/lib/keycloak_sdk/tokens.rb +12 -1
- data/lib/keycloak_sdk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73ecdc82d8bb713a7529ce242c2eacfa8279d9e413ce09aa2e2e468e4fd269c3
|
|
4
|
+
data.tar.gz: c6670b1e3767730f49604d2215a4650d3630e34b5cac979ecc24d63517c423e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 165d9c457ceb36f8b38e0cf622c15175af2ce8643e6ccfdba818143b4ca85a09d7967e0c51422108ce5b906ae6f805e28f0bc44380695f0a530e96cc9b0e01b6
|
|
7
|
+
data.tar.gz: fcbce56000bd6f42ff3c376b0c8913487792eac3803e7fbbc2a718d58bf1fec6826dc3f99e4d59b61f7cc950116926629766cb408b990347479df87bd2e64f6e
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Authentication (OIDC / OAuth2) and the Admin REST API for [Keycloak](https://www
|
|
|
4
4
|
|
|
5
5
|
Part of a **nine-language polyglot SDK** (Java · Python · Node · Go · C# · PHP · Rust · Ruby · Kotlin) — one API surface, isomorphic across all of them: [github.com/xzawed/KeyCloakSDK](https://github.com/xzawed/KeyCloakSDK).
|
|
6
6
|
|
|
7
|
-
> **`1.0
|
|
7
|
+
> **`1.2.0` is on RubyGems** — a minor release on top of `1.1.0`; `1.0.0` was the first release carrying the stability guarantee. A bare `gem install keycloak-sdk` resolves it. **Fixed**: with `expected_audience` set to something other than the client_id (a resource server), `exchange_code` with an `expected_nonce:` rejected the server's valid id_token as `invalid id_token` (`Invalid audience`), because the id_token was checked against the same override as access tokens. The id_token's `aud` is now checked for the **client_id** (OIDC Core §2, §3.1.3.7), so that exchange passes, and an id_token without the client_id in `aud` is refused even when it carries the overridden value. `validate` still applies the override to access tokens, and both share one JWKS cache and refetch limit. **Added**: an `audience:` keyword on `JwtValidator#validate(token, audience:)` — omit it and nothing changes; `nil` or a blank value raises `ConfigError`.
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
@@ -55,7 +55,7 @@ client.admin.users.delete(user_id)
|
|
|
55
55
|
client.close
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
> **Audience:** validation requires the token's `aud` to contain `client_id`. A stock realm does *not* put the client id in a client-credentials token's `aud`, so on a default realm either pass `expected_audience: "my-api"` (the audience your realm actually issues), or add an *Audience* protocol mapper to the client in Keycloak.
|
|
58
|
+
> **Audience:** validation requires the token's `aud` to contain `client_id`. A stock realm does *not* put the client id in a client-credentials token's `aud`, so on a default realm either pass `expected_audience: "my-api"` (the audience your realm actually issues), or add an *Audience* protocol mapper to the client in Keycloak. `expected_audience` applies to access tokens (`validate`) only: the `id_token` that `exchange_code(expected_nonce:)` validates must always carry `client_id` in `aud` (OIDC Core §3.1.3.7).
|
|
59
59
|
|
|
60
60
|
The five admin resources — `users` / `clients` / `roles` / `groups` / `realms` — offer symmetric CRUD, and `client.admin.raw` is the escape hatch to the underlying bearer-authenticated `Faraday::Connection`.
|
|
61
61
|
|
|
@@ -32,7 +32,8 @@ module KeycloakSdk
|
|
|
32
32
|
|
|
33
33
|
raise AdminError.from_status(resp.status, "admin request failed: HTTP #{resp.status}")
|
|
34
34
|
rescue Faraday::Error => e
|
|
35
|
-
|
|
35
|
+
# 원본을 달면 `Faraday::ParsingError#inspect` 가 요청의 베어러 토큰을, 메시지가 응답 앞부분을 찍는다(실측).
|
|
36
|
+
raise TransportError, "admin transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def id_from_location(resp)
|
|
@@ -11,6 +11,10 @@ module KeycloakSdk
|
|
|
11
11
|
class AuthClient
|
|
12
12
|
include TokenProvider
|
|
13
13
|
|
|
14
|
+
# 메시지에 실어도 되는 OAuth 오류 코드 모양 — RFC 6749·OIDC 등록 코드와 Keycloak 코드는 전부 이 모양이고,
|
|
15
|
+
# 토큰·시크릿(JWT·base64·hex·UUID)은 대문자·숫자·점·하이픈 때문에 걸러진다.
|
|
16
|
+
OAUTH_CODE = /\A[a-z_]{1,64}\z/
|
|
17
|
+
|
|
14
18
|
def initialize(config:, http:, jwt_validator:)
|
|
15
19
|
@config = config
|
|
16
20
|
@http = http
|
|
@@ -35,37 +39,25 @@ module KeycloakSdk
|
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# `expected_nonce`가 주어지면(create_authorization_request가 항상 돌려주는 nonce) 응답 id_token을
|
|
38
|
-
# realm JWKS로 서명·iss·aud·exp까지 강화 검증한 뒤 nonce 클레임을 대조한다 — OIDC nonce 재생
|
|
42
|
+
# realm JWKS로 서명·iss·aud(client_id)·exp까지 강화 검증한 뒤 nonce 클레임을 대조한다 — OIDC nonce 재생
|
|
39
43
|
# 방지. 불일치·부재·검증실패는 모두 거부(fail-closed). 생략 시 id_token 검증을 건너뛴다
|
|
40
44
|
# (여덟 언어 공통 — exchange에서 nonce를 필수로 만들지 않는다).
|
|
41
45
|
def exchange_code(code:, code_verifier:, redirect_uri:, expected_nonce: nil)
|
|
42
46
|
client = oauth_client(redirect_uri: redirect_uri)
|
|
43
47
|
client.authorization_code = code
|
|
44
|
-
token_set =
|
|
48
|
+
token_set = token_request("authorization_code exchange") { client.access_token!(code_verifier: code_verifier) }
|
|
45
49
|
verify_nonce!(token_set.id_token, expected_nonce) unless expected_nonce.nil?
|
|
46
50
|
token_set
|
|
47
|
-
rescue Rack::OAuth2::Client::Error => e
|
|
48
|
-
raise AuthError.new("authorization_code exchange failed: #{e.message}", oauth_error: e.response[:error].to_s)
|
|
49
|
-
rescue Faraday::Error => e
|
|
50
|
-
raise TransportError, "token endpoint transport error: #{e.message}"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def refresh(refresh_token:)
|
|
54
54
|
client = oauth_client
|
|
55
55
|
client.refresh_token = refresh_token
|
|
56
|
-
|
|
57
|
-
rescue Rack::OAuth2::Client::Error => e
|
|
58
|
-
raise AuthError.new("refresh failed: #{e.message}", oauth_error: e.response[:error].to_s)
|
|
59
|
-
rescue Faraday::Error => e
|
|
60
|
-
raise TransportError, "token endpoint transport error: #{e.message}"
|
|
56
|
+
token_request("refresh") { client.access_token! }
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
def client_credentials_token
|
|
64
|
-
|
|
65
|
-
rescue Rack::OAuth2::Client::Error => e
|
|
66
|
-
raise AuthError.new("client-credentials failed: #{e.message}", oauth_error: e.response[:error].to_s)
|
|
67
|
-
rescue Faraday::Error => e
|
|
68
|
-
raise TransportError, "token endpoint transport error: #{e.message}"
|
|
60
|
+
token_request("client-credentials") { oauth_client.access_token!(scope: @config.scopes.join(" ")) }
|
|
69
61
|
end
|
|
70
62
|
|
|
71
63
|
# TokenProvider 계약(직접 사용용). admin은 캐싱 provider를 별도로 쓴다.
|
|
@@ -81,7 +73,7 @@ module KeycloakSdk
|
|
|
81
73
|
|
|
82
74
|
IntrospectionResult.from_response(resp.body)
|
|
83
75
|
rescue Faraday::Error => e
|
|
84
|
-
raise TransportError, "introspection transport error: #{e
|
|
76
|
+
raise TransportError, "introspection transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
85
77
|
end
|
|
86
78
|
|
|
87
79
|
def logout(refresh_token:)
|
|
@@ -93,7 +85,7 @@ module KeycloakSdk
|
|
|
93
85
|
|
|
94
86
|
nil
|
|
95
87
|
rescue Faraday::Error => e
|
|
96
|
-
raise TransportError, "logout transport error: #{e
|
|
88
|
+
raise TransportError, "logout transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
def validate(token)
|
|
@@ -112,14 +104,13 @@ module KeycloakSdk
|
|
|
112
104
|
end
|
|
113
105
|
end
|
|
114
106
|
|
|
115
|
-
# id_token의 nonce 클레임을 대조하기 전에 강화 JwtValidator로 서명·iss·aud·exp까지
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# 이 흐름을 쓴다면 해당 오디언스를 id_token에도 매핑해야 한다(audience 매퍼의 "Add to ID token").
|
|
107
|
+
# id_token의 nonce 클레임을 대조하기 전에 강화 JwtValidator로 서명·iss·aud·exp까지 검증한다.
|
|
108
|
+
# aud 는 `expected_audience` 재정의가 아니라 **client_id** 다(OIDC Core §2·§3.1.3.7) — 재정의는 액세스 토큰의 것.
|
|
109
|
+
# 검증기(=키 저장소)는 `validate` 와 같은 하나를 쓴다.
|
|
119
110
|
def verify_nonce!(id_token, expected_nonce)
|
|
120
111
|
raise AuthError, "authorization_code exchange failed: missing id_token for nonce validation" if id_token.nil?
|
|
121
112
|
|
|
122
|
-
validated = @jwt_validator.validate(id_token)
|
|
113
|
+
validated = @jwt_validator.validate(id_token, audience: @config.client_id)
|
|
123
114
|
return if validated.claims["nonce"] == expected_nonce
|
|
124
115
|
|
|
125
116
|
raise AuthError, "authorization_code exchange failed: unexpected nonce"
|
|
@@ -127,6 +118,27 @@ module KeycloakSdk
|
|
|
127
118
|
raise AuthError, "authorization_code exchange failed: invalid id_token: #{e.message}"
|
|
128
119
|
end
|
|
129
120
|
|
|
121
|
+
# rack-oauth2 토큰 호출의 오류 경계(§4). 하위 예외는 SDK 타입이 되고 `cause` 에는 원본 대신 `RedactedCause` 가 달린다.
|
|
122
|
+
# ⚠️ OAuth 오류는 **코드와 HTTP 상태만** 싣는다 — `error_description` 은 서버가 고른 자유 문장이라 토큰을
|
|
123
|
+
# 되울릴 수 있고, 오류 본문이 JSON 이 아니면 rack-oauth2 가 본문 전체를 거기 넣는다. 코드 자리도 서버 값이라
|
|
124
|
+
# **코드 모양**(`OAUTH_CODE` — 등록 코드는 전부 소문자·밑줄)일 때만 메시지에 싣는다(`oauth_error` 에는 그대로).
|
|
125
|
+
# ⚠️ 마지막 `StandardError` 는 rack-oauth2 가 형식이 틀린 200 을 읽다 내는 것(NoMethodError·AttrMissing·
|
|
126
|
+
# 'Unknown Token Type')과 `to_token_set` 의 형 변환 실패다 — 원본이 새면 Ruby 3.2 의 NoMethodError 가 본문을 인용한다.
|
|
127
|
+
def token_request(operation)
|
|
128
|
+
to_token_set(yield)
|
|
129
|
+
rescue Error
|
|
130
|
+
raise
|
|
131
|
+
rescue Rack::OAuth2::Client::Error => e
|
|
132
|
+
code = e.response[:error].is_a?(String) ? e.response[:error] : ""
|
|
133
|
+
shown = code.match?(OAUTH_CODE) ? code : "non-standard error code"
|
|
134
|
+
raise AuthError.new("#{operation} failed: #{shown} (HTTP #{e.status})", oauth_error: code),
|
|
135
|
+
cause: RedactedCause.new(e)
|
|
136
|
+
rescue Faraday::Error => e
|
|
137
|
+
raise TransportError, "token endpoint transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
raise AuthError, "#{operation} failed: unusable token response (#{e.class})", cause: RedactedCause.new(e)
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
def oauth_client(redirect_uri: nil)
|
|
131
143
|
Rack::OAuth2::Client.new(
|
|
132
144
|
identifier: @config.client_id,
|
|
@@ -137,17 +149,19 @@ module KeycloakSdk
|
|
|
137
149
|
)
|
|
138
150
|
end
|
|
139
151
|
|
|
152
|
+
# `expires_in` 은 `TokenSet.from_response` 와 같이 정수로 읽는다(문자열 "300" 허용) — 예전에는 문자열이면
|
|
153
|
+
# `Float + String` 의 TypeError 가 경계를 뚫었다. 변환 실패는 `token_request` 가 AuthError 로 바꾼다.
|
|
140
154
|
def to_token_set(token)
|
|
141
155
|
raw = token.raw_attributes || {}
|
|
142
|
-
|
|
156
|
+
expires_in = token.expires_in && Integer(token.expires_in)
|
|
143
157
|
TokenSet.new(
|
|
144
158
|
access_token: token.access_token,
|
|
145
159
|
token_type: "Bearer",
|
|
146
|
-
expires_in:
|
|
160
|
+
expires_in: expires_in,
|
|
147
161
|
refresh_token: token.refresh_token,
|
|
148
162
|
id_token: raw[:id_token] || raw["id_token"],
|
|
149
|
-
scope: scope,
|
|
150
|
-
expires_at:
|
|
163
|
+
scope: raw[:scope] || raw["scope"],
|
|
164
|
+
expires_at: expires_in ? Time.now.to_f + expires_in : nil
|
|
151
165
|
)
|
|
152
166
|
end
|
|
153
167
|
end
|
data/lib/keycloak_sdk/errors.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
3
7
|
module KeycloakSdk
|
|
4
8
|
# 모든 SDK 오류의 루트.
|
|
5
9
|
class Error < StandardError; end
|
|
@@ -46,4 +50,39 @@ module KeycloakSdk
|
|
|
46
50
|
class NotFoundError < AdminError; end
|
|
47
51
|
class ConflictError < AdminError; end
|
|
48
52
|
class ForbiddenError < AdminError; end
|
|
53
|
+
|
|
54
|
+
# SDK 오류의 `cause` 에 원본 하위 예외 대신 다는 사본 — **클래스 이름 사슬과 백트레이스만** 옮긴다.
|
|
55
|
+
# 경계는 `raise …, cause: RedactedCause.new(e)` 로 단다(`KeycloakSdk::Error` 가 아니다 — 원인 자리에만 있다).
|
|
56
|
+
#
|
|
57
|
+
# ⚠️ Ruby 는 `rescue` 안의 `raise` 가 처리 중인 예외를 `cause` 에 **자동으로** 달고, `full_message`(잡히지 않은
|
|
58
|
+
# 예외·로거)가 그 사슬의 메시지를 찍는다. 하위 예외는 응답을 싣는다 — JSON 파서는 본문 앞 32 바이트를,
|
|
59
|
+
# rack-oauth2 는 `error_description`(JSON 이 아닌 오류면 본문 전체)을, Net::HTTP 는 깨진 상태 줄을 메시지에 싣고,
|
|
60
|
+
# `Faraday::ParsingError#inspect` 는 **요청** 본문·헤더(클라이언트 시크릿·refresh 토큰·베어러)까지 찍었다. Ruby 3.2 의
|
|
61
|
+
# `NoMethodError` 는 수신자(응답 본문)를 인용했다(실측 2026-09-26 · `spec/unit/hostile_token_response_spec.rb`).
|
|
62
|
+
# 원본 객체를 공개 API 로 내보내지 않는 것은 §4 도 요구한다.
|
|
63
|
+
class RedactedCause < StandardError
|
|
64
|
+
# 메시지를 SDK 메시지로 옮겨도 되는 하위 원인 — 응답을 인용할 수 없는 소켓·타임아웃·TLS 실패뿐이다.
|
|
65
|
+
# ⚠️ `Faraday::ConnectionFailed` 를 통째로 넣지 않는다 — `Net::HTTPBadResponse`(깨진 상태 줄 dump)도 그것으로 온다.
|
|
66
|
+
QUOTE_FREE = [SystemCallError, SocketError, IOError, Timeout::Error, OpenSSL::SSL::SSLError].freeze
|
|
67
|
+
|
|
68
|
+
# 원본과 그 `cause` 들의 클래스 이름(바깥부터).
|
|
69
|
+
attr_reader :lower_classes
|
|
70
|
+
|
|
71
|
+
def initialize(error)
|
|
72
|
+
@lower_classes = []
|
|
73
|
+
link = error
|
|
74
|
+
while link && @lower_classes.size < 8
|
|
75
|
+
@lower_classes << link.class.name
|
|
76
|
+
link = link.cause
|
|
77
|
+
end
|
|
78
|
+
super("#{@lower_classes.join(' <- ')} (message withheld: it can quote the response)")
|
|
79
|
+
set_backtrace(error.backtrace) if error.backtrace
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# SDK 메시지에 실을 하위 오류 설명 — 인용할 수 없는 연결 실패면 그 메시지, 아니면 클래스 이름뿐.
|
|
83
|
+
def self.describe(error)
|
|
84
|
+
origin = error.respond_to?(:wrapped_exception) && error.wrapped_exception ? error.wrapped_exception : error
|
|
85
|
+
QUOTE_FREE.any? { |k| origin.is_a?(k) } ? error.message : error.class.name
|
|
86
|
+
end
|
|
87
|
+
end
|
|
49
88
|
end
|
|
@@ -122,11 +122,11 @@ module KeycloakSdk
|
|
|
122
122
|
body = begin
|
|
123
123
|
JSON.parse(buf)
|
|
124
124
|
rescue JSON::ParserError => e
|
|
125
|
-
raise TransportError, "JWKS response unparsable:
|
|
125
|
+
raise TransportError, "JWKS response unparsable (JSON::ParserError)", cause: RedactedCause.new(e)
|
|
126
126
|
end
|
|
127
127
|
validate_key_set!(body)
|
|
128
128
|
rescue Faraday::Error => e
|
|
129
|
-
raise TransportError, "JWKS transport error: #{e
|
|
129
|
+
raise TransportError, "JWKS transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# 파싱된 본문이 **캐시에 올려도 되는 집합인가**. 통과하면 그 본문을 돌려준다.
|
|
@@ -30,21 +30,28 @@ module KeycloakSdk
|
|
|
30
30
|
algorithms: config.signature_algorithms, clock_skew: config.clock_skew)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# `audience:` 는 이 호출만의 기대 aud 다(기본 = 생성 시 값). 코드 교환이 id_token 을 client_id 로 검사할 때
|
|
34
|
+
# 쓴다 — 키 저장소(캐시 · 재조회 제한 · 백오프)는 그대로 이 인스턴스의 것 하나다.
|
|
35
|
+
# ⚠️ nil·공백은 생성자와 같이 ConfigError — ruby-jwt 는 `aud: nil` 이면 aud 검사를 건너뛴다.
|
|
36
|
+
def validate(token, audience: @audience)
|
|
37
|
+
raise ConfigError, "audience is required" if audience.nil? || audience.to_s.strip.empty?
|
|
38
|
+
|
|
39
|
+
payload, = JWT.decode(token, nil, true, decode_options(audience))
|
|
35
40
|
to_validated(payload)
|
|
36
41
|
rescue JWT::DecodeError => e
|
|
37
|
-
|
|
42
|
+
# ruby-jwt 메시지는 입력을 인용하지 않는다. 그 **원인**은 인용한다 — 조각이 JSON 이 아니면 JSON::ParserError 가
|
|
43
|
+
# 디코드한 토큰 조각을 싣는다(실측) — 그래서 사슬은 `RedactedCause` 로 끊는다.
|
|
44
|
+
raise TokenValidationError, "JWT validation failed: #{e.message}", cause: RedactedCause.new(e)
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
private
|
|
41
48
|
|
|
42
|
-
def decode_options
|
|
49
|
+
def decode_options(audience)
|
|
43
50
|
{
|
|
44
51
|
algorithms: @algorithms,
|
|
45
52
|
jwks: jwks_loader,
|
|
46
53
|
verify_iss: true, iss: @issuer,
|
|
47
|
-
verify_aud: true, aud:
|
|
54
|
+
verify_aud: true, aud: audience,
|
|
48
55
|
verify_expiration: true,
|
|
49
56
|
verify_not_before: true,
|
|
50
57
|
required_claims: %w[exp iss aud],
|
|
@@ -59,7 +59,7 @@ module KeycloakSdk
|
|
|
59
59
|
|
|
60
60
|
TokenSet.from_response(resp.body, received_at: Time.now.to_f)
|
|
61
61
|
rescue Faraday::Error => e
|
|
62
|
-
raise TransportError, "token endpoint transport error: #{e
|
|
62
|
+
raise TransportError, "token endpoint transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
data/lib/keycloak_sdk/tokens.rb
CHANGED
|
@@ -17,6 +17,9 @@ module KeycloakSdk
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.from_response(body, received_at: Time.now.to_f)
|
|
20
|
+
# 배열 본문은 `Array#[]` 의 TypeError 로 경계를 뚫었다 — 객체가 아니면 SDK 오류다.
|
|
21
|
+
raise AuthError, "token response is not a JSON object" unless body.is_a?(Hash)
|
|
22
|
+
|
|
20
23
|
# ⚠️ **존재 검사는 타입 검사가 아니다.** 예전에는 값을 그대로 담아 숫자·해시·nil 이
|
|
21
24
|
# `access_token` 이 됐고, 소비자는 그것을 Bearer 로 실어 보내 매번 401 을 받았다
|
|
22
25
|
# (조용한 반복 실패). `expires_in` 의 문자열 허용은 **의도된 것**이라 건드리지 않는다.
|
|
@@ -25,7 +28,12 @@ module KeycloakSdk
|
|
|
25
28
|
raise AuthError, "token response has no usable access_token"
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
expires_in =
|
|
31
|
+
expires_in = begin
|
|
32
|
+
body["expires_in"] && Integer(body["expires_in"])
|
|
33
|
+
rescue ArgumentError, TypeError, RangeError # RangeError: JSON 1e400 = Infinity → FloatDomainError
|
|
34
|
+
# `cause: nil` — ArgumentError 는 그 값을 인용한다(`invalid value for Integer(): "…"`).
|
|
35
|
+
raise AuthError, "token response has an unusable expires_in", cause: nil
|
|
36
|
+
end
|
|
29
37
|
new(
|
|
30
38
|
access_token: access_token,
|
|
31
39
|
token_type: body["token_type"],
|
|
@@ -67,7 +75,10 @@ module KeycloakSdk
|
|
|
67
75
|
|
|
68
76
|
# RFC 7662 introspection 결과.
|
|
69
77
|
IntrospectionResult = Data.define(:active, :username, :client_id, :claims) do
|
|
78
|
+
# 객체가 아닌 본문은 SDK 오류다 — 배열은 TypeError 로 경계를 뚫었고, 문자열은 `claims` 가 문자열인 결과가 됐다.
|
|
70
79
|
def self.from_response(body)
|
|
80
|
+
raise AuthError, "introspection response is not a JSON object" unless body.is_a?(Hash)
|
|
81
|
+
|
|
71
82
|
new(active: body["active"] == true, username: body["username"],
|
|
72
83
|
client_id: body["client_id"], claims: body)
|
|
73
84
|
end
|
data/lib/keycloak_sdk/version.rb
CHANGED