keycloak-sdk 0.1.0.rc1 → 0.1.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 +1 -2
- data/lib/keycloak_sdk/auth_client.rb +7 -5
- data/lib/keycloak_sdk/config.rb +32 -4
- data/lib/keycloak_sdk/jwks_store.rb +4 -1
- data/lib/keycloak_sdk/jwt_validator.rb +4 -1
- data/lib/keycloak_sdk/tokens.rb +4 -2
- 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: 8c092fe05aa83b05dbdeff11100f82093a4e757085d503c9e7f0bf2c25686823
|
|
4
|
+
data.tar.gz: ca6060f7b57451442cb91d57df7d2d711d7ce5568dd8e60dcd2a120aa9df9a64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0acfdff469b5e3b7962e93219394ec0cf982e0bfa9cd779f5cf57696c832bd36102904338fb8e94aa19e54e8aed5f1a5a975bfeadd8c54742c97fecbbb3756a
|
|
7
|
+
data.tar.gz: 61043f7b7e7912fb3c3fbb5d29a006483dbc16e768f0b1678e60ccfb1725901573bd4538762ea57dda26a8beca1f0a03d7ca94d996e1c656ef676692da68f09a
|
data/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Authentication (OIDC / OAuth2) and the Admin REST API for [Keycloak](https://www.keycloak.org/) behind one consistent facade, with hardened JWT validation.
|
|
4
4
|
|
|
5
|
-
English · [한국어](https://github.com/xzawed/KeyCloakSDK/blob/main/ruby/README.ko.md)
|
|
6
|
-
|
|
7
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).
|
|
8
6
|
|
|
9
7
|
> **Pre-release** — the first release candidate (`0.1.0.rc1`) is on RubyGems; there is no stable release yet. ⚠️ **RubyGems does not install a pre-release by default**: a bare `gem install keycloak-sdk` finds nothing until a stable version exists. Ask for it explicitly — `gem install keycloak-sdk --pre`, or pin the exact version as shown below.
|
|
@@ -68,6 +66,7 @@ The SDK replaces the unsafe library defaults rather than inheriting them:
|
|
|
68
66
|
- **Algorithm pinning** — the header-supplied `alg` is never trusted, so `alg: none` and HS/RS confusion are rejected structurally: the pin is applied before key lookup and signature verification, not after.
|
|
69
67
|
- **Strict claim checks** — exact `iss` match, `aud` containment, mandatory `exp`, `nbf`, and a bounded clock skew.
|
|
70
68
|
- **DoS-safe JWKS** — a refetch is triggered only by an unresolved key ID and never by a bad signature, and is rate-limited to a minimum interval (`jwks_min_refetch`, 30s by default). The gate applies on a cold cache too, so it cannot be sidestepped by hitting the SDK before its first successful fetch — no volume of forged tokens makes the SDK issue more than one JWKS request per interval.
|
|
69
|
+
- **OIDC nonce / `id_token` replay protection** — `create_authorization_request` always issues a nonce (same default as `state:`) and puts it on the authorization URL. Pass it back as `exchange_code(expected_nonce:)` and the SDK fully validates the `id_token` before comparing the nonce claim. Omit `expected_nonce:` and id_token validation is skipped (same opt-out as the other eight languages).
|
|
71
70
|
- **Secret handling** — `Config`, `TokenSet`, and `AuthorizationRequest` mask secrets and tokens in `#inspect` (`***`, no prefix leak), TLS verification is on by default, timeouts are always applied, and redirect-following middleware is never installed (SSRF hardening).
|
|
72
71
|
|
|
73
72
|
Masking covers this SDK's own `#inspect`; it cannot cover what your logging framework or a backtrace does with a value you hand it. Ruby has no erasable string type, so the client secret lives in an ordinary `String` for its lifetime — masking is defence in depth, not an erasure guarantee.
|
|
@@ -19,23 +19,25 @@ module KeycloakSdk
|
|
|
19
19
|
configure_rack_oauth2_timeouts(config)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def create_authorization_request(redirect_uri:, scopes: nil, state: SecureRandom.urlsafe_base64(24),
|
|
22
|
+
def create_authorization_request(redirect_uri:, scopes: nil, state: SecureRandom.urlsafe_base64(24),
|
|
23
|
+
nonce: SecureRandom.urlsafe_base64(24))
|
|
23
24
|
verifier = SecureRandom.urlsafe_base64(64)
|
|
24
25
|
challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
|
25
26
|
params = {
|
|
26
27
|
scope: (scopes || @config.scopes).join(" "),
|
|
27
28
|
state: state,
|
|
29
|
+
nonce: nonce,
|
|
28
30
|
code_challenge: challenge,
|
|
29
31
|
code_challenge_method: :S256
|
|
30
32
|
}
|
|
31
|
-
params[:nonce] = nonce if nonce
|
|
32
33
|
url = oauth_client(redirect_uri: redirect_uri).authorization_uri(params)
|
|
33
|
-
AuthorizationRequest.new(url: url.to_s, state: state, code_verifier: verifier)
|
|
34
|
+
AuthorizationRequest.new(url: url.to_s, state: state, code_verifier: verifier, nonce: nonce)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# `expected_nonce`가 주어지면(create_authorization_request가
|
|
37
|
+
# `expected_nonce`가 주어지면(create_authorization_request가 항상 돌려주는 nonce) 응답 id_token을
|
|
37
38
|
# realm JWKS로 서명·iss·aud·exp까지 강화 검증한 뒤 nonce 클레임을 대조한다 — OIDC nonce 재생
|
|
38
|
-
# 방지. 불일치·부재·검증실패는 모두 거부(fail-closed). 생략 시 id_token 검증을 건너뛴다
|
|
39
|
+
# 방지. 불일치·부재·검증실패는 모두 거부(fail-closed). 생략 시 id_token 검증을 건너뛴다
|
|
40
|
+
# (여덟 언어 공통 — exchange에서 nonce를 필수로 만들지 않는다).
|
|
39
41
|
def exchange_code(code:, code_verifier:, redirect_uri:, expected_nonce: nil)
|
|
40
42
|
client = oauth_client(redirect_uri: redirect_uri)
|
|
41
43
|
client.authorization_code = code
|
data/lib/keycloak_sdk/config.rb
CHANGED
|
@@ -3,15 +3,31 @@
|
|
|
3
3
|
module KeycloakSdk
|
|
4
4
|
# 불변 설정. 생성 시 검증하고 freeze한다. client_secret은 inspect에서 마스킹.
|
|
5
5
|
class Config
|
|
6
|
+
# JWKS 최소 재조회 간격 기본값의 **유일한 정의 자리**(초). DoS 증폭 상한이고 아홉 언어가
|
|
7
|
+
# 같은 값으로 정렬돼 있다 — `scripts/test/test-security-defaults.sh`가 아홉 언어 코드와
|
|
8
|
+
# 소비자 문서를 함께 대조한다.
|
|
9
|
+
#
|
|
10
|
+
# ⚠️ 이 숫자를 다른 곳에 다시 적지 말 것. 예전에는 여기 30.0, `JwksStore#initialize`에 10.0으로
|
|
11
|
+
# **두 번** 적혀 있었다. `JwksStore`는 평범한 public 클래스라 소비자가 직접 생성하면
|
|
12
|
+
# (파사드를 거치지 않으면) 문서가 말하는 30초가 아니라 10초를 받아 **IdP를 3배 자주** 때렸다.
|
|
13
|
+
# 한글 README도 그 10.0을 그대로 옮겨 적고 있었다(2026-08-12 문서 감사 → 2026-08-13 Task D1).
|
|
14
|
+
DEFAULT_JWKS_MIN_REFETCH = 30.0
|
|
15
|
+
|
|
16
|
+
# JWT `exp`/`nbf` 검증의 시계 오차 허용치 기본값(초). 이것도 아홉 언어 공동 불변식이다 —
|
|
17
|
+
# 한 언어만 커지면 **그 언어에서만 만료된 토큰이 더 오래 통과한다**.
|
|
18
|
+
# ⚠️ `JwtValidator#initialize`가 같은 값을 두 번째로 적고 있었다(둘 다 30이라 아직 갈리지는
|
|
19
|
+
# 않았으나 JWKS가 10.0/30.0으로 갈린 것과 똑같은 모양이다). 그 자리는 이제 이 상수를 참조한다.
|
|
20
|
+
DEFAULT_CLOCK_SKEW = 30
|
|
21
|
+
|
|
6
22
|
attr_reader :server_url, :realm, :client_id, :client_secret,
|
|
7
23
|
:scopes, :signature_algorithms, :connect_timeout, :read_timeout, :clock_skew,
|
|
8
24
|
:jwks_min_refetch, :expected_audience
|
|
9
25
|
|
|
10
26
|
def initialize(server_url:, realm:, client_id:, client_secret: nil,
|
|
11
27
|
scopes: ["openid"], signature_algorithms: ["RS256"],
|
|
12
|
-
connect_timeout: 10, read_timeout: 10, clock_skew:
|
|
13
|
-
jwks_min_refetch:
|
|
14
|
-
@server_url = normalize_required("server_url", server_url)
|
|
28
|
+
connect_timeout: 10, read_timeout: 10, clock_skew: DEFAULT_CLOCK_SKEW,
|
|
29
|
+
jwks_min_refetch: DEFAULT_JWKS_MIN_REFETCH, expected_audience: nil)
|
|
30
|
+
@server_url = strip_trailing_slashes(normalize_required("server_url", server_url))
|
|
15
31
|
@realm = normalize_required("realm", realm)
|
|
16
32
|
@client_id = normalize_required("client_id", client_id)
|
|
17
33
|
@client_secret = client_secret
|
|
@@ -22,7 +38,7 @@ module KeycloakSdk
|
|
|
22
38
|
@connect_timeout = positive("connect_timeout", connect_timeout)
|
|
23
39
|
@read_timeout = positive("read_timeout", read_timeout)
|
|
24
40
|
@clock_skew = non_negative("clock_skew", clock_skew)
|
|
25
|
-
# 미해결 kid로 인한 JWKS 재조회의 최소 간격(
|
|
41
|
+
# 미해결 kid로 인한 JWKS 재조회의 최소 간격(초) — DoS 증폭 상한. 기본값은 위 상수.
|
|
26
42
|
@jwks_min_refetch = non_negative("jwks_min_refetch", jwks_min_refetch)
|
|
27
43
|
# 토큰 aud에 들어있어야 할 값(기본 nil = client_id). 기본 realm은 client-credentials 토큰의
|
|
28
44
|
# aud에 client_id를 넣지 않으므로, realm이 실제로 발급하는 리소스/오디언스를 지정한다.
|
|
@@ -39,6 +55,18 @@ module KeycloakSdk
|
|
|
39
55
|
|
|
40
56
|
private
|
|
41
57
|
|
|
58
|
+
# 후행 슬래시 제거. 정규식(`sub(%r{/+\z}, "")`)이 아니라 선형 스캔인 이유는 **동형성**이다 —
|
|
59
|
+
# 같은 일을 하는 아홉 언어 중 go(`TrimRight`)·dotnet(`TrimEnd`)·php(`rtrim`)·rust
|
|
60
|
+
# (`trim_end_matches`)·kotlin(`trimEnd`) 다섯이 선형 문자열 트림을 쓰고, 정규식을 쓰던 것은
|
|
61
|
+
# java·node·ruby 셋뿐이었다. java/node는 SonarCloud S8786(정규식 초선형 백트래킹)으로 지적됐고
|
|
62
|
+
# ruby는 지적되지 않았지만, 셋을 함께 옮겨야 "같은 개념은 같은 모양"이라는 이 저장소의 전제가
|
|
63
|
+
# 유지된다. 동작은 정규식과 동일하다(후행 슬래시 전부 제거, 내부 슬래시 보존).
|
|
64
|
+
def strip_trailing_slashes(str)
|
|
65
|
+
i = str.length
|
|
66
|
+
i -= 1 while i.positive? && str[i - 1] == "/"
|
|
67
|
+
str[0, i]
|
|
68
|
+
end
|
|
69
|
+
|
|
42
70
|
def normalize_required(name, value)
|
|
43
71
|
raise ConfigError, "#{name} is required" if value.nil?
|
|
44
72
|
|
|
@@ -8,7 +8,10 @@ module KeycloakSdk
|
|
|
8
8
|
# 미해결 kid(force:true)만 재조회하며, rate-limit gate는 재조회 결정 시점에 stamp한다
|
|
9
9
|
# (성공 아님 — IdP 장애창에서 위조 kid 폭주에도 재조회를 상한한다). Go/Rust/Python 동형.
|
|
10
10
|
class JwksStore
|
|
11
|
-
|
|
11
|
+
# ⚠️ 기본값을 여기 숫자로 적지 말 것 — `Config`가 유일한 정의 자리다. 이 클래스는 평범한
|
|
12
|
+
# public 클래스라 소비자가 파사드를 거치지 않고 직접 생성할 수 있고, 예전에는 그 경로가
|
|
13
|
+
# 문서의 30초가 아니라 10초를 받아 IdP를 3배 자주 때렸다(2026-08-13 Task D1).
|
|
14
|
+
def initialize(jwks_url:, http:, min_refetch: Config::DEFAULT_JWKS_MIN_REFETCH)
|
|
12
15
|
@jwks_url = jwks_url
|
|
13
16
|
@http = http
|
|
14
17
|
@min_refetch = min_refetch
|
|
@@ -7,7 +7,10 @@ module KeycloakSdk
|
|
|
7
7
|
# RS256 핀(none/confusion 구조적 거부)·iss 정확·aud 포함·exp 필수·nbf·클록스큐.
|
|
8
8
|
# 키는 DoS-safe JwksStore로 조회한다(위조 서명은 재조회 미유발). 헤더 alg는 검증 알고리즘 선택에 미사용.
|
|
9
9
|
class JwtValidator
|
|
10
|
-
|
|
10
|
+
# ⚠️ `clock_skew` 기본값을 여기 숫자로 적지 말 것 — `Config`가 유일한 정의 자리다(Task D1과
|
|
11
|
+
# 같은 부류). 이 클래스도 public이라 소비자가 직접 생성할 수 있고, 두 자리가 갈리면 그
|
|
12
|
+
# 경로에서만 만료된 토큰이 더 오래 통과한다.
|
|
13
|
+
def initialize(issuer:, audience:, jwks_store:, algorithms: ["RS256"], clock_skew: Config::DEFAULT_CLOCK_SKEW)
|
|
11
14
|
# ruby-jwt의 verify_iss/verify_aud 빌더는 값이 nil이면 조용히 no-op이 되어
|
|
12
15
|
# verify_iss:true/verify_aud:true를 켜도 검사를 건너뛴다 — fail-closed로 방어.
|
|
13
16
|
raise ConfigError, "issuer is required" if issuer.nil? || issuer.to_s.strip.empty?
|
data/lib/keycloak_sdk/tokens.rb
CHANGED
|
@@ -47,9 +47,11 @@ module KeycloakSdk
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# authorization-code 흐름 시작 값(PKCE code_verifier 포함·inspect 마스킹).
|
|
50
|
-
|
|
50
|
+
# nonce는 인가 URL에 실리는 재생 방지 값이라 비밀이 아니다(state와 동급 — code_verifier만 마스킹).
|
|
51
|
+
AuthorizationRequest = Data.define(:url, :state, :code_verifier, :nonce) do
|
|
51
52
|
def inspect
|
|
52
|
-
"#<KeycloakSdk::AuthorizationRequest url=#{url.inspect} state=#{state.inspect}
|
|
53
|
+
"#<KeycloakSdk::AuthorizationRequest url=#{url.inspect} state=#{state.inspect} " \
|
|
54
|
+
"nonce=#{nonce.inspect} code_verifier=\"***\">"
|
|
53
55
|
end
|
|
54
56
|
alias_method :to_s, :inspect
|
|
55
57
|
end
|
data/lib/keycloak_sdk/version.rb
CHANGED