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: 488ce49b-afa4-42a5-a2ce-de2e0653cd82
kind: CASE
slug: split-custody-access-token
title: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
topic: OAuth/OIDC 인증 경계
topic: oauth-oidc-auth-boundary
topicName: OAuth/OIDC 인증 경계
project: KeyCloak Patterns
status: 게시 중
version: 22
@@ -34,11 +35,11 @@ confidential client인 mediator가 code를 교환하고 refresh token을 server-
## 문제
Mediator에서는 Spring mediator가 confidential client가 되어 code를 교환하고 access token과 refresh token을 server-side authorized-client service에 저장한다. 브라우저에는 HttpOnly `AP2_SESSION`만 관리하게 된다.
Mediator에서는 Spring mediator가 confidential client가 되어 code를 교환하고 access token과 refresh token을 server-side authorized-client service에 저장한다. 브라우저에는 HttpOnly AP2_SESSION만 관리하게 된다.
서버에서 token을 보관한다는 점만 보면 BFF와 비슷하다. 하지만 Mediator에서는 브라우저가 Resource Server를 직접 호출한다. Resource Server를 호출하려면 access token이 필요하기 때문에 mediator가 access token을 응답으로 다시 반환한다.
처음에는 refresh token을 서버로 옮기면 브라우저가 credential을 직접 다뤄야 하는 범위도 대부분 줄어든다고 봤다. `/token/access` 응답부터 Resource Server 요청까지 따라가 보니 refresh token은 서버에 남지만 access token은 계속 브라우저에서 사용되고 있었다.
처음에는 refresh token을 서버로 옮기면 브라우저가 credential을 직접 다뤄야 하는 범위도 대부분 줄어든다고 봤다. /token/access 응답부터 Resource Server 요청까지 따라가 보니 refresh token은 서버에 남지만 access token은 계속 브라우저에서 사용되고 있었다.
## 결론
@@ -79,25 +80,25 @@ HTTP : o
## 재현 조건
1. Mediator UI에서 로그인한 뒤 `/token/boundary`를 호출한다.
1. Mediator UI에서 로그인한 뒤 /token/boundary를 호출한다.
accessTokenStored : true
refreshTokenStored : true
browserReceivesRefreshToken : false
2. `/token/access` 응답에 다음 세 key만 있는지 확인한다.
2. /token/access 응답에 다음 세 key만 있는지 확인한다.
access_token, token_type, expires_at
3. 같은 응답의 `Cache-Control``no-store`가 있는지 확인한다.
3. 같은 응답의 Cache-Controlno-store가 있는지 확인한다.
4. 반환된 access JWT를 decode해 audience에 `keycloak-pattern-api`가 있는지 확인한다.
4. 반환된 access JWT를 decode해 audience에 keycloak-pattern-api가 있는지 확인한다.
5. 브라우저가 해당 token으로 Resource Server를 직접 호출했을 때 200을 받는지 확인한다.
6. cookie가 `AP2_SESSION`이며 HttpOnly와 SameSite=Lax인지 확인한다.
6. cookie가 AP2_SESSION이며 HttpOnly와 SameSite=Lax인지 확인한다.
7. Local Storage와 Session Storage에 access token 원문이나 `refresh_token` 문자열이 없는지 확인한다.
7. Local Storage와 Session Storage에 access token 원문이나 refresh_token 문자열이 없는지 확인한다.
## 본문