refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -0,0 +1,373 @@
# keycloak 상세 리뷰 — 2026-09-18
## 판정
**keycloak = 아직 더 봐야댐**
자동 품질 게이트는 대부분 통과하지만, OAuth/OIDC 의미를 일반화하는 과정에서 기술적 범위 오류가 남아 있다. 특히 AP2/AP3 PKCE 범위, Keycloak refresh-token 재사용 설정, public/confidential client와 token endpoint caller의 관계는 수정이 필요하다.
현재 source repository:
`/home/donghyeon/workspace/keycloak-pattern`
는 이 머신에 존재하지 않는다. 따라서 live source reconciliation은 **UNVERIFIABLE**이다. 현재 판정은 커밋된 SSOT/Record/evidence와 외부 표준·제품 문서 사이의 의미 검토 결과이며, 실제 source code와의 최신 일치 여부를 PASS로 간주하지 않는다.
---
## 1. 현재 통과한 자동 검증
| 항목 | 결과 |
|---|---|
| Tech Log Tree | PASS — topics 1, nodes 24, written 24, unwritten 0 |
| Project Layout | PASS — error 0, warn 10 |
| Record Audit | PASS — 문제 없음 |
| Figure Text | PASS — SVG 10, 문장 오류 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 hard fail 0 |
| Voice | PASS — 24/24 OK |
| Command Pedagogy | PASS — records 24, findings 0 |
| Standalone pipeline baseline | PASS — exit 0 |
| Full unittest regression baseline | PASS — 391 tests, skipped 14 |
| git diff --check baseline | PASS |
| Live source reconciliation | **UNVERIFIABLE** — source repository 없음 |
주의: 위 PASS는 자동 검사기가 검사하는 계약에 대한 결과다. 기술 의미의 과도한 일반화나 문장 주어 누락까지 자동으로 잡는 것은 아니다.
---
# Findings
## K01 — HIGH — AP3 설명 직후 AP2 미검증 문장이 붙어 PKCE 의미가 충돌한다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-authorization-code-and-pkce.md`
현재 120~122행의 흐름:
- 120행: AP3의 `bff-confidential``client_secret_basic` + PKCE S256을 함께 사용한다고 설명한다.
- 122행: 주어 없이 “클라이언트 설정에 S256 강제 속성이 없고 테스트도 challenge를 검사하지 않는다”고 이어진다.
이 마지막 문장은 SSOT 기준으로 **AP2**를 가리킨다. 실제 SSOT는 AP2에 대해 다음처럼 범위를 제한한다.
- AP2 client 설정에는 S256을 강제하는 속성이 없음
- AP2 E2E도 authorization request challenge를 검사하지 않음
- 따라서 AP2는 Authorization Code confidential client까지 확인했고 PKCE S256은 확인하지 않음
Record에서는 `AP2`라는 주어가 빠져 바로 앞 AP3 설명을 뒤집는 문장처럼 읽힌다.
### 수정 방향
예:
> 반면 AP2의 `token-mediating-confidential`은 client 설정에서 S256을 강제하지 않고 AP2 E2E도 authorization request의 challenge를 검사하지 않는다. 따라서 AP2는 Authorization Code를 사용하는 confidential client라는 사실까지 확인했으며, PKCE S256이 고정되었다고 기록하지 않는다.
### 완료 조건
- AP3 PKCE S256 = 확인된 사실
- AP2 PKCE S256 = 미확인
- 두 범위가 같은 Concept 안에서 주어가 분명하게 분리되어야 한다.
---
## K02 — HIGH — Keycloak refresh-token “재사용 허용”을 시간 창처럼 설명한다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/question/question-refresh-rotation-replica.md`
현재 99~103행:
> 재사용을 짧게 허용하면 ...
>
> 허용한 시간 안에는 훔친 refresh token이 ...
문제는 Keycloak의 `refreshTokenMaxReuse`가 **시간(duration) 설정이 아니라 정수형 reuse count**라는 점이다. Keycloak의 현재 API 모델도 `refreshTokenMaxReuse: Integer`로 노출한다. `Revoke Refresh Token`은 사용한 refresh token을 회전시키는 동작이며, timeout과 max reuse를 같은 “허용 시간”으로 설명하면 안 된다.
### 수정 방향
시간 표현을 제거하고 reuse count로 쓴다.
예:
> 재사용 허용 횟수를 늘리면 동일 refresh token의 추가 사용을 일정 횟수 받아들이도록 구성할 수 있다. 그만큼 탈취된 refresh token의 replay를 허용할 여지도 커진다. 현재 실험은 rotation + max reuse 0을 전제로 하므로 이 선택지는 제외한다.
정확한 Keycloak 26.7.0 동작을 source config와 runtime으로 다시 확인할 수 있을 때에는 실제 realm 값과 동시 refresh 결과를 별도 evidence로 남긴다.
### 완료 조건
- `refreshTokenMaxReuse`를 시간 창처럼 설명하는 문장 0건
- rotation / token lifespan / max reuse count를 서로 다른 설정으로 구분
- replica race 결과는 계속 **미검증**으로 유지
---
## K03 — HIGH — “client type이 token endpoint caller를 결정한다”는 인과가 잘못됐다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-authorization-code-endpoints.md`
현재 40, 56행의 핵심:
> 보내는 쪽은 클라이언트 종류에 따라 서버이거나 브라우저
>
> Token Endpoint를 서버 안에서만 부르게 하려면 클라이언트 종류부터 confidential client로 정해야 한다.
이 프로젝트의 네 구현에서는 실제로:
- AP1 public SPA → browser가 token endpoint 호출
- AP2/AP3/AP4 confidential component → server-side component가 code 교환
이라는 배치가 맞다.
하지만 이것은 **이 프로젝트의 아키텍처 배치**이지 OAuth의 client type 자체가 network caller를 결정한다는 뜻이 아니다.
OAuth의 public/confidential 구분은 authorization server에 대해 안전하게 client authentication을 수행하고 client credential의 기밀성을 유지할 수 있는지에 대한 분류다. 실제 token exchange를 browser에서 할지 server에서 할지는 callback/code-exchange ownership과 배포 구조가 정한다.
### 수정 방향
일반 규칙과 프로젝트 사실을 분리한다.
예:
> 이 프로젝트에서는 AP1 public SPA가 브라우저에서 token endpoint를 호출하고, AP2~AP4의 confidential component가 server-side에서 code를 교환한다. 다만 public/confidential client 구분 자체가 token endpoint의 호출 위치를 강제하는 것은 아니다. 서버 전용 교환은 callback과 code exchange를 server component가 소유하고, client credential이 browser로 노출되지 않도록 설계함으로써 만든다.
### 완료 조건
- “client type → caller 위치” 직접 인과 제거
- “현재 네 패턴에서의 배치”와 “OAuth 일반 규칙”을 별도 문장으로 구분
---
## K04 — MEDIUM — Public/Confidential 정의를 client secret 하나로 축소했다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-public-confidential-client.md`
현재 23, 41, 47, 51행 주변에서 반복적으로:
> 클라이언트 종류는 클라이언트 시크릿을 안전하게 보관할 수 있는지로 정한다.
이 설명은 **현재 프로젝트가 shared client secret을 사용한다는 범위**에서는 이해하기 쉽지만, 일반 OAuth Reference의 정의로는 너무 좁다.
RFC 6749의 기준은 client credential의 confidentiality 및 authorization server와의 secure client authentication 능력이다. confidential client 인증 수단은 shared secret만이 아니라 private key, mTLS 같은 방식도 가능하다. RFC 9700도 asymmetric client authentication을 권고한다.
또 38~39행의:
> PKCE를 쓸지, 클라이언트 시크릿으로 클라이언트를 인증할지 정할 수 있다
는 표현은 둘 중 하나를 고르는 것처럼 읽힐 수 있다. 같은 문서 뒤쪽에서 confidential client도 PKCE를 함께 쓸 수 있다고 올바르게 설명하고 있으므로 앞부분도 그 의미와 맞춰야 한다.
### 수정 방향
일반 정의:
> OAuth client type은 authorization server에 대해 client credential을 안전하게 보호하고 신뢰할 수 있는 client authentication을 수행할 수 있는지로 구분한다.
프로젝트 scope:
> 이 프로젝트의 confidential client들은 `client_secret_basic`을 사용하므로, 여기서는 server-side client secret 보관 여부가 그 차이를 가장 직접적으로 보여 준다.
PKCE:
> PKCE와 client authentication은 대체 관계가 아니다. public client에는 PKCE가 필수적인 보호 수단이고, confidential client에도 함께 적용할 수 있다.
### 완료 조건
- 일반 정의에서 “client secret만”이 client type의 유일한 기준처럼 읽히지 않음
- 현재 프로젝트의 `client_secret_basic` 사용은 별도 scope로 명시
- PKCE vs client authentication을 양자택일처럼 읽히게 하는 문장 제거
---
## K05 — MEDIUM — “남는 선택지는 하나”가 문서 범위를 넘어 일반화된다
파일:
`docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/reference/reference-pattern-selection.md`
현재 94행:
> 브라우저에 토큰을 둘 수 없고 서버가 API를 조합해야 하면 남는 선택지는 하나다.
이 문서가 비교하는 **AP1~AP4 네 패턴 안에서만** 보면 의도는 AP3 BFF다. 그러나 Reference 문장 자체는 OAuth/OIDC 전체 설계 공간에서 유일한 답인 것처럼 읽힌다.
### 수정 방향
> 이 문서에서 비교하는 네 패턴만 놓고 보면, 브라우저에 OAuth token을 둘 수 없고 server-side API composition이 필요할 때 AP3 BFF가 해당 조건을 만족한다.
### 완료 조건
- 선택의 유일성이 “이 문서의 네 패턴” 범위로 제한됨
- 외부 구조 전체에 대한 일반론으로 확대되지 않음
---
## K06 — MEDIUM — CSRF token을 “사용자 의도 증명”으로 설명한다
대표 파일:
- `concept/concept-cookie-auth-csrf.md`
- `concept/concept-browser-credential-storage.md`
- `reference/reference-bff-auth-design.md`
- `reference/reference-token-vs-session.md`
- `question/question-edge-authorization-scope.md`
- SSOT `final/document.md`에도 같은 표현이 있음
현재 반복 표현:
> 상태를 바꾸는 요청이 사용자가 의도한 것인지 확인한다.
CSRF token이 증명하는 것은 인간 사용자의 실제 의도 자체가 아니다. 예상한 anti-CSRF token을 가진 클라이언트 문맥에서 요청이 만들어졌는지를 검사해 cross-site forged request를 구분하는 방어선에 가깝다.
현재 문서 자체도 뒤쪽에서 **same-origin XSS가 XSRF-TOKEN을 읽고 사용자의 세션으로 요청할 수 있다**고 정확하게 설명한다. 따라서 “사용자 의도 확인” 표현은 같은 문서의 XSS 설명과도 의미적으로 충돌한다.
### 수정 방향
다음처럼 바꾼다.
> 브라우저가 session cookie를 자동 첨부하므로, state-changing request에는 공격 사이트가 cookie만 이용해 만든 cross-site forged request와 애플리케이션이 anti-CSRF token을 가지고 만든 요청을 구분하는 CSRF 검증이 필요하다.
또는 더 짧게:
> CSRF token은 cookie가 자동으로 붙는 요청에 별도의 검증 값을 요구해 cross-site forged request를 차단한다.
### 완료 조건
- “사용자의 실제 의도 자체를 증명한다”는 의미 제거
- SameSite / CSRF token / same-origin XSS 역할이 서로 구분됨
- SSOT와 파생 Record를 함께 수정하여 semantic consistency 유지
---
# SVG / Figure 검토
현재 10개 SVG는 자동 검증상 다음을 통과한다.
- Figure Text PASS
- Figure Provenance PASS
- Figure Overlap PASS
직접 VizSpec의 title, actor, edge label도 확인했다. 현재 내용과 그림의 **주제 선택 자체는 적절하다**.
특히 다음 연결은 독자 설명 흐름에 유효하다.
- AP1: browser memory ↔ Keycloak ↔ Resource Server
- AP2: mediator custody와 browser API caller 분리
- AP3: browser session → BFF → authorized client → Bearer
- AP3 CSRF: masked body vs raw cookie/header
- AP4: Nginx auth_request → oauth2-proxy → trusted upstream header
- login/API phase split: credential owner와 API caller가 다른 축이라는 설명
다만 Project Layout에는 warn 10건이 남아 있다.
- 7건: **SSOT 문맥이 바뀐 뒤 그림을 다시 보지 않았다**
- 3건: techviz 도구 부재로 **전체 SSOT hash를 대조하지 못함**, context snapshot은 일치
따라서 SVG를 현재 당장 “틀린 그림”으로 판정하지는 않는다. 하지만 K01~K06 수정으로 주변 문맥이 다시 바뀌므로 수정 후에는 10개 figure context/provenance를 다시 검증해야 한다. 특히 PKCE 범위를 건드리는 경우 `login-api-phase-split`과 AP2 관련 그림의 label이 AP2 PKCE를 암시하지 않는지 다시 확인한다.
---
# Evidence / Provenance 검토
좋은 점:
1. 최신 실행 성적표와 “커밋된 테스트가 확인하도록 정의한 acceptance contract”를 구분한다.
2. AP3의 `browserTokenCount: 0` self-report만으로 tokenless browser를 증명하지 않고 browser network/Web Storage를 별도 evidence로 둔다.
3. AP4에서 exact `/api/edge` 401과 general location redirect의 범위를 구분한다.
4. mock OIDC provider 검증과 실제 Google account/public HTTPS callback 검증을 분리한다.
5. AP2 PKCE를 SSOT에서는 미확인으로 유지한다.
6. source repository 부재 시 live reconciliation을 꾸며낼 근거가 없다.
남은 제약:
- source repository가 없으므로 현재 Record의 source-derived 세부 사실을 live code와 재대조할 수 없다.
- historical `verifiedOn`과 source revision은 현재 runtime 실행을 뜻하지 않는다.
- figure context warning 10건은 hard failure는 아니지만 수정 완료 후 다시 닫아야 한다.
---
# SSOT ↔ Record semantic consistency
현재 가장 명확한 mismatch는 K01이다.
SSOT:
> AP2 client 설정에는 S256을 강제하는 속성이 없고 AP2 E2E도 challenge를 검사하지 않는다.
Record:
> AP3 PKCE S256 설명 직후 주어 없이 “S256을 강제하지 않고 검사하지 않았다”고 적음.
따라서 Record가 SSOT의 scope를 잃었다.
K02, K03, K04, K05는 SSOT에서 직접 강하게 주장한 내용이라기보다 Reference/Question으로 확장하는 과정에서 범위가 넓어진 문제다.
K06은 반대로 SSOT와 여러 Record가 같은 부정확한 표현을 공유하므로 **SSOT부터 정정한 뒤 파생 Record를 같이 고쳐야 한다.**
---
# 수정 우선순위
## 1차 — 반드시 수정
1. K01 AP2/AP3 PKCE scope
2. K02 refresh max reuse를 time window로 표현한 부분
3. K03 client type과 token endpoint caller의 잘못된 인과
## 2차 — 같은 수정 묶음에서 처리
4. K04 public/confidential 일반 정의 scope
5. K05 “남는 선택지는 하나” scope
6. K06 CSRF “사용자 의도” 표현
## 3차 — 수정 후 재검증
1. stale phrase 검색
2. SSOT ↔ Record semantic consistency
3. Natural prose / Voice
4. Tree / Layout
5. Figure Text / Figure Provenance / Figure Overlap
6. Required Content / SSOT Facts
7. Command Pedagogy
8. `git diff --check`
9. standalone `verify-pipeline.py`
10. full unittest regression
pipeline과 unittest는 병렬 실행하지 않는다.
---
# 외부 기술 근거
검토 시 다음 공식 문서/표준을 기준으로 의미를 대조했다.
- RFC 6749 §2.1 Client Types
https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
- RFC 9700 OAuth 2.0 Security Best Current Practice
https://www.rfc-editor.org/rfc/rfc9700.html
- Keycloak Server Administration Guide 26.7.x — Refresh token rotation
https://www.keycloak.org/docs/latest/server_admin/
- Keycloak `RealmRepresentation.refreshTokenMaxReuse` API
https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/representations/idm/RealmRepresentation.html
- Spring Security `OAuth2AuthorizedClientService` API
https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientService.html
- oauth2-proxy session options
https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview/
RFC 6749는 confidential client를 shared secret 자체가 아니라 client credentials의 기밀성 유지 또는 다른 안전한 client authentication 능력으로 정의한다. RFC 9700은 public client에 PKCE를 요구하고 confidential client에도 PKCE를 권고하며, asymmetric client authentication도 권고한다. Keycloak의 current API는 `refreshTokenMaxReuse`를 Integer로 표현한다.
---
# 최종 판정
**keycloak = 아직 더 봐야댐**
자동 검사기는 통과했지만 K01~K03은 기술 의미를 바꾸는 문제이므로 완료로 볼 수 없다. K04~K06도 Reference/Concept의 일반화 범위를 바로잡아야 한다.
수정 후에는 대상 문서만 다시 읽는 것으로 끝내지 않고, SSOT/Record/figure context와 전체 pipeline + unittest regression까지 다시 확인해야 한다.