The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.3 KiB
id, kind, slug, title, topic, topicName, project, status, version, questionStatus, studio, public
| id | kind | slug | title | topic | topicName | project | status | version | questionStatus | studio | public |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 18a5cde2-dd1e-4bff-9f1c-997577ae438f | QUESTION | bff-session-authorized-client-store | BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가 | oauth-oidc-auth-boundary | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 중 | 32 | OPEN | https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit | https://hyeonworks.com/questions/bff-session-authorized-client-store |
BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
Application Session과 Authorized Client는 저장하고 조회하는 기준이 서로 다르다.
Session은 session ID를 기준으로 조회하지만, Authorized Client는 client registration 이름과 principal name을 기준으로 조회한다.
그래서 두 상태를 반드시 같은 저장소에 보관해야 하는 것은 아니며, 각각의 조회 방식과 운영 요구사항에 맞게 저장 구조를 결정해야 한다.
현재 Shared Store의 후보로는 Redis를 우선 생각하고 있지만 아직 최종 저장소로 결정한 것은 아니다. 특히 Access Token과 Refresh Token을 Redis에 저장할 경우 Token을 어떤 방식으로 암호화할지, Session과 Token의 만료 시간을 어떻게 맞출지, Logout할 때 Session과 Authorized Client가 모두 정상적으로 제거되는지까지는 확인하지 않았다.
따라서 현재 단계에서는 Redis를 저장소 후보로 작성하고, Token 보호와 만료 처리, Logout 시 상태 정리까지 검증한 뒤 실제 저장 구조를 결정한다.
관계
- 서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가 이 질문에서 저장소 부분만 떼어 낸 것이다.
- Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 session과 authorized client의 열쇠가 다르다는 사실의 출처다.
- BFF 인증 구조 설계 기준 이 기준의 저장소 항목이 이 질문의 답을 기다린다.
- Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가 저장소를 공유한 뒤에야 replica 경쟁이 재현된다.
사실
- 현재 구성에 Spring Session과 Redis, JDBC repository, 암호화 token store가 없다.
- 현재 HttpSession은 servlet container의 in-memory 구현을 사용하므로 해당 process가 종료되면 session 데이터도 유지되지 않는다.
- OAuth2AuthorizedClientService도 자동구성이 고르는 in-memory 구현이다.
- session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. 두 저장 구조를 shared store로 전환할 때 각각 따로 설계해야 한다.
- authorized client manager에 authorization-code와 refresh-token provider가 함께 구성돼 있어서, 저장소를 공유하게 되면 여러 인스턴스가 같은 항목을 동시에 갱신할 수 있게 된다.
가정
- 두 상태를 같은 저장소에 둘 필요는 없다.
- 저장된 refresh token을 평문으로 두면 안 된다.
- session 만료와 token 만료 중 하나가 먼저 오게 되면 그 순간의 동작이 정의돼 있어야 한다. 예를 들면 token이 만료됐을 때 사용자의 로그인 상태가 여전히 유지되는지.
미지수
- Redis와 JDBC 중 무엇이 이 상태의 접근 패턴에 맞는가. 요청마다 읽는 값과 가끔 읽는 값이 섞여 있다.
- session과 authorized client를 같은 store에 둘지 나눌지.
- 암호화 key를 어디에 두고 어떻게 교체하게 되는가. 교체하는 동안 이전 key로 저장된 값은 어떻게 읽는가.
- session TTL과 refresh token 수명 중 어느 것을 기준으로 만료를 맞추게 되는가.
- session과 Authorized Client 두 store를 logout에서 어떻게 한 번에 지우게 되는가.
- sticky session이 durable store의 대안이 되는가 보완이 되는가.
제약
- authorized client의 조회 시 session ID가 없어서 session만 공유해도 같은 사용자의 여러 session이 같은 token을 보게 된다.
- 현재 테스트에는 저장소 관련 계약이 없다.
- 모든 UI 요청이 BFF를 지나기 때문에 저장소 지연이 화면 지연으로 바로 드러나게 된다.
선택지
1. session과 authorized client를 모두 Redis에 둔다
Spring Session Redis와 Redis 기반 Authorized Client 저장소를 사용하면 여러 애플리케이션 인스턴스가 같은 Session과 Authorized Client 정보를 조회할 수 있다. 상태가 특정 인스턴스의 메모리에 묶이지 않으므로 서버가 재시작되거나 요청이 다른 Replica로 전달되는 환경에서도 인증 상태를 공유하기 쉬워진다.
Session과 Authorized Client에 설정된 유효 시간에 따라 Redis의 TTL을 이용해 저장된 상태를 만료시키는 구조도 구성할 수 있다. 다만 어떤 상태를 얼마 동안 유지할지는 애플리케이션의 Session 정책과 OAuth Token의 수명에 맞춰 별도로 정해야 한다.
이 구성에서는 인증 경로가 Redis의 가용성에 의존하게 된다. Redis에 장애가 발생했을 때 기존 Session과 Authorized Client를 조회하지 못하는 상황을 어떻게 처리할지 정해야 하며, 장애 복구와 데이터 유지 방식도 함께 고려해야 한다.
또한 Access Token과 Refresh Token을 Redis에 저장한다면 저장된 Token을 어떤 방식으로 보호할지도 결정해야 한다. Token을 암호화해서 저장할지, 암호화한다면 Key를 어디에 보관하고 어떻게 교체할지까지 저장소 설계에 포함한다.
2. session과 authorized client를 모두 JDBC에 둔다
JDBC 기반 저장소를 사용하면 이미 운영 중인 관계형 DB에 Session과 Authorized Client 정보를 저장할 수 있다.
인증 과정에서 Session이나 Authorized Client를 조회할 때마다 DB 접근이 발생하므로, 인증 요청이 기존 관계형 DB의 가용성과 성능에 영향을 받게 된다. 요청량이 증가했을 때 Session 조회가 지연되지 않는지 확인하고, 인증 관련 조회가 기존 애플리케이션 쿼리와 서로 영향을 주지 않는지도 확인해야 한다.
또한 만료된 Session과 Authorized Client 데이터가 계속 쌓이지 않도록 정리하는 방법과 주기를 정해야 한다. JDBC를 고르면 기존 DB 운영 체계를 그대로 쓰면서 조회 지연과 DB 부하, 만료 데이터 정리까지 같이 관리하게 된다.
3. session만 공유하고 sticky session을 쓴다
Session Affinity를 사용하면 같은 사용자의 요청을 가능한 한 동일한 애플리케이션 인스턴스로 전달할 수 있으므로 기존 구조의 변경을 줄일 수 있다.
하지만 이것만으로 인증 상태가 여러 인스턴스에 공유되는 것은 아니다. Authorized Client가 여전히 특정 애플리케이션 인스턴스의 메모리에 저장되어 있다면, 요청이 다른 인스턴스로 전달되거나 해당 인스턴스가 종료되었을 때 기존 Token 정보를 조회할 수 없다.
Session Affinity는 요청을 특정 인스턴스로 보내는 방법이다. Session과 Authorized Client를 공유하는 문제와 장애 이후에 인증 상태를 유지하는 문제는 그대로 남는다. 인스턴스 장애와 Replica 간 이동까지 고려한다면 Session과 Authorized Client의 저장 방식을 별도로 설계해야 한다.
4. session은 Redis, token은 암호화한 JDBC에 둔다
Session과 Authorized Client를 서로 다른 저장소에 보관하는 방법도 있다. 요청마다 자주 조회되는 Session은 Redis와 같이 빠르게 접근할 수 있는 저장소에 두고, Access Token과 Refresh Token은 암호화와 장기 보관 정책을 적용하기 쉬운 관계형 DB에 저장할 수 있다. 각 데이터의 접근 패턴과 보호 요구사항에 맞춰 저장소를 선택할 수 있다는 장점이 있다.
두 저장소의 상태는 함께 관리해야 한다. Session이 만료되었는데 Authorized Client가 남거나, 반대로 Authorized Client가 먼저 제거되어 유효한 Session에서 Token을 찾지 못하는 상황이 발생할 수 있다. 따라서 각각의 만료 정책을 어떻게 맞출지 정해야 한다.
Logout에서도 Session과 Authorized Client가 서로 다른 저장소에 있으므로 두 상태를 모두 정리해야 한다. 한쪽을 제거하는 과정에서 실패했을 때 어떻게 처리할지도 함께 결정해야 한다.
또한 Redis와 관계형 DB를 모두 인증 경로에서 사용하게 되므로 모니터링, 장애 대응, 백업 등 운영해야 하는 저장소도 늘어난다. 저장소를 나눠서 얻는 것과 늘어나는 운영 대상을 같이 놓고 정한다.
다음 검증
후보마다 같은 입력으로 비교한다.
- 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
- 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.
- session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
- logout 뒤 두 store에 잔여 항목이 없는지 확인한다.
- 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.