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>
135 lines
9.3 KiB
Markdown
135 lines
9.3 KiB
Markdown
---
|
|
id: 18a5cde2-dd1e-4bff-9f1c-997577ae438f
|
|
kind: QUESTION
|
|
slug: bff-session-authorized-client-store
|
|
title: BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
version: 32
|
|
questionStatus: OPEN
|
|
studio: "https://hyeonworks.com/studio/documents/18a5cde2-dd1e-4bff-9f1c-997577ae438f/edit"
|
|
public: "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를 모두 인증 경로에서 사용하게 되므로 모니터링, 장애 대응, 백업 등 운영해야 하는 저장소도 늘어난다.
|
|
저장소를 나눠서 얻는 것과 늘어나는 운영 대상을 같이 놓고 정한다.
|
|
|
|
## 다음 검증
|
|
|
|
후보마다 같은 입력으로 비교한다.
|
|
|
|
1. 인스턴스 두 대에서 로그인 유지와 재시작 복구가 되는지 본다.
|
|
2. 저장소를 직접 열어 refresh token이 평문으로 남는지 확인한다.
|
|
3. session TTL과 token 만료를 어긋나게 두고 그 순간의 응답과 화면을 기록한다.
|
|
4. logout 뒤 두 store에 잔여 항목이 없는지 확인한다.
|
|
5. 저장소를 끊은 상태에서 로그인과 API 호출이 어떤 오류를 내는지 본다.
|
|
|
|
암호화 key 교체 절차는 후보를 고른 뒤에 따로 설계한다.
|