docs(keycloak-session-store): import the session-storage lab as a new project

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>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -3,7 +3,8 @@ id: 97eddd97-1096-426a-a2c6-a6c5bf1cd09f
kind: REFERENCE
slug: bff-authentication-design-criteria
title: BFF 인증 구조 설계 기준
topic: OAuth/OIDC 인증 경계
topic: oauth-oidc-auth-boundary
topicName: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 21
@@ -43,7 +44,7 @@ cookie가 credential이 되면 브라우저가 요청마다 자동으로 붙여
Access Token과 Refresh Token은 BFF 서버의 Authorized Client에 보관한다.
브라우저는 OAuth Token을 직접 사용하지 않고 Session Cookie를 이용해 BFF에 요청한다.
BFF는 이 Session을 확인한 뒤, 저장해 둔 Access Token으로 `Authorization: Bearer ...` 헤더를 새로 만들어 Resource Server를 호출한다. 브라우저가 보낸 Session Cookie는 Resource Server로 전달되지 않는다. Session Cookie는 브라우저와 BFF 사이의 Credential이고, Access Token은 BFF와 Resource Server 사이의 Credential이다.
BFF는 이 Session을 확인한 뒤, 저장해 둔 Access Token으로 Authorization: Bearer ... 헤더를 새로 만들어 Resource Server를 호출한다. 브라우저가 보낸 Session Cookie는 Resource Server로 전달되지 않는다. Session Cookie는 브라우저와 BFF 사이의 Credential이고, Access Token은 BFF와 Resource Server 사이의 Credential이다.
### 2. cookie가 credential이면 상태 변경 요청에 CSRF 검증을 둔다
@@ -55,21 +56,21 @@ BFF 구조에서는 브라우저가 요청할 때 Session Cookie를 자동으로
이때 화면이나 응답 본문에 표시되는 CSRF Token과 실제 요청 Header에 넣어야 하는 값이 항상 같다고 생각하면 안 된다.
응답 본문에 노출된 값이 별도의 처리를 거친 값이라면, 클라이언트는 실제 CSRF Cookie에서 값을 읽어 Header에 넣어야 한다.
잘못된 값을 보내면 정상적인 요청이라도 CSRF 검증에 실패해 `403 Forbidden` 응답을 받게 된다.
잘못된 값을 보내면 정상적인 요청이라도 CSRF 검증에 실패해 403 Forbidden 응답을 받게 된다.
`SameSite`와 CSRF Token도 서로 다른 역할을 한다.
`SameSite`는 브라우저가 Cross-Site 요청에 Cookie를 전송할지 제한하는 정책이고, CSRF Token은 Cookie가 포함되어 들어온 상태 변경 요청이 정상적인 클라이언트에서 만들어졌는지를 확인하기 위한 값이다.
SameSite와 CSRF Token도 서로 다른 역할을 한다.
SameSite는 브라우저가 Cross-Site 요청에 Cookie를 전송할지 제한하는 정책이고, CSRF Token은 Cookie가 포함되어 들어온 상태 변경 요청이 정상적인 클라이언트에서 만들어졌는지를 확인하기 위한 값이다.
또한 SameSite는 Origin이 아니라 Site를 기준으로 판단하므로, Origin은 다르지만 같은 Site에 속하는 요청도 존재할 수 있다.
### 3. session과 authorized client의 수명주기를 따로 설계한다
Application Session과 Authorized Client는 서로 다른 값을 저장하고 조회한다.
Session은 `session ID`를 기준으로 조회하지만, Authorized Client는 `client registration 이름``principal name`을 기준으로 조회한다.
Session은 session ID를 기준으로 조회하지만, Authorized Client는 client registration 이름principal name을 기준으로 조회한다.
따라서 여러 인스턴스에서 상태를 공유하기 위해 Shared Store를 도입할 때도 Session 저장소와 Authorized Client 저장소를 각각 어떻게 구성할지 확인해야 한다.
특히 Authorized Client의 조회 기준에는 `session ID`가 포함되지 않는다.
특히 Authorized Client의 조회 기준에는 session ID가 포함되지 않는다.
그래서 같은 사용자가 두 브라우저에서 동일한 Client로 로그인하면 두 Session이 같은 Authorized Client 정보를 사용하거나, 나중에 로그인하면서 저장된 Token 정보가 갱신될 수 있다.
브라우저나 Session마다 서로 다른 Token을 유지해야 한다면 `session ID`까지 포함해 Token을 구분할 수 있도록 별도의 저장 구조를 설계해야 한다.
브라우저나 Session마다 서로 다른 Token을 유지해야 한다면 session ID까지 포함해 Token을 구분할 수 있도록 별도의 저장 구조를 설계해야 한다.
운영 환경에서는 서버가 재시작되거나 요청이 다른 Replica로 전달되더라도 로그인 상태와 Token을 계속 사용할 수 있는지도 고려해야 한다. 이를 위해 Session과 Authorized Client를 공유 저장소에 보관할지, Session Affinity를 사용할지 등을 결정해야 한다.
Token을 외부 저장소에 보관한다면 Access Token과 Refresh Token을 어떻게 보호할지도 정해야 하며, 저장 시 암호화한다면 암호화 Key의 보관 위치와 교체 방법까지 함께 설계해야 한다.
@@ -81,8 +82,8 @@ Session과 Authorized Client는 조회 기준과 저장소가 다르므로, Logo
### 4. Downstream 오류를 클라이언트 응답으로 변환한다
BFF가 Resource Server의 오류를 그대로 브라우저에 전달하면 화면에서는 오류의 원인을 일관되게 판단하기 힘들다.
예를 들어 Resource Server에서 `401 Unauthorized`가 발생했다면 Access Token이 만료되었거나 더 이상 유효하지 않은 상황인지 확인하고, 필요한 경우 Token 갱신이나 재로그인으로 연결해야 한다.
하지만 인증은 정상적으로 되었지만 해당 기능을 사용할 권한이 없어 `403 Forbidden`이 발생한 경우에는 권한 부족으로 처리해야 한다.
예를 들어 Resource Server에서 401 Unauthorized가 발생했다면 Access Token이 만료되었거나 더 이상 유효하지 않은 상황인지 확인하고, 필요한 경우 Token 갱신이나 재로그인으로 연결해야 한다.
하지만 인증은 정상적으로 되었지만 해당 기능을 사용할 권한이 없어 403 Forbidden이 발생한 경우에는 권한 부족으로 처리해야 한다.
Resource Server가 응답하지 않거나 처리가 지연되는 경우도 별도의 규칙이 필요하다.
요청을 얼마 동안 기다릴지 Timeout을 정하고, 실패한 요청을 다시 시도할 수 있는 경우에는 Retry 정책을 적용한다.