544 lines
18 KiB
Markdown
544 lines
18 KiB
Markdown
# keycloak 재검토 리뷰 — 2026-09-19
|
|
|
|
## 판정
|
|
|
|
**keycloak = 아직 더 봐야댐**
|
|
|
|
직전 상세 리뷰의 K01~K06은 상당 부분 반영되었다. 그러나 K03과 K04는 의미상 완전히 닫히지 않았고, 같은 기술 정확성 기준으로 전체 문맥을 다시 읽는 과정에서 Authorization Endpoint의 Referrer 설명이 지나치게 일반화된 부분도 추가로 확인했다.
|
|
|
|
자동 검증은 모두 통과했다. 이번 판정이 남아 있는 이유는 빌드/파이프라인 실패가 아니라 **기술 의미와 문서 내부 정의의 일관성** 때문이다.
|
|
|
|
현재 source repository:
|
|
|
|
`/home/donghyeon/workspace/keycloak-pattern`
|
|
|
|
는 현재 머신에 존재하지 않는다. 따라서 live source reconciliation은 계속 **UNVERIFIABLE**이다. 이를 PASS로 간주하지 않는다.
|
|
|
|
---
|
|
|
|
# 1. 이번 재검토 기준
|
|
|
|
이번 검토는 직전 리뷰의 K01~K06 문자열만 없어졌는지 확인하는 방식으로 하지 않았다.
|
|
|
|
동일한 기준을 세 층으로 유지했다.
|
|
|
|
## 1.1 정본 정확성
|
|
|
|
`final/document.md` 자체가 OAuth/OIDC, Keycloak, Spring Security의 의미를 기술적으로 정확하게 설명하는지 본다.
|
|
|
|
확인 기준:
|
|
|
|
- 일반 표준 규칙과 이 프로젝트의 구현 사실을 구분하는가
|
|
- 사실, 추론, 미확인을 섞지 않는가
|
|
- 현재 구현에서 확인하지 않은 것을 확인한 것처럼 쓰지 않는가
|
|
- 특정 구현의 우연한 배치를 OAuth/OIDC 일반 규칙으로 확대하지 않는가
|
|
|
|
## 1.2 SSOT → Record 의미 보존
|
|
|
|
Concept / Reference / Case / Question이 SSOT를 파생하면서 다음을 바꾸지 않는지 본다.
|
|
|
|
- fact
|
|
- scope
|
|
- certainty
|
|
- causality
|
|
- condition
|
|
|
|
즉 문장이 자연스러워졌는지가 아니라 **정본이 말한 범위와 확실성이 그대로 유지됐는지**를 본다.
|
|
|
|
## 1.3 문서 시스템 무결성
|
|
|
|
의미가 맞더라도 문서 시스템에서 깨진 것이 없는지 본다.
|
|
|
|
- Natural prose
|
|
- Voice
|
|
- Tech Log Tree
|
|
- Project Layout
|
|
- Figure Text
|
|
- Figure Provenance
|
|
- Figure Overlap
|
|
- Required Content
|
|
- SSOT Facts
|
|
- Command Pedagogy
|
|
- git diff --check
|
|
- standalone pipeline
|
|
- full unittest regression
|
|
|
|
그림은 자동 PASS만 보지 않고 SSOT가 바뀐 뒤 context/spec이 여전히 같은 주장을 그리는지도 직접 본다.
|
|
|
|
### 맥락 유지 원칙
|
|
|
|
이번 재검토에서 **새 평가 축을 임의로 추가하지 않았다.**
|
|
|
|
새로 발견된 항목이 있더라도 그것은 기존 기준인:
|
|
|
|
1. 기술 정확성
|
|
2. 사실/추론/미확인 구분
|
|
3. SSOT ↔ Record semantic consistency
|
|
4. 설명 범위의 과도한 일반화 방지
|
|
|
|
중 하나로 설명될 때만 finding으로 잡았다.
|
|
|
|
---
|
|
|
|
# 2. 직전 K01~K06 반영 상태
|
|
|
|
| ID | 직전 finding | 현재 상태 | 판정 |
|
|
|---|---|---|---|
|
|
| K01 | AP2/AP3 PKCE scope 충돌 | AP2라는 주어가 명시되고 AP2 PKCE S256 미확인이 분리됨 | **CLOSED** |
|
|
| K02 | refreshTokenMaxReuse를 시간 창처럼 설명 | reuse count와 lifespan을 분리하고 runtime 경쟁은 미검증 유지 | **CLOSED** |
|
|
| K03 | client type이 token endpoint caller를 결정한다는 인과 | 본문은 수정됐으나 섹션 제목이 여전히 과도한 일반화 | **PARTIAL** |
|
|
| K04 | Public/Confidential 정의를 client secret 하나로 축소 | Reference 본문은 개선됐으나 서두·SSOT·Case에 축약 정의 잔존 | **PARTIAL** |
|
|
| K05 | “남는 선택지는 하나” 일반화 | AP1~AP4 네 패턴 범위로 제한됨 | **CLOSED** |
|
|
| K06 | CSRF token = 사용자 의도 증명 | cross-site forged request를 구분하는 anti-CSRF 검증으로 수정됨 | **CLOSED** |
|
|
|
|
따라서 K01~K06 전체가 닫힌 것은 아니다.
|
|
|
|
---
|
|
|
|
# 3. 남은 Findings
|
|
|
|
## R01 — HIGH — K04가 문서 전체에서는 아직 닫히지 않았다
|
|
|
|
직전 리뷰에서 요구한 핵심은:
|
|
|
|
> Public/Confidential client를 shared client secret 하나로 정의하지 말고,
|
|
> client credential의 기밀성 및 신뢰할 수 있는 client authentication 능력으로 설명한다.
|
|
|
|
Reference 본문은 이 방향으로 잘 수정됐다.
|
|
|
|
현재 올바르게 수정된 부분:
|
|
|
|
`reference-public-confidential-client.md:41`
|
|
|
|
> OAuth client type은 authorization server에 대해 client credential의 기밀성을 유지하고 신뢰할 수 있는 client authentication을 수행할 수 있는지로 구분한다.
|
|
|
|
`reference-public-confidential-client.md:53`
|
|
|
|
> Shared secret은 한 방식이고 private key나 mTLS 같은 다른 client authentication 방식도 가능하다.
|
|
|
|
문제는 이보다 앞의 문장과 다른 파생 문서가 여전히 예전 정의를 그대로 사용한다는 점이다.
|
|
|
|
### 잔여 1 — Reference 첫 정의
|
|
|
|
파일:
|
|
|
|
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-public-confidential-client.md`
|
|
|
|
23행:
|
|
|
|
> OAuth 클라이언트의 종류는 클라이언트 시크릿을 안전하게 보관할 수 있는지로 정한다.
|
|
|
|
41행에서 더 정확한 정의를 다시 제시하므로 **한 문서 안에서 정의가 두 개**가 된다.
|
|
|
|
### 잔여 2 — SSOT
|
|
|
|
파일:
|
|
|
|
`docs/keycloak/final/document.md`
|
|
|
|
183행:
|
|
|
|
> Public client는 브라우저처럼 client secret을 안전하게 숨길 수 없는 애플리케이션입니다.
|
|
|
|
210행:
|
|
|
|
> Confidential client는 client secret을 server에서 보관할 수 있는 애플리케이션입니다.
|
|
|
|
이 문장들은 “이 프로젝트가 shared secret을 사용한다”는 범위를 붙이면 설명용 단축 표현으로 사용할 수 있다.
|
|
|
|
하지만 현재 문장 구조는 OAuth client type의 일반 정의처럼 읽힌다.
|
|
|
|
### 잔여 3 — Case
|
|
|
|
파일:
|
|
|
|
`case-browser-credential-boundary.md`
|
|
|
|
101행:
|
|
|
|
> public client는 브라우저처럼 client secret을 안전하게 숨길 수 없는 애플리케이션이다.
|
|
|
|
파일:
|
|
|
|
`case-ap2-split-custody.md`
|
|
|
|
116행:
|
|
|
|
> confidential client는 client secret을 서버에 두고 자기를 인증할 수 있는 애플리케이션이다.
|
|
|
|
Case가 프로젝트 구현을 설명하는 문서인 만큼 shared secret 자체를 언급하는 것은 문제가 아니다. 다만 일반 정의처럼 시작하지 말고 프로젝트 scope를 먼저 걸어야 한다.
|
|
|
|
### 왜 중요한가
|
|
|
|
RFC 6749의 public/confidential 구분은 단순히 shared secret을 숨길 수 있는가만으로 정의되지 않는다. 핵심은 client credentials의 confidentiality를 유지할 수 있는지와 authorization server에 대해 secure client authentication을 수행할 수 있는지다.
|
|
|
|
따라서 Reference 본문만 수정하고 SSOT/Case의 정의형 문장을 남기면 semantic consistency가 깨진다.
|
|
|
|
### 수정 방향
|
|
|
|
Reference 첫 문장:
|
|
|
|
> OAuth client type은 authorization server에 대해 client credential의 기밀성을 유지하고 신뢰할 수 있는 client authentication을 수행할 수 있는지로 구분한다.
|
|
|
|
SSOT AP1:
|
|
|
|
> 이 프로젝트의 AP1 SPA는 사용자 기기에서 실행되어 장기 client credential의 기밀성을 유지하기 어려우므로 public client로 구성했습니다.
|
|
|
|
SSOT AP2:
|
|
|
|
> 이 프로젝트의 AP2 mediator는 server-side에서 shared client secret을 보호하고 `client_secret_basic`으로 인증하므로 confidential client로 구성했습니다.
|
|
|
|
Case도 같은 방식으로 **“일반 정의”가 아니라 “이 프로젝트에서 왜 그렇게 등록했는지”**로 바꾼다.
|
|
|
|
### 완료 조건
|
|
|
|
다음 패턴의 일반 정의형 문장이 0건이어야 한다.
|
|
|
|
- “public client는 client secret을 숨길 수 없는 애플리케이션이다”
|
|
- “confidential client는 client secret을 서버에 보관하는 애플리케이션이다”
|
|
- “client type은 client secret 보관 가능 여부로 정한다”
|
|
|
|
프로젝트에서 실제로 `client_secret_basic`을 쓴 사실은 삭제하지 않는다.
|
|
|
|
---
|
|
|
|
## R02 — MEDIUM — K03 본문은 고쳤지만 섹션 제목이 이전 의미를 남긴다
|
|
|
|
파일:
|
|
|
|
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md`
|
|
|
|
현재 제목:
|
|
|
|
> ### 2. Token Endpoint에서 비로소 클라이언트를 인증한다
|
|
|
|
그 아래 본문은 이미 다음처럼 정확하게 바뀌었다.
|
|
|
|
> client authentication도 이 요청에서 수행할 수 있다.
|
|
|
|
그리고:
|
|
|
|
> 이 프로젝트의 confidential client들은 `client_secret_basic`을 사용하므로 server-side component가 client secret으로 자신을 인증한다.
|
|
|
|
즉 본문은:
|
|
|
|
- public client가 항상 client authentication을 하는 것은 아님
|
|
- confidential client는 이 프로젝트에서 token endpoint에서 인증함
|
|
|
|
으로 고쳐졌다.
|
|
|
|
그러나 제목은 여전히 **Authorization Code Flow의 모든 client가 token endpoint에서 “클라이언트를 인증한다”**는 의미로 읽힌다.
|
|
|
|
### 수정 방향
|
|
|
|
예:
|
|
|
|
> ### 2. Client authentication이 필요한 경우 Token Endpoint에서 수행한다
|
|
|
|
또는 프로젝트 중심으로:
|
|
|
|
> ### 2. 이 프로젝트의 confidential client는 Token Endpoint에서 자신을 인증한다
|
|
|
|
첫 번째가 Reference 문서 성격에는 더 적절하다.
|
|
|
|
### 완료 조건
|
|
|
|
제목과 본문의 범위가 같아야 한다.
|
|
|
|
---
|
|
|
|
## R03 — MEDIUM — Authorization Endpoint URL이 “referrer에 남는다”는 표현이 과도하게 단정적이다
|
|
|
|
파일:
|
|
|
|
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md`
|
|
|
|
39행:
|
|
|
|
> URL이 히스토리와 서버 로그, referrer에 남고
|
|
|
|
46행:
|
|
|
|
> 링크를 타고 온 경우에는 referrer에도 남는다.
|
|
|
|
주소창, 브라우저 history, Authorization Server access log에 authorization request URL이 남을 수 있다는 설명은 타당하다.
|
|
|
|
문제는 **Referrer는 Referrer-Policy에 따라 전달 범위가 달라진다**는 점이다.
|
|
|
|
현대 브라우저의 기본 정책인 `strict-origin-when-cross-origin`에서는 일반적인 cross-origin 이동에서 전체 path/query가 아니라 origin만 보내며, downgrade에서는 referrer를 보내지 않는다. 같은 origin이거나 더 허용적인 정책에서는 path/query가 전달될 수 있다.
|
|
|
|
따라서 “authorization request URL이 referrer에 남는다”를 일반 규칙처럼 쓰면 scope가 넓다.
|
|
|
|
### 수정 방향
|
|
|
|
39행:
|
|
|
|
> Authorization Endpoint 요청 URL은 주소창과 브라우저 history, Authorization Server access log에 남을 수 있다. Referer 전달 범위는 브라우저의 Referrer-Policy와 요청 관계에 따라 달라진다.
|
|
|
|
46행:
|
|
|
|
> 같은 origin이거나 더 허용적인 Referrer-Policy에서는 path/query가 Referer에 포함될 수 있으므로, authorization request URL에 비밀값을 싣지 않는다.
|
|
|
|
이 문서의 핵심 결론인 “authorization endpoint query에 client secret을 넣지 않는다”는 유지한다.
|
|
|
|
### 완료 조건
|
|
|
|
- referrer에 full URL/query가 항상 남는다는 인상 제거
|
|
- Referrer-Policy에 따라 범위가 달라진다는 조건 명시
|
|
- 주소창/history/access log와 Referer를 같은 확실성으로 묶지 않음
|
|
|
|
---
|
|
|
|
## R04 — LOW — Figure 자동 검증은 PASS지만 context 검토 상태가 완전히 닫히지 않았다
|
|
|
|
현재 자동 결과:
|
|
|
|
`PROJECT LAYOUT: PASS — error 0 · warn 10`
|
|
|
|
keycloak warning:
|
|
|
|
- 8건: techviz 도구 부재로 전체 SSOT hash 대조 불가, context snapshot은 일치
|
|
- 2건: SSOT 문맥이 바뀐 뒤 그림을 다시 보지 않았다고 기록됨
|
|
- `final/.techviz/ap1-direct-architecture`
|
|
- `final/.techviz/login-api-phase-split`
|
|
|
|
이번 재검토에서 위 두 spec/context를 직접 읽었다.
|
|
|
|
### 직접 판독 결과
|
|
|
|
`ap1-direct-architecture`
|
|
|
|
- AP1 public SPA
|
|
- PKCE S256
|
|
- browser JS memory token custody
|
|
- Resource Server issuer/time/audience 검증
|
|
|
|
으로 현재 SSOT와 의미가 맞는다.
|
|
|
|
`login-api-phase-split`
|
|
|
|
- AP2 mediator가 token을 받음
|
|
- AP2 browser가 실제 API caller
|
|
- AP3는 BFF가 login/API 모두 담당
|
|
- AP4는 oauth2-proxy와 Nginx 책임 분리
|
|
- PKCE verifier 목록은 AP1/AP3/AP4이며 AP2를 억지로 포함하지 않음
|
|
|
|
으로 K01 수정과도 충돌하지 않는다.
|
|
|
|
따라서 **그림 내용 자체의 오류는 발견하지 않았다.**
|
|
|
|
다만 completion contract를 엄격하게 적용한다면 techviz가 남긴 stale-review 상태는 적절한 techviz review/regeneration 경로로 닫는 편이 맞다.
|
|
|
|
### 주의
|
|
|
|
- hash를 손으로 맞추지 않는다.
|
|
- SVG를 임의 수작업으로 고치지 않는다.
|
|
- techviz 정본 → spec/context → SVG 경로를 유지한다.
|
|
|
|
---
|
|
|
|
# 4. 직전 Findings 중 제대로 닫힌 항목
|
|
|
|
## K01 — CLOSED
|
|
|
|
AP3 설명 뒤에 AP2라는 주어 없이 붙어 있던 PKCE 미검증 문장이 다음처럼 수정됐다.
|
|
|
|
> 반면 AP2의 `token-mediating-confidential`은 ...
|
|
|
|
이제 다음 범위가 분명하다.
|
|
|
|
- AP3 = PKCE S256 확인
|
|
- AP2 = Authorization Code confidential client 확인, PKCE S256 미확인
|
|
|
|
SSOT와 Record의 certainty가 일치한다.
|
|
|
|
---
|
|
|
|
## K02 — CLOSED
|
|
|
|
`refreshTokenMaxReuse`가 시간 창이 아니라 reuse count라는 점이 명시됐다.
|
|
|
|
또:
|
|
|
|
- rotation
|
|
- max reuse count
|
|
- refresh token lifespan
|
|
|
|
을 서로 다른 설정으로 분리했다.
|
|
|
|
replica 동시 refresh의 실제 결과도 여전히 미검증으로 유지했다.
|
|
|
|
---
|
|
|
|
## K05 — CLOSED
|
|
|
|
기존:
|
|
|
|
> 남는 선택지는 하나다.
|
|
|
|
에서:
|
|
|
|
> 이 문서에서 비교하는 AP1~AP4 네 패턴만 놓고 보면 ...
|
|
|
|
으로 범위가 제한됐다.
|
|
|
|
OAuth/OIDC 전체 설계 공간의 유일한 답처럼 보이던 문제가 사라졌다.
|
|
|
|
---
|
|
|
|
## K06 — CLOSED
|
|
|
|
SSOT와 관련 Record에서:
|
|
|
|
> 사용자가 의도한 요청인지 확인
|
|
|
|
이라는 표현을 제거하고:
|
|
|
|
> cookie가 자동 첨부되는 state-changing request에 별도 anti-CSRF 검증 값을 요구해 cross-site forged request를 구분
|
|
|
|
하는 의미로 정리됐다.
|
|
|
|
SameSite / CSRF token / same-origin XSS의 역할도 기존 문맥과 충돌하지 않는다.
|
|
|
|
---
|
|
|
|
# 5. 자동 검증 결과
|
|
|
|
이번 재검토에서 현재 변경 상태 그대로 다시 실행했다.
|
|
|
|
| 항목 | 결과 |
|
|
|---|---|
|
|
| Tech Log Tree | PASS — records 24, topics 1, nodes 24, written 24, unwritten 0 |
|
|
| Project Layout | PASS — error 0, warn 10 |
|
|
| Record Audit | PASS — 문제 0 |
|
|
| Figure Text | PASS — SVG 10, sentence error 0 |
|
|
| Figure Provenance | PASS — error 0 |
|
|
| Figure Overlap | PASS — overlap 0 |
|
|
| Required Content | PASS — records 24, error 0 |
|
|
| SSOT Facts | PASS — error 0 |
|
|
| Natural Prose | PASS — 24/24 |
|
|
| Voice | PASS — 24/24 |
|
|
| Command Pedagogy | PASS — records 24, findings 0 |
|
|
| git diff --check | PASS |
|
|
| standalone verify-pipeline.py | PASS — exit 0 |
|
|
| full unittest regression | PASS — 391 tests, skipped 14 |
|
|
| live source reconciliation | **UNVERIFIABLE** |
|
|
|
|
pipeline과 unittest는 병렬로 돌리지 않았다.
|
|
|
|
---
|
|
|
|
# 6. 왜 자동 PASS인데 아직 완료가 아닌가
|
|
|
|
현재 자동 검사는 구조적으로 깨진 문서, tree, figure provenance, prose hard error, pipeline regression을 잘 잡는다.
|
|
|
|
하지만 다음은 자동 검사기가 보장하지 않는다.
|
|
|
|
- 한 문서의 첫 정의와 본문 정의가 의미상 서로 다른지
|
|
- 제목이 본문보다 범위를 넓히는지
|
|
- 프로젝트 구현 사실을 OAuth 일반 규칙으로 오해하게 만드는지
|
|
- Referrer-Policy 같은 조건을 생략해 문장이 과도하게 단정적인지
|
|
|
|
실제로 `check-ssot-facts.py` 자체도:
|
|
|
|
> 적힌 것이 맞는지만 본다. SSOT가 빠뜨린 finding은 찾지 못한다.
|
|
|
|
는 한계를 출력한다.
|
|
|
|
따라서 자동 PASS만으로 완료 판정을 내리지 않는다.
|
|
|
|
---
|
|
|
|
# 7. 수정 우선순위
|
|
|
|
## 1순위 — K04 완전 종료
|
|
|
|
다음 문서의 public/confidential 정의형 문장을 같은 scope로 맞춘다.
|
|
|
|
1. `final/document.md`
|
|
2. `reference-public-confidential-client.md`
|
|
3. `case-browser-credential-boundary.md`
|
|
4. `case-ap2-split-custody.md`
|
|
|
|
일반 정의와 프로젝트의 `client_secret_basic` 구현 사실을 분리한다.
|
|
|
|
## 2순위 — K03 제목 정리
|
|
|
|
`reference-authorization-code-endpoints.md`
|
|
|
|
> Token Endpoint에서 비로소 클라이언트를 인증한다
|
|
|
|
를 conditional/confidential scope가 드러나는 제목으로 바꾼다.
|
|
|
|
## 3순위 — Referrer scope
|
|
|
|
같은 Reference의 두 문장을 Referrer-Policy 조건을 포함하도록 수정한다.
|
|
|
|
## 4순위 — figure review 상태
|
|
|
|
내용은 직접 판독상 문제 없지만, 가능하면 techviz 정식 review 경로로 stale context 상태를 닫는다.
|
|
|
|
---
|
|
|
|
# 8. 수정 후 재검증 순서
|
|
|
|
1. stale client-type definition 검색
|
|
2. token endpoint heading/context 직접 판독
|
|
3. referrer 일반화 검색
|
|
4. SSOT ↔ Record semantic consistency
|
|
5. Natural prose
|
|
6. Voice
|
|
7. Tree
|
|
8. Layout
|
|
9. Figure Text / Provenance / Overlap
|
|
10. Required Content / SSOT Facts
|
|
11. Command Pedagogy
|
|
12. `git diff --check`
|
|
13. standalone `verify-pipeline.py`
|
|
14. full unittest regression
|
|
|
|
standalone pipeline과 unittest는 순차 실행한다.
|
|
|
|
---
|
|
|
|
# 9. 기술 근거
|
|
|
|
- RFC 6749 §2.1 — Client Types
|
|
- public/confidential은 client credential confidentiality 및 secure client authentication 능력 기준
|
|
- RFC 6749 Token Endpoint
|
|
- confidential client 또는 client credentials가 발급된 client가 token endpoint에서 인증
|
|
- 모든 public client가 token endpoint에서 client authentication을 하는 것은 아님
|
|
- RFC 9700 — OAuth 2.0 Security Best Current Practice
|
|
- public client PKCE 요구
|
|
- confidential client에도 PKCE 권고
|
|
- asymmetric client authentication 권고
|
|
- Keycloak RealmRepresentation
|
|
- `refreshTokenMaxReuse`는 Integer
|
|
- lifespan 계열 필드와 별도 속성
|
|
- MDN Referrer-Policy
|
|
- 기본 `strict-origin-when-cross-origin`
|
|
- same-origin과 cross-origin에서 Referer 전송 범위가 다름
|
|
|
|
---
|
|
|
|
# 최종 판정
|
|
|
|
**keycloak = 아직 더 봐야댐**
|
|
|
|
직전 리뷰를 무시하거나 새로운 방향으로 리뷰를 바꾼 것이 아니다.
|
|
|
|
오히려 직전 K01~K06을 같은 기준으로 다시 추적했을 때:
|
|
|
|
- K01 CLOSED
|
|
- K02 CLOSED
|
|
- K03 PARTIAL
|
|
- K04 PARTIAL
|
|
- K05 CLOSED
|
|
- K06 CLOSED
|
|
|
|
라는 상태다.
|
|
|
|
남은 핵심은 **public/confidential client 정의의 scope를 SSOT와 Record 전체에서 통일하는 것**, **token endpoint client authentication 제목을 본문 범위와 맞추는 것**, **Referrer-Policy 조건을 반영하는 것**이다.
|
|
|
|
이 세 축이 닫히고 동일한 전체 회귀가 다시 통과하면 그때 완료 여부를 다시 판정한다.
|